@polymorphism-tech/morph-spec 9.0.0 → 9.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +8 -9
- package/README.md +38 -36
- package/bin/morph-spec.js +11 -63
- package/bin/validate.js +1 -39
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +2 -1
- package/framework/MORPH.md +3 -3
- package/framework/agents.json +19 -11
- package/framework/commands/morph-apply.md +1 -1
- package/framework/commands/morph-status.md +1 -1
- package/framework/docs/CLI.md +22 -31
- package/framework/evals/review-checklist.md +12 -2
- package/framework/hooks/README.md +8 -44
- package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +6 -49
- package/framework/hooks/claude-code/core/block-raw-worktree-add.js +2 -35
- package/framework/hooks/claude-code/core/chain.js +4 -65
- package/framework/hooks/claude-code/core/comment-guard.js +2 -16
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +6 -50
- package/framework/hooks/claude-code/core/gate-guard.js +7 -67
- package/framework/hooks/claude-code/core/graph-refresh-kick.js +3 -24
- package/framework/hooks/claude-code/core/heavy-gate.js +3 -21
- package/framework/hooks/claude-code/core/heavy-release.js +2 -13
- package/framework/hooks/claude-code/core/loop-detect.js +2 -31
- package/framework/hooks/claude-code/core/pre-bash-denylist.js +0 -4
- package/framework/hooks/claude-code/core/protect-spec-files.js +7 -67
- package/framework/hooks/claude-code/core/state-sync.js +12 -68
- package/framework/hooks/claude-code/core/telemetry-log.js +12 -93
- package/framework/hooks/claude-code/core/validate-completion.js +11 -57
- package/framework/hooks/claude-code/core/warn-bash-source-write.js +1 -9
- package/framework/hooks/claude-code/core/worktree-context.js +10 -110
- package/framework/hooks/claude-code/core/worktree-create.js +6 -70
- package/framework/hooks/claude-code/core/worktree-env-inject.js +5 -56
- package/framework/hooks/claude-code/core/worktree-remove.js +5 -31
- package/framework/hooks/claude-code/statusline.sh +11 -11
- package/framework/hooks/shared/activity-logger.js +9 -89
- package/framework/hooks/shared/agent-reap.js +2 -17
- package/framework/hooks/shared/build-check.js +6 -75
- package/framework/hooks/shared/claim-registry.js +9 -119
- package/framework/hooks/shared/command-tokens.js +6 -51
- package/framework/hooks/shared/comment-rules.js +5 -20
- package/framework/hooks/shared/comment-scanner.js +6 -25
- package/framework/hooks/shared/comment-stripper.js +11 -85
- package/framework/hooks/shared/e2e-check.js +0 -0
- package/framework/hooks/shared/errata.js +2 -15
- package/framework/hooks/shared/feature-locator.js +11 -95
- package/framework/hooks/shared/frontend-locations.js +1 -3
- package/framework/hooks/shared/gate-question.js +1 -7
- package/framework/hooks/shared/git-exclude.js +3 -20
- package/framework/hooks/shared/git-paths.js +8 -104
- package/framework/hooks/shared/graph-freshness.js +22 -166
- package/framework/hooks/shared/graph-refresh.js +21 -149
- package/framework/hooks/shared/heavy-command.js +4 -12
- package/framework/hooks/shared/hook-response.js +8 -83
- package/framework/hooks/shared/morph-cli.js +8 -52
- package/framework/hooks/shared/payload-utils.js +10 -132
- package/framework/hooks/shared/phase-utils.js +1 -42
- package/framework/hooks/shared/redact-secrets.js +4 -15
- package/framework/hooks/shared/reparse-scan.js +6 -66
- package/framework/hooks/shared/slice-progress.js +4 -23
- package/framework/hooks/shared/source-files.js +4 -17
- package/framework/hooks/shared/state-reader.js +13 -183
- package/framework/hooks/shared/state-writer.js +8 -52
- package/framework/hooks/shared/stdin-reader.js +1 -11
- package/framework/hooks/shared/telemetry-logger.js +7 -67
- package/framework/hooks/shared/transcript-usage.js +7 -29
- package/framework/hooks/shared/worktree-root.js +7 -29
- package/framework/rules/csharp-standards.md +1 -1
- package/framework/rules/frontend-standards.md +1 -0
- package/framework/rules/hangfire-standards.md +1 -1
- package/framework/rules/neon-standards.md +1 -1
- package/framework/rules/nextjs-standards.md +1 -1
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +4 -4
- package/framework/skills/core/morph-hotfix/SKILL.md +3 -3
- package/framework/skills/core/morph-machine/SKILL.md +1 -1
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +9 -7
- package/framework/skills/core/morph-review/SKILL.md +9 -6
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +29 -12
- package/framework/standards/STANDARDS.json +36 -14
- package/framework/standards/STANDARDS.meta.json +16 -2
- package/framework/standards/ai-agents/agent-archetypes.md +2 -2
- package/framework/standards/ai-agents/agent-session.md +1 -1
- package/framework/standards/ai-agents/conversational-agent-with-phases.md +4 -4
- package/framework/standards/ai-agents/durable-workflows-hitl.md +3 -3
- package/framework/standards/ai-agents/guardrails.md +1 -1
- package/framework/standards/ai-agents/multi-agent-patterns.md +2 -2
- package/framework/standards/ai-agents/observability-patterns.md +1 -1
- package/framework/standards/ai-agents/production.md +1 -1
- package/framework/standards/ai-agents/prompt-sources.md +2 -2
- package/framework/standards/ai-agents/providers/model-registry.md +1 -1
- package/framework/standards/ai-agents/rag-custom-pgvector.md +2 -2
- package/framework/standards/ai-agents/setup.md +2 -2
- package/framework/standards/ai-agents/sweet-spot.md +2 -2
- package/framework/standards/ai-agents/tool-adapters.md +0 -2
- package/framework/standards/ai-agents/vector-data-extensions.md +4 -4
- package/framework/standards/ai-agents/workflows.md +1 -1
- package/framework/standards/architecture/code-comments.md +1 -1
- package/framework/standards/architecture/vertical-slice/vertical-slice.md +1 -1
- package/framework/standards/backend/api/validation.md +1 -1
- package/framework/standards/backend/dotnet/testing.md +10 -4
- package/framework/standards/backend/dotnet/vsa-handler-patterns.md +5 -5
- package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +1 -1
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -2
- package/framework/standards/data/nosql/cache/redis.md +1 -1
- package/framework/standards/data/nosql/cosmos-db.md +3 -3
- package/framework/standards/frontend/admin-app/forms.md +1 -1
- package/framework/standards/frontend/design-system/art-direction.md +88 -0
- package/framework/standards/frontend/design-system/premium-finish.md +77 -2
- package/framework/standards/frontend/nextjs/testing.md +10 -6
- package/framework/standards/infrastructure/neon/neon-pgvector.md +5 -5
- package/framework/templates/.idea/morph-templates.xml +92 -0
- package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
- package/framework/templates/CLAUDE.md.core.template +3 -3
- package/framework/templates/IDE-SNIPPETS.md +262 -262
- package/framework/templates/REGISTRY.json +9 -9
- package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
- package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
- package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
- package/framework/templates/docs/plan.md +3 -1
- package/framework/templates/docs/ui-mockups.md +4 -0
- package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
- package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
- package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
- package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
- package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
- package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
- package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
- package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
- package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
- package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
- package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
- package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
- package/framework/templates/state.template.json +17 -17
- package/framework/templates/ui/design-system.css +226 -226
- package/package.json +1 -1
- package/src/commands/context/context-refresh.js +0 -16
- package/src/commands/dashboard/dashboard.js +2 -8
- package/src/commands/e2e/e2e.js +17 -104
- package/src/commands/graph/affected.js +0 -14
- package/src/commands/graph/build.js +11 -50
- package/src/commands/graph/explain.js +0 -16
- package/src/commands/graph/status.js +1 -12
- package/src/commands/mcp/mcp-setup.js +4 -62
- package/src/commands/observability/cost.js +5 -57
- package/src/commands/observability/telemetry.js +1 -14
- package/src/commands/project/branch.js +4 -45
- package/src/commands/project/claim.js +4 -48
- package/src/commands/project/doctor.js +48 -302
- package/src/commands/project/env.js +1 -27
- package/src/commands/project/fleet.js +3 -32
- package/src/commands/project/init.js +4 -68
- package/src/commands/project/install-plugin-cmd.js +0 -1
- package/src/commands/project/standards.js +1 -62
- package/src/commands/project/status.js +0 -53
- package/src/commands/project/update.js +31 -239
- package/src/commands/project/worktree.js +108 -882
- package/src/commands/scaffold/scaffold.js +0 -25
- package/src/commands/state/advance.js +14 -85
- package/src/commands/state/approve.js +15 -97
- package/src/commands/state/archive.js +12 -64
- package/src/commands/state/auto-approval.js +2 -5
- package/src/commands/state/create.js +22 -109
- package/src/commands/state/delete.js +11 -63
- package/src/commands/state/finish.js +98 -475
- package/src/commands/state/gate-check.js +22 -132
- package/src/commands/state/gate-decision.js +3 -39
- package/src/commands/state/handoff.js +3 -30
- package/src/commands/state/list.js +14 -84
- package/src/commands/state/report-check.js +8 -58
- package/src/commands/state/retype.js +4 -26
- package/src/commands/validation/index.js +0 -3
- package/src/commands/validation/mutate.js +11 -11
- package/src/commands/validation/plan-check.js +4 -2
- package/src/commands/validation/tests-audit.js +7 -0
- package/src/commands/validation/validate-feature.js +10 -97
- package/src/commands/validation/verify.js +30 -113
- package/src/core/paths/output-schema.js +2 -101
- package/src/core/state/feature-identity.js +1 -25
- package/src/core/state/gate-order.js +0 -27
- package/src/core/state/lifecycle-audit.js +8 -100
- package/src/core/state/state-manager.js +22 -304
- package/src/core/templates/template-registry.js +2 -97
- package/src/core/templates/template-renderer.js +13 -93
- package/src/core/templates/template-seed.js +0 -1
- package/src/core/work-types.js +1 -36
- package/src/dashboard/app/App.jsx +1 -8
- package/src/dashboard/app/components/ActivityFeed.jsx +0 -5
- package/src/dashboard/app/components/CommandPalette.jsx +0 -7
- package/src/dashboard/app/components/DetailPanel.jsx +1 -5
- package/src/dashboard/app/components/EntityLink.jsx +0 -12
- package/src/dashboard/app/components/ErrorBoundary.jsx +1 -8
- package/src/dashboard/app/components/KnowledgeMarkdown.jsx +0 -11
- package/src/dashboard/app/components/Markdown.jsx +0 -9
- package/src/dashboard/app/components/PhaseRail.jsx +0 -11
- package/src/dashboard/app/components/SearchFilter.jsx +0 -4
- package/src/dashboard/app/components/Sidebar.jsx +0 -5
- package/src/dashboard/app/hooks/useMorphQuery.js +1 -11
- package/src/dashboard/app/hooks/usePref.js +1 -5
- package/src/dashboard/app/lib/format.js +0 -2
- package/src/dashboard/app/lib/phases.js +1 -5
- package/src/dashboard/app/lib/project.js +0 -12
- package/src/dashboard/app/main.jsx +13 -13
- package/src/dashboard/app/pages/AgentMap.jsx +1 -8
- package/src/dashboard/app/pages/FeatureDetail.jsx +1 -5
- package/src/dashboard/app/pages/Features.jsx +1 -8
- package/src/dashboard/app/pages/Guide.jsx +0 -1
- package/src/dashboard/app/pages/HookInspector.jsx +2 -23
- package/src/dashboard/app/pages/PhasePipeline.jsx +0 -3
- package/src/dashboard/app/pages/Portfolio.jsx +0 -1
- package/src/dashboard/app/pages/RulesMap.jsx +1 -16
- package/src/dashboard/app/pages/SkillsLibrary.jsx +1 -16
- package/src/dashboard/app/pages/Standards.jsx +2 -17
- package/src/dashboard/scanner/activity-scanner.js +10 -69
- package/src/dashboard/scanner/agents-scanner.js +1 -23
- package/src/dashboard/scanner/docs-scanner.js +0 -6
- package/src/dashboard/scanner/feature-status.js +2 -5
- package/src/dashboard/scanner/features-scanner.js +10 -66
- package/src/dashboard/scanner/hooks-scanner.js +5 -28
- package/src/dashboard/scanner/index.js +0 -4
- package/src/dashboard/scanner/portfolio.js +5 -29
- package/src/dashboard/scanner/project-api.js +8 -54
- package/src/dashboard/scanner/rules-scanner.js +1 -2
- package/src/dashboard/scanner/skills-scanner.js +2 -12
- package/src/dashboard/scanner/standards-scanner.js +2 -6
- package/src/dashboard/vite-plugin-morph-data.js +5 -19
- package/src/dashboard/vite.config.js +24 -24
- package/src/lib/ai/ai-checks.js +7 -52
- package/src/lib/ai/ai-project-scan.js +14 -108
- package/src/lib/ai/code-audit.js +7 -63
- package/src/lib/ai/pin-drift.js +8 -51
- package/src/lib/ai/registry-audit.js +11 -90
- package/src/lib/ci/ci-verify.js +4 -19
- package/src/lib/ci/github-ci.js +9 -37
- package/src/lib/ci/workflow-scaffold.js +14 -10
- package/src/lib/detectors/claude-config-detector.js +13 -74
- package/src/lib/detectors/index.js +4 -50
- package/src/lib/e2e/applicability.js +37 -0
- package/src/lib/e2e/compose-env.js +5 -66
- package/src/lib/e2e/stack-teardown.js +6 -40
- package/src/lib/generators/architecture-scanner.js +18 -156
- package/src/lib/generators/handoff-generator.js +9 -53
- package/src/lib/generators/scaffold-generator.js +5 -101
- package/src/lib/generators/settings-generator.js +4 -37
- package/src/lib/git/branch-classifier.js +11 -84
- package/src/lib/git/default-branch.js +14 -169
- package/src/lib/git/evidence-staleness.js +1 -0
- package/src/lib/git/gh.js +2 -17
- package/src/lib/git/pending-prs.js +6 -37
- package/src/lib/git/pr-files.js +3 -28
- package/src/lib/git/pr-index.js +3 -34
- package/src/lib/git/tree-fingerprint.js +3 -15
- package/src/lib/git/tree-state.js +6 -33
- package/src/lib/graph/god-nodes.js +2 -22
- package/src/lib/graph/graph-health.js +3 -24
- package/src/lib/graph/graph-loader.js +5 -56
- package/src/lib/graph/graphify-runtime.js +7 -37
- package/src/lib/graph/graphifyignore.js +8 -74
- package/src/lib/graph/sanitize.js +16 -136
- package/src/lib/heavy/cleanup.js +1 -1
- package/src/lib/installers/effects.js +8 -93
- package/src/lib/installers/mcp-installer.js +9 -169
- package/src/lib/installers/memory-installer.js +4 -27
- package/src/lib/installers/templates-reconcile.js +5 -60
- package/src/lib/mutation/adapters.js +6 -17
- package/src/lib/mutation/dotnet-introspection.js +20 -106
- package/src/lib/mutation/runner.js +23 -75
- package/src/lib/mutation/subset.js +1 -4
- package/src/lib/mutation/trx.js +0 -1
- package/src/lib/plan/artifact-vocabulary.js +5 -25
- package/src/lib/plan/checks.js +34 -33
- package/src/lib/plan/plan-parser.js +7 -8
- package/src/lib/projects/registry.js +7 -44
- package/src/lib/review-reports.js +11 -26
- package/src/lib/risk-detector.js +41 -173
- package/src/lib/specs/adr-index.js +6 -34
- package/src/lib/specs/frontmatter.js +2 -15
- package/src/lib/specs/gfm-table.js +1 -7
- package/src/lib/specs/revocations.js +1 -14
- package/src/lib/specs/screen-checklist.js +47 -124
- package/src/lib/stack/dotnet-build-ignores.js +2 -6
- package/src/lib/stack/stack-profile.js +1 -28
- package/src/lib/stack/stack-scan.js +1 -8
- package/src/lib/stack-filter.js +4 -27
- package/src/lib/standards/registry.js +1 -7
- package/src/lib/standards/sections.js +2 -13
- package/src/lib/standards/verified-against.js +19 -191
- package/src/lib/tasks/eval-node.js +21 -169
- package/src/lib/tasks/linux-runner.js +18 -142
- package/src/lib/tasks/test-nodes.js +43 -240
- package/src/lib/tasks/test-runner.js +43 -341
- package/src/lib/telemetry/metrics.js +7 -36
- package/src/lib/telemetry/telemetry-reader.js +13 -125
- package/src/lib/telemetry/verify-event.js +2 -30
- package/src/lib/tests-audit/audit.js +96 -0
- package/src/lib/validators/code-budget/code-budget-validator.js +4 -44
- package/src/lib/validators/code-budget/index.js +0 -7
- package/src/lib/validators/contracts/contract-compliance-validator.js +2 -24
- package/src/lib/validators/contracts/index.js +0 -6
- package/src/lib/validators/nextjs/index.js +0 -5
- package/src/lib/validators/nextjs/next-component-validator.js +4 -81
- package/src/lib/validators/packages/index.js +0 -6
- package/src/lib/validators/packages/package-validator.js +6 -56
- package/src/lib/validators/reuse/index.js +0 -8
- package/src/lib/validators/reuse/reuse-validator.js +11 -101
- package/src/lib/validators/reuse/symbol-scanner.js +6 -62
- package/src/lib/validators/traceability-check.js +5 -63
- package/src/lib/validators/validation-runner.js +21 -158
- package/src/lib/validators/verify-runner.js +85 -376
- package/src/lib/work-type-detector.js +5 -67
- package/src/lib/worktree/docker-audit.js +17 -136
- package/src/lib/worktree/env-allocator.js +10 -98
- package/src/lib/worktree/feature-truth.js +12 -115
- package/src/lib/worktree/finish-backup.js +32 -242
- package/src/lib/worktree/fleet-scanner.js +13 -95
- package/src/lib/worktree/ignored-triage.js +15 -91
- package/src/lib/worktree/vscode-settings.js +2 -3
- package/src/lib/worktree/worktree-audit.js +21 -213
- package/src/lib/worktree/worktree-include.js +3 -5
- package/src/scripts/global-install.js +3 -9
- package/src/scripts/install-plugin.js +1 -13
- package/src/scripts/setup-infra.js +17 -147
- package/src/utils/agents-installer.js +29 -276
- package/src/utils/banner.js +4 -25
- package/src/utils/claude-md-injector.js +1 -37
- package/src/utils/claude-settings-manager.js +1 -52
- package/src/utils/file-copier.js +11 -95
- package/src/utils/hooks-installer.js +21 -298
- package/src/utils/skills-installer.js +4 -101
- package/src/utils/version-checker.js +4 -83
- package/framework/commands/morph-proposal.md +0 -9
- package/framework/hooks/git/commit-msg/conventional-commits.sh +0 -33
- package/framework/hooks/git/pre-commit/agents.sh +0 -25
- package/framework/hooks/git/pre-commit/orchestrator.sh +0 -64
- package/framework/hooks/git/pre-push/run-tests.sh +0 -44
- package/src/commands/utils/index.js +0 -3
- package/src/lib/detectors/design-system-detector.js +0 -188
- package/src/lib/generators/design-system-generator.js +0 -508
- package/src/lib/specs/old-format.js +0 -68
- package/src/utils/agent-id-mapper.js +0 -11
|
@@ -1,32 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared payload utilities for Claude Code Write/Edit hooks.
|
|
3
|
-
*
|
|
4
|
-
* Extracts file path and content from tool_input payloads,
|
|
5
|
-
* normalizing between Write (content, file_path) and Edit (new_string, old_string, file_path).
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
1
|
import { createHash } from 'crypto';
|
|
9
2
|
import { existsSync } from 'fs';
|
|
10
3
|
import { dirname, join, isAbsolute } from 'path';
|
|
11
4
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
* directory to the nearest ancestor containing one of `markers` (default
|
|
15
|
-
* `package.json` — the tsc/build root). Falls back to `fallbackCwd` when
|
|
16
|
-
* nothing matches or on any error.
|
|
17
|
-
*
|
|
18
|
-
* This exists so a PostToolUse hook typechecks/builds in the CORRECT root when
|
|
19
|
-
* a subagent edits a file inside its own git worktree (`isolation: "worktree"`,
|
|
20
|
-
* morph-apply §3b-w) instead of always assuming the main working tree. In the
|
|
21
|
-
* normal (non-worktree) case it resolves to the same root as `process.cwd()`.
|
|
22
|
-
*
|
|
23
|
-
* Fail-open: never throws — returns `fallbackCwd` on any error.
|
|
24
|
-
*
|
|
25
|
-
* @param {string} filePath - edited file path (absolute or relative)
|
|
26
|
-
* @param {string} fallbackCwd - root to use if resolution fails (usually process.cwd())
|
|
27
|
-
* @param {string[]} [markers] - ancestor marker filenames to look for
|
|
28
|
-
* @returns {string} the resolved root directory
|
|
29
|
-
*/
|
|
5
|
+
// A sub-agent may edit inside its own worktree: the build root comes from the
|
|
6
|
+
// file, not from the session's cwd.
|
|
30
7
|
export function resolveRootForFile(filePath, fallbackCwd, markers = ['package.json']) {
|
|
31
8
|
try {
|
|
32
9
|
if (!filePath) return fallbackCwd;
|
|
@@ -48,33 +25,9 @@ export function resolveRootForFile(filePath, fallbackCwd, markers = ['package.js
|
|
|
48
25
|
}
|
|
49
26
|
}
|
|
50
27
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
*
|
|
55
|
-
* A hook process knows only where the SESSION lives (`process.cwd()` /
|
|
56
|
-
* `CLAUDE_PROJECT_DIR`). That is not the same thing as where the FILE lives: a
|
|
57
|
-
* session rooted in the primary repo can legitimately write to a feature that
|
|
58
|
-
* lives in a secondary git worktree. Resolving state from the process instead
|
|
59
|
-
* of the path is what made `enforce-phase-writes` block a legal `2-plan/` write
|
|
60
|
-
* (the root still had the feature at `proposal`) while `protect-spec-files`
|
|
61
|
-
* simultaneously went DARK there (the root had no approved gate to enforce) —
|
|
62
|
-
* one defect, opposite directions.
|
|
63
|
-
*
|
|
64
|
-
* The anchor is exact, so no marker walk is needed: everything before the
|
|
65
|
-
* file's own `.morph/features/` segment IS the owning root. `lastIndexOf` picks
|
|
66
|
-
* the CLOSEST enclosing `.morph/` — the worktree's, not the root it nests under.
|
|
67
|
-
*
|
|
68
|
-
* Note this cannot be solved by locating the hook file itself: `worktree setup`
|
|
69
|
-
* junctions `.morph/framework/` back to the primary root, so every worktree
|
|
70
|
-
* executes the very same hook file. Only the target path identifies the tree.
|
|
71
|
-
*
|
|
72
|
-
* Fail-open: never throws — returns `fallbackCwd` for any non-feature path.
|
|
73
|
-
*
|
|
74
|
-
* @param {string} filePath - target file path (absolute or relative)
|
|
75
|
-
* @param {string} fallbackCwd - root to use when the path owns no `.morph/`
|
|
76
|
-
* @returns {string} the resolved project root
|
|
77
|
-
*/
|
|
28
|
+
// State is resolved from the target PATH, never the session: a session in the
|
|
29
|
+
// primary root may write a feature that lives in a worktree. `lastIndexOf` picks
|
|
30
|
+
// the closest enclosing `.morph/`.
|
|
78
31
|
export function resolveMorphRoot(filePath, fallbackCwd) {
|
|
79
32
|
try {
|
|
80
33
|
if (!filePath) return fallbackCwd;
|
|
@@ -88,111 +41,41 @@ export function resolveMorphRoot(filePath, fallbackCwd) {
|
|
|
88
41
|
}
|
|
89
42
|
}
|
|
90
43
|
|
|
91
|
-
|
|
92
|
-
* Is this an already-absolute path, on EITHER convention?
|
|
93
|
-
*
|
|
94
|
-
* `path.isAbsolute` is platform-bound: `C:/x` is absolute on win32 and relative
|
|
95
|
-
* on posix. That asymmetry is a real defect here, not a theoretical one — a
|
|
96
|
-
* Windows-shaped path reaching a posix `join()` produces `/cwd/C:/x`, silently
|
|
97
|
-
* resolving state against a directory that cannot exist. Recognizing the drive
|
|
98
|
-
* letter explicitly makes the answer the same everywhere (and is a no-op on
|
|
99
|
-
* win32, where `isAbsolute` already accepts it).
|
|
100
|
-
*/
|
|
44
|
+
// `path.isAbsolute` is platform-bound: on posix `C:/x` would join into `/cwd/C:/x`.
|
|
101
45
|
const isAbsolutePath = (p) => isAbsolute(p) || /^[a-zA-Z]:\//.test(p);
|
|
102
46
|
|
|
103
|
-
/** Normalize to forward slashes and make absolute against `fallbackCwd`. */
|
|
104
47
|
function toAbsolutePosix(filePath, fallbackCwd) {
|
|
105
48
|
const p = String(filePath).replace(/\\/g, '/');
|
|
106
49
|
return isAbsolutePath(p) ? p : join(fallbackCwd, p).replace(/\\/g, '/');
|
|
107
50
|
}
|
|
108
51
|
|
|
109
|
-
/**
|
|
110
|
-
* Absolute path of a hook's target file, resolved against the root that owns
|
|
111
|
-
* it. Pairs with `resolveMorphRoot` so a hook never mixes "the tree I read
|
|
112
|
-
* state from" with "the file I read content from".
|
|
113
|
-
*
|
|
114
|
-
* @param {string} filePath
|
|
115
|
-
* @param {string} fallbackCwd
|
|
116
|
-
* @returns {string}
|
|
117
|
-
*/
|
|
118
52
|
export function resolveTargetPath(filePath, fallbackCwd) {
|
|
119
53
|
return toAbsolutePosix(filePath || '', fallbackCwd);
|
|
120
54
|
}
|
|
121
55
|
|
|
122
|
-
/**
|
|
123
|
-
* Get the file path from a hook payload, normalizing backslashes.
|
|
124
|
-
* @param {Object} payload - Hook payload from stdin
|
|
125
|
-
* @returns {string} Normalized file path or empty string
|
|
126
|
-
*/
|
|
127
56
|
export function getFilePath(payload) {
|
|
128
57
|
const raw = payload?.tool_input?.file_path || payload?.tool_input?.path || '';
|
|
129
58
|
return raw.replace(/\\/g, '/');
|
|
130
59
|
}
|
|
131
60
|
|
|
132
|
-
/**
|
|
133
|
-
* Get the content being written or edited.
|
|
134
|
-
* For Write: returns `content` (full file content).
|
|
135
|
-
* For Edit: returns `new_string` (replacement text).
|
|
136
|
-
* @param {Object} payload - Hook payload from stdin
|
|
137
|
-
* @returns {string} Content string or empty string
|
|
138
|
-
*/
|
|
139
61
|
export function getContentToValidate(payload) {
|
|
140
62
|
const input = payload?.tool_input;
|
|
141
63
|
if (!input) return '';
|
|
142
|
-
// Write tool uses 'content', Edit tool uses 'new_string'
|
|
143
64
|
return input.content || input.new_string || '';
|
|
144
65
|
}
|
|
145
66
|
|
|
146
|
-
/**
|
|
147
|
-
* Check if this is a Write operation (full file content available).
|
|
148
|
-
* @param {Object} payload - Hook payload from stdin
|
|
149
|
-
* @returns {boolean} True if Write (content exists), false if Edit or unknown
|
|
150
|
-
*/
|
|
151
67
|
export function isWriteOperation(payload) {
|
|
152
68
|
return typeof payload?.tool_input?.content === 'string';
|
|
153
69
|
}
|
|
154
70
|
|
|
155
|
-
/**
|
|
156
|
-
* Get the tool name from a hook payload.
|
|
157
|
-
* @param {Object} payload - Hook payload from stdin
|
|
158
|
-
* @returns {string} Tool name or 'unknown'
|
|
159
|
-
*/
|
|
160
71
|
export function getToolName(payload) {
|
|
161
72
|
return payload?.tool_name || 'unknown';
|
|
162
73
|
}
|
|
163
74
|
|
|
164
|
-
/** Assinatura de Bash: comando normalizado, truncado. Ver getCommandSignature. */
|
|
165
75
|
const BASH_SIGNATURE_MAX = 120;
|
|
166
76
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
* - Edit/Write: "Edit:normalized/file/path#<sha1[0:8] do conteúdo>" ("Edit:path" sem conteúdo)
|
|
170
|
-
* - Bash: "Bash:<normalized command, truncated>"
|
|
171
|
-
* - Every other tool WITH input: "Tool:<truncated JSON of tool_input>"
|
|
172
|
-
* - No tool_input at all: just the tool name
|
|
173
|
-
*
|
|
174
|
-
* The general (non-Edit/Write/Bash) case matters: a burst of `Read`/`Skill`/
|
|
175
|
-
* `Grep`/`Agent` calls against DIFFERENT targets (phase-context loading,
|
|
176
|
-
* post-task evaluation reads, parallel subagent dispatch) is normal work, not
|
|
177
|
-
* a loop. Collapsing every one of them to the bare tool name is what made
|
|
178
|
-
* loop-detect fire on exactly that legitimate traffic — a real, reported
|
|
179
|
-
* false positive. Folding in the actual input (rather than hand-listing a
|
|
180
|
-
* target field per tool) fixes it uniformly for every tool at once.
|
|
181
|
-
*
|
|
182
|
-
* **Bash ficou de fora daquele fix (2026-07-22) e continuou colapsando em DUAS
|
|
183
|
-
* PALAVRAS**, o que é quase sempre o nome da ferramenta e nada mais:
|
|
184
|
-
* `npm test -- a`, `npm test -- b` e `npm run build` viravam `Bash:npm test` /
|
|
185
|
-
* `Bash:npm run`. Um ciclo legítimo de mutação — rodar o mesmo runner sobre
|
|
186
|
-
* alvos diferentes, que é exatamente como se testa se um teste morde — disparava
|
|
187
|
-
* o aviso de loop três vezes seguidas. Aviso que toca à toa é aviso que se
|
|
188
|
-
* aprende a ignorar, e aí ele não serve nem quando está certo.
|
|
189
|
-
*
|
|
190
|
-
* O discriminador agora é o comando inteiro (normalizado e truncado), mesma
|
|
191
|
-
* forma dos demais tools. Repetir o comando IDÊNTICO continua sendo loop.
|
|
192
|
-
*
|
|
193
|
-
* @param {Object} payload - Hook payload from stdin
|
|
194
|
-
* @returns {string} Signature string
|
|
195
|
-
*/
|
|
77
|
+
// The input is part of the signature: the same tool on DIFFERENT targets is
|
|
78
|
+
// work, not a loop. Only an identical call repeats the signature.
|
|
196
79
|
export function getCommandSignature(payload) {
|
|
197
80
|
const tool = getToolName(payload);
|
|
198
81
|
if (tool === 'unknown') return 'unknown';
|
|
@@ -203,11 +86,8 @@ export function getCommandSignature(payload) {
|
|
|
203
86
|
if (tool === 'Edit' || tool === 'Write') {
|
|
204
87
|
const path = (input.file_path || input.path || '').replace(/\\/g, '/');
|
|
205
88
|
if (!path) return tool;
|
|
206
|
-
// O caminho sozinho confundia
|
|
207
|
-
//
|
|
208
|
-
// fonte) dava só duas assinaturas e disparava o aviso. O conteúdo da
|
|
209
|
-
// edição entra na assinatura; repetir a MESMA edição continua casando, e o
|
|
210
|
-
// ping-pong A→B / B→A continua dando duas assinaturas — loop de verdade.
|
|
89
|
+
// O caminho sozinho confundia editar regiões diferentes com loop; o
|
|
90
|
+
// ping-pong A→B / B→A continua dando duas assinaturas.
|
|
211
91
|
const body = tool === 'Write'
|
|
212
92
|
? input.content
|
|
213
93
|
: (input.old_string !== undefined || input.new_string !== undefined)
|
|
@@ -218,8 +98,6 @@ export function getCommandSignature(payload) {
|
|
|
218
98
|
}
|
|
219
99
|
|
|
220
100
|
if (tool === 'Bash') {
|
|
221
|
-
// Espaços colapsados para que a mesma invocação escrita com espaçamento
|
|
222
|
-
// diferente continue casando — o alvo é o comando, não a digitação.
|
|
223
101
|
const cmd = (input.command || '').replace(/\s+/g, ' ').trim();
|
|
224
102
|
return cmd ? `Bash:${cmd.slice(0, BASH_SIGNATURE_MAX)}` : 'Bash';
|
|
225
103
|
}
|
|
@@ -1,25 +1,16 @@
|
|
|
1
1
|
// GENERATED by scripts/generate-refs.js — DO NOT EDIT manually
|
|
2
2
|
// Source of truth: src/core/paths/output-schema.js
|
|
3
3
|
// Regenerate with: node scripts/generate-refs.js
|
|
4
|
-
/**
|
|
5
|
-
* Shared phase utilities for Claude Code hooks.
|
|
6
|
-
*
|
|
7
|
-
* Maps phases to directories and output types.
|
|
8
|
-
*/
|
|
9
4
|
|
|
10
|
-
|
|
11
|
-
export const PHASE_ORDER = ["plan","uiux","implement","review","sync"];
|
|
5
|
+
export const PHASE_ORDER = ["plan","uiux","implement","review"];
|
|
12
6
|
|
|
13
|
-
/** Map phase → allowed output subdirectory */
|
|
14
7
|
export const PHASE_DIRS = {
|
|
15
8
|
plan: '2-plan',
|
|
16
9
|
uiux: '1-design',
|
|
17
10
|
implement: '3-implement',
|
|
18
11
|
review: '4-review',
|
|
19
|
-
sync: '3-implement',
|
|
20
12
|
};
|
|
21
13
|
|
|
22
|
-
/** Map output type (camelCase) → phase that produces it */
|
|
23
14
|
export const OUTPUT_PHASE_MAP = {
|
|
24
15
|
plan: 'plan',
|
|
25
16
|
checks: 'plan',
|
|
@@ -33,7 +24,6 @@ export const OUTPUT_PHASE_MAP = {
|
|
|
33
24
|
securityReport: 'review',
|
|
34
25
|
};
|
|
35
26
|
|
|
36
|
-
/** Map filename → output type (camelCase) */
|
|
37
27
|
export const FILENAME_TO_OUTPUT = {
|
|
38
28
|
'plan.md': 'plan',
|
|
39
29
|
'checks.json': 'checks',
|
|
@@ -46,7 +36,6 @@ export const FILENAME_TO_OUTPUT = {
|
|
|
46
36
|
'security.md': 'securityReport',
|
|
47
37
|
};
|
|
48
38
|
|
|
49
|
-
/** Protected spec files and the approval gate that locks them */
|
|
50
39
|
export const PROTECTED_SPEC_FILES = {
|
|
51
40
|
'plan.md': 'plan',
|
|
52
41
|
'checks.json': 'plan',
|
|
@@ -57,63 +46,33 @@ export const PROTECTED_SPEC_FILES = {
|
|
|
57
46
|
};
|
|
58
47
|
|
|
59
48
|
|
|
60
|
-
/**
|
|
61
|
-
* Extract feature name from a .morph/features/{feature}/... path
|
|
62
|
-
* @param {string} filePath - File path to analyze
|
|
63
|
-
* @returns {string|null} Feature name or null
|
|
64
|
-
*/
|
|
65
49
|
export function extractFeatureName(filePath) {
|
|
66
50
|
const normalized = filePath.replace(/\\/g, '/');
|
|
67
51
|
const match = normalized.match(/\.morph\/features\/([^/]+)\//);
|
|
68
52
|
return match ? match[1] : null;
|
|
69
53
|
}
|
|
70
54
|
|
|
71
|
-
/**
|
|
72
|
-
* Extract phase subdirectory from a .morph/features/{feature}/{phaseDir}/... path
|
|
73
|
-
* @param {string} filePath - File path to analyze
|
|
74
|
-
* @returns {string|null} Phase directory (e.g., '2-plan', '1-design') or null
|
|
75
|
-
*/
|
|
76
55
|
export function extractPhaseDir(filePath) {
|
|
77
56
|
const normalized = filePath.replace(/\\/g, '/');
|
|
78
57
|
const match = normalized.match(/\.morph\/features\/[^/]+\/(\d+-[^/]+)\//);
|
|
79
58
|
return match ? match[1] : null;
|
|
80
59
|
}
|
|
81
60
|
|
|
82
|
-
/**
|
|
83
|
-
* Check if a file path is inside .morph/features/
|
|
84
|
-
* @param {string} filePath
|
|
85
|
-
* @returns {boolean}
|
|
86
|
-
*/
|
|
87
61
|
export function isFeaturePath(filePath) {
|
|
88
62
|
const normalized = filePath.replace(/\\/g, '/');
|
|
89
63
|
return normalized.includes('.morph/features/');
|
|
90
64
|
}
|
|
91
65
|
|
|
92
|
-
/**
|
|
93
|
-
* Check if a file path is inside .morph/framework/ (readonly)
|
|
94
|
-
* @param {string} filePath
|
|
95
|
-
* @returns {boolean}
|
|
96
|
-
*/
|
|
97
66
|
export function isFrameworkPath(filePath) {
|
|
98
67
|
const normalized = filePath.replace(/\\/g, '/');
|
|
99
68
|
return normalized.includes('.morph/framework/');
|
|
100
69
|
}
|
|
101
70
|
|
|
102
|
-
/**
|
|
103
|
-
* Check if a file path is state.json
|
|
104
|
-
* @param {string} filePath
|
|
105
|
-
* @returns {boolean}
|
|
106
|
-
*/
|
|
107
71
|
export function isStatePath(filePath) {
|
|
108
72
|
const normalized = filePath.replace(/\\/g, '/');
|
|
109
73
|
return normalized.endsWith('.morph/state.json');
|
|
110
74
|
}
|
|
111
75
|
|
|
112
|
-
/**
|
|
113
|
-
* Get the basename of a file path
|
|
114
|
-
* @param {string} filePath
|
|
115
|
-
* @returns {string}
|
|
116
|
-
*/
|
|
117
76
|
export function getBasename(filePath) {
|
|
118
77
|
const normalized = filePath.replace(/\\/g, '/');
|
|
119
78
|
return normalized.split('/').pop();
|
|
@@ -1,14 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* under `.git/`, so whatever lands there outlives the session.
|
|
5
|
-
*
|
|
6
|
-
* Pattern-based by design: it catches a secret written NEXT TO its name
|
|
7
|
-
* (`KEY=…`, `Authorization: …`, `user:pass@`) or in a well-known token shape.
|
|
8
|
-
* A bare value with no name beside it is invisible to any pattern — which is
|
|
9
|
-
* why tool OUTPUT (stdout/stderr, a failure's `error` body) is never persisted
|
|
10
|
-
* at all instead of being redacted.
|
|
11
|
-
*/
|
|
1
|
+
// Catches a secret written next to its name or in a known token shape; a bare
|
|
2
|
+
// value is invisible to any pattern, which is why tool OUTPUT is never
|
|
3
|
+
// persisted at all instead of being redacted.
|
|
12
4
|
|
|
13
5
|
export const REDACTED = '[REDACTED]';
|
|
14
6
|
|
|
@@ -43,10 +35,7 @@ const RULES = [
|
|
|
43
35
|
[/\beyJ[A-Za-z0-9_-]{5,}\.eyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]+/g, REDACTED],
|
|
44
36
|
];
|
|
45
37
|
|
|
46
|
-
/**
|
|
47
|
-
* @param {*} text
|
|
48
|
-
* @returns {*} the text with secrets replaced by `[REDACTED]`; non-strings unchanged
|
|
49
|
-
*/
|
|
38
|
+
/** Non-strings pass through unchanged. */
|
|
50
39
|
export function redactSecrets(text) {
|
|
51
40
|
if (typeof text !== 'string' || text === '') return text;
|
|
52
41
|
let out = text;
|
|
@@ -1,63 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
*
|
|
5
|
-
* This lives in `framework/hooks/shared/` rather than `src/` because BOTH sides
|
|
6
|
-
* need it and the dependency direction in this repo only runs one way: `src/`
|
|
7
|
-
* imports from `framework/hooks/shared/` (see `finish.js`'s `claim-registry`
|
|
8
|
-
* and `e2e-check` imports), never the reverse — a hook shipped into a target
|
|
9
|
-
* project has the package, not this repo's `src/` layout.
|
|
10
|
-
*
|
|
11
|
-
* Two callers, one question:
|
|
12
|
-
* - `teardownWorktree` (src/commands/project/worktree.js) — may this worktree
|
|
13
|
-
* be handed to `git worktree remove`?
|
|
14
|
-
* - the `block-destructive-worktree-removal` PreToolUse hook — is the command
|
|
15
|
-
* about to run going to traverse a link out of the worktree?
|
|
16
|
-
*
|
|
17
|
-
* WHY IT MUST BE EXHAUSTIVE — both `git worktree remove` and its `--force`
|
|
18
|
-
* fallback FOLLOW reparse points out of the worktree and delete the PRIMARY
|
|
19
|
-
* ROOT's real files. The plain removal is not the safe one: it refuses only
|
|
20
|
-
* when it sees modified/untracked *tracked-scope* content, so a GITIGNORED
|
|
21
|
-
* junction (`node_modules`, `.claude`) leaves `git status` empty, the removal
|
|
22
|
-
* proceeds, exits 0 — and the target is gone.
|
|
23
|
-
*
|
|
24
|
-
* So the check cannot live in the `catch` of the plain removal (unreachable on
|
|
25
|
-
* exactly the destructive path), and it cannot be a fixed list of the links
|
|
26
|
-
* `setup` creates: a nested link inside a real `node_modules` (npm link, npm
|
|
27
|
-
* workspaces) is invisible to any top-level inventory. We scan the whole tree
|
|
28
|
-
* and NEVER recurse INTO a reparse point — descending one would walk the target
|
|
29
|
-
* we are trying to protect (and could not terminate on a cycle).
|
|
30
|
-
*
|
|
31
|
-
* FAIL-CLOSED, deliberately: an empty result is NOT the same as a safe one. A
|
|
32
|
-
* directory we could not read or an entry we could not lstat might BE (or
|
|
33
|
-
* contain) a reparse point. Those answer `unprovable`, and every caller must
|
|
34
|
-
* treat that exactly like a found link — decline. "I saw nothing" and "I proved
|
|
35
|
-
* there is nothing" are different claims, and only the second may authorize a
|
|
36
|
-
* removal. This is the one module in the shared/ directory that must NOT be
|
|
37
|
-
* fail-open; the whole point is that uncertainty blocks.
|
|
38
|
-
*
|
|
39
|
-
* `lstat` reports a POSIX symlink and a Windows junction alike as a symbolic
|
|
40
|
-
* link, so the scan behaves identically on both platforms — as does the
|
|
41
|
-
* destruction it prevents.
|
|
42
|
-
*/
|
|
1
|
+
// `git worktree remove` (plain or --force) follows reparse points and deletes the
|
|
2
|
+
// primary root's files, nested links included. FAIL-CLOSED: an entry that cannot
|
|
3
|
+
// be read is `unprovable` and blocks exactly like a found link.
|
|
43
4
|
|
|
44
5
|
import { readdirSync, lstatSync } from 'fs';
|
|
45
6
|
import { join, relative } from 'path';
|
|
46
7
|
|
|
47
|
-
/**
|
|
48
|
-
* Every reparse point anywhere under `dir`, as relative paths.
|
|
49
|
-
*
|
|
50
|
-
* @param {string} dir
|
|
51
|
-
* @param {string} [base] - root of the scan, for relative reporting
|
|
52
|
-
* @param {{points: string[], unprovable: string[]}} [acc]
|
|
53
|
-
* @returns {{points: string[], unprovable: string[]}} both empty = provably clean
|
|
54
|
-
*/
|
|
8
|
+
/** @returns {{points: string[], unprovable: string[]}} both empty = provably clean */
|
|
55
9
|
export function findReparsePoints(dir, base = dir, acc = { points: [], unprovable: [] }) {
|
|
56
10
|
let entries;
|
|
57
11
|
try {
|
|
58
12
|
entries = readdirSync(dir, { withFileTypes: true });
|
|
59
13
|
} catch {
|
|
60
|
-
// Unreadable (ACL, lock, EMFILE): we cannot prove what is in here.
|
|
61
14
|
acc.unprovable.push((relative(base, dir) || '.').replace(/\\/g, '/'));
|
|
62
15
|
return acc;
|
|
63
16
|
}
|
|
@@ -71,28 +24,15 @@ export function findReparsePoints(dir, base = dir, acc = { points: [], unprovabl
|
|
|
71
24
|
continue;
|
|
72
25
|
}
|
|
73
26
|
if (st.isSymbolicLink()) {
|
|
74
|
-
// Forward slashes: every other path this command reports is normalized,
|
|
75
|
-
// and these end up in JSON the caller may compare or display.
|
|
76
27
|
acc.points.push(relative(base, p).replace(/\\/g, '/'));
|
|
77
|
-
continue; //
|
|
28
|
+
continue; // descending would walk the target, not the worktree
|
|
78
29
|
}
|
|
79
30
|
if (st.isDirectory() && entry.name !== '.git') findReparsePoints(p, base, acc);
|
|
80
31
|
}
|
|
81
32
|
return acc;
|
|
82
33
|
}
|
|
83
34
|
|
|
84
|
-
|
|
85
|
-
* The single question both callers actually ask: may this tree be handed to a
|
|
86
|
-
* command that follows links?
|
|
87
|
-
*
|
|
88
|
-
* Collapses `points` and `unprovable` into one list precisely because callers
|
|
89
|
-
* must not weigh them differently — `teardownWorktree` already concatenated
|
|
90
|
-
* them, and a hook that treated `unprovable` as "probably fine" would reopen
|
|
91
|
-
* the hole this module exists to close.
|
|
92
|
-
*
|
|
93
|
-
* @param {string} dir
|
|
94
|
-
* @returns {{ safe: boolean, blockers: string[] }}
|
|
95
|
-
*/
|
|
35
|
+
// One list on purpose: callers must not weigh `unprovable` lighter than a link.
|
|
96
36
|
export function scanForBlockers(dir) {
|
|
97
37
|
const { points, unprovable } = findReparsePoints(dir);
|
|
98
38
|
const blockers = [...points, ...unprovable];
|
|
@@ -14,21 +14,13 @@ export function sliceReportPath(featureDir, sliceId) {
|
|
|
14
14
|
return join(featureDir, '3-implement', 'reports', `${sliceId}.md`);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
/** Status of a slice from its report text: no report = pendente, PARCIAL first line = parcial, else feito. */
|
|
18
17
|
export function sliceStatusFromReport(text) {
|
|
19
18
|
if (text == null) return 'pendente';
|
|
20
19
|
const first = String(text).split(/\r?\n/).find((l) => l.trim() !== '') ?? '';
|
|
21
20
|
return PARTIAL_MARKER.test(first) ? 'parcial' : 'feito';
|
|
22
21
|
}
|
|
23
22
|
|
|
24
|
-
/**
|
|
25
|
-
* Progress of a feature's slices.
|
|
26
|
-
*
|
|
27
|
-
* @param {string|null} checksText - the raw extracted checks file of the plan
|
|
28
|
-
* @param {(sliceId: string) => string|null} readReport - report text, null when absent
|
|
29
|
-
* @returns {{ total: number, done: number, partial: number, pending: number,
|
|
30
|
-
* slices: { id: string, title: string, depends: string[], covers: string[], status: string }[] } | null} null when that file is unreadable
|
|
31
|
-
*/
|
|
23
|
+
/** @returns {{ total, done, partial, pending, slices } | null} null when the checks text is unreadable */
|
|
32
24
|
export function sliceProgressFrom(checksText, readReport) {
|
|
33
25
|
let slices;
|
|
34
26
|
try {
|
|
@@ -58,23 +50,18 @@ function readOrNull(path) {
|
|
|
58
50
|
}
|
|
59
51
|
}
|
|
60
52
|
|
|
61
|
-
/** Slice progress of the feature directory on disk. */
|
|
62
53
|
export function sliceProgress(featureDir) {
|
|
63
54
|
return sliceProgressFrom(readOrNull(checksPath(featureDir)), (id) => readOrNull(sliceReportPath(featureDir, id)));
|
|
64
55
|
}
|
|
65
56
|
|
|
66
|
-
/** Unfinished slices whose dependencies are all done: what the next dispatch can take. */
|
|
67
57
|
export function readySlices(progress) {
|
|
68
58
|
if (!progress) return [];
|
|
69
59
|
const done = new Set(progress.slices.filter((s) => s.status === 'feito').map((s) => s.id));
|
|
70
60
|
return progress.slices.filter((s) => s.status !== 'feito' && s.depends.every((d) => done.has(d)));
|
|
71
61
|
}
|
|
72
62
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
* Loose on purpose: a false "complete" costs a read, a false "incomplete"
|
|
76
|
-
* re-dispatches finished work.
|
|
77
|
-
*/
|
|
63
|
+
// Loose on purpose: a false "complete" costs a read, a false "incomplete"
|
|
64
|
+
// re-dispatches finished work.
|
|
78
65
|
export const REPORT_MARKERS = [
|
|
79
66
|
{ id: 'files', re: /^\s*(?:[-*]\s*)?(?:\*\*|#+\s*)?arquivos\s*:?\**\s*:?/mi },
|
|
80
67
|
];
|
|
@@ -83,13 +70,7 @@ const MIN_PROSE_CHARS = 80;
|
|
|
83
70
|
|
|
84
71
|
const scenarioRe = (id) => new RegExp(`(?<![\\w-])${id}(?![\\w-])`);
|
|
85
72
|
|
|
86
|
-
/**
|
|
87
|
-
* Inspect one report file: does it exist, which mandatory items it lacks,
|
|
88
|
-
* and whether it is partial.
|
|
89
|
-
*
|
|
90
|
-
* @param {string} path
|
|
91
|
-
* @param {{ covers?: string[] }} [slice] - every scenario the slice covers must be named
|
|
92
|
-
*/
|
|
73
|
+
/** Every scenario in `covers` must be named in the report. */
|
|
93
74
|
export function inspectReport(path, { covers = [] } = {}) {
|
|
94
75
|
const missingAll = ['what', 'files', ...covers];
|
|
95
76
|
if (!existsSync(path)) return { exists: false, complete: false, missing: missingAll };
|
|
@@ -1,34 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* `reuse` and `code-budget` validators and the `comment-guard` hook, so the
|
|
4
|
-
* three never disagree about the same file.
|
|
5
|
-
*
|
|
6
|
-
* @module source-files
|
|
7
|
-
*/
|
|
1
|
+
// One reading of "hand-written project source" for the `reuse` and
|
|
2
|
+
// `code-budget` validators and the `comment-guard` hook.
|
|
8
3
|
|
|
9
4
|
const SCANNABLE = /\.(cs|ts|tsx|js|jsx|mjs|cjs)$/i;
|
|
10
5
|
|
|
11
|
-
// `.morph/`
|
|
12
|
-
// graph as a second `RoulettePriority`.
|
|
6
|
+
// `.morph/` holds archived contracts that would duplicate real types.
|
|
13
7
|
const NOT_SOURCE = /(^|\/)(?:\.morph|node_modules|dist|build|out|\.next|bin|obj|coverage)\//;
|
|
14
8
|
|
|
15
9
|
const DECLARATION_FILE = /\.d\.ts$/i;
|
|
16
10
|
|
|
17
11
|
const GENERATED = /(?:\.Designer|\.g|\.g\.i)\.cs$|ModelSnapshot\.cs$/i;
|
|
18
12
|
|
|
19
|
-
/**
|
|
20
|
-
* @param {string} sourceFile
|
|
21
|
-
* @returns {boolean} project code in a scanned extension, tests included
|
|
22
|
-
*/
|
|
13
|
+
/** Tests included. */
|
|
23
14
|
export function isProjectSource(sourceFile) {
|
|
24
15
|
const f = String(sourceFile || '').replace(/\\/g, '/');
|
|
25
16
|
return SCANNABLE.test(f) && !DECLARATION_FILE.test(f) && !NOT_SOURCE.test(f);
|
|
26
17
|
}
|
|
27
18
|
|
|
28
|
-
/**
|
|
29
|
-
* @param {string} sourceFile
|
|
30
|
-
* @returns {boolean} generated C# nobody writes by hand
|
|
31
|
-
*/
|
|
32
19
|
export function isGenerated(sourceFile) {
|
|
33
20
|
return GENERATED.test(String(sourceFile || ''));
|
|
34
21
|
}
|