@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
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { tableCells, isTableLine, SEPARATOR_ROW_RE } from '../specs/gfm-table.js';
|
|
2
|
+
import { ARTIFACT_MARKER_RE } from './artifact-vocabulary.js';
|
|
3
|
+
import { markdownLines } from '../../../framework/hooks/shared/errata.js';
|
|
4
|
+
|
|
5
|
+
export const PROOF_KINDS = ['teste', 'e2e', 'eval', 'manual'];
|
|
6
|
+
export const REUSE_VERDICTS = ['REUSAR', 'ESTENDER', 'CRIAR'];
|
|
7
|
+
export const ADR_SCOPES = ['feature', 'projeto'];
|
|
8
|
+
|
|
9
|
+
export const SECTIONS = {
|
|
10
|
+
problem: 'Problema',
|
|
11
|
+
outOfScope: 'Fora de escopo',
|
|
12
|
+
assumptions: 'Suposições não confirmadas',
|
|
13
|
+
scenarios: 'Cenários',
|
|
14
|
+
decisions: 'Decisões',
|
|
15
|
+
architecture: 'Arquitetura',
|
|
16
|
+
reuse: 'Reuso',
|
|
17
|
+
slices: 'Fatias',
|
|
18
|
+
contract: 'Contrato de implementação',
|
|
19
|
+
errata: 'Errata',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const CONFLICTS_HEADING = 'Conflitos com o acervo';
|
|
23
|
+
|
|
24
|
+
const SLICE_KEYS = {
|
|
25
|
+
persona: 'persona',
|
|
26
|
+
'superfície': 'surface',
|
|
27
|
+
superficie: 'surface',
|
|
28
|
+
standards: 'standards',
|
|
29
|
+
arquivos: 'files',
|
|
30
|
+
depende: 'depends',
|
|
31
|
+
cobre: 'covers',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const DASH = String.raw`\s+[—–-]\s+`;
|
|
35
|
+
const SCENARIO_RE = new RegExp(String.raw`^[-*]\s+\*\*(C\d+)\*\*${DASH}(.+)$`);
|
|
36
|
+
const ADR_RE = new RegExp(String.raw`^###\s+(ADR-\d+)${DASH}(.+?)\s*$`);
|
|
37
|
+
const SLICE_RE = new RegExp(String.raw`^###\s+(F\d+)${DASH}(.+?)\s*$`);
|
|
38
|
+
const PROOF_RE = /^\s*prova\s*:\s*(.*?)\s*$/i;
|
|
39
|
+
const WHY_RE = new RegExp(String.raw`^\s*>\s*` + ARTIFACT_MARKER_RE.source + String.raw`\s*(.*?)\s*$`, 'i');
|
|
40
|
+
const SCOPE_RE = /^\s*\*\*Alcance:\*\*\s*(.*?)\s*$/i;
|
|
41
|
+
const SLICE_LINE_RE = /^\s*[-*]\s+([\p{L}]+)\s*:\s*(.*?)\s*$/u;
|
|
42
|
+
|
|
43
|
+
const fold = (s) => s.normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().trim();
|
|
44
|
+
|
|
45
|
+
const listOf = (value) => value
|
|
46
|
+
.split(',')
|
|
47
|
+
.map((v) => v.replace(/`/g, '').trim())
|
|
48
|
+
.filter((v) => v && !/^[—–-]$/.test(v) && fold(v) !== 'nenhum' && fold(v) !== 'nenhuma');
|
|
49
|
+
|
|
50
|
+
function splitSections(lines) {
|
|
51
|
+
const sections = [];
|
|
52
|
+
let current = null;
|
|
53
|
+
for (const line of lines) {
|
|
54
|
+
const h2 = !line.fenced && /^##\s+(.+?)\s*$/.exec(line.raw);
|
|
55
|
+
if (h2 && !line.raw.startsWith('###')) {
|
|
56
|
+
current = { name: h2[1], line: line.n, lines: [] };
|
|
57
|
+
sections.push(current);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (current) current.lines.push(line);
|
|
61
|
+
}
|
|
62
|
+
return sections;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function parseScenarios(lines) {
|
|
66
|
+
const scenarios = [];
|
|
67
|
+
let current = null;
|
|
68
|
+
for (const line of lines) {
|
|
69
|
+
if (line.fenced) { current = null; continue; }
|
|
70
|
+
const m = SCENARIO_RE.exec(line.raw);
|
|
71
|
+
if (m) {
|
|
72
|
+
current = { id: m[1], text: m[2].trim(), proof: null, artifactWhy: null, line: line.n };
|
|
73
|
+
scenarios.push(current);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (!current) continue;
|
|
77
|
+
if (/^\s*$/.test(line.raw) || /^#/.test(line.raw) || /^[-*]\s/.test(line.raw)) { current = null; continue; }
|
|
78
|
+
const why = WHY_RE.exec(line.raw);
|
|
79
|
+
if (why) { current.artifactWhy = why[1] || ''; continue; }
|
|
80
|
+
const proof = PROOF_RE.exec(line.raw);
|
|
81
|
+
if (proof && current.proof === null) { current.proof = proof[1]; continue; }
|
|
82
|
+
if (/^\s*>/.test(line.raw)) continue;
|
|
83
|
+
current.text = `${current.text} ${line.raw.trim()}`;
|
|
84
|
+
}
|
|
85
|
+
return scenarios;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function parseDecisions(lines) {
|
|
89
|
+
const adrs = [];
|
|
90
|
+
let conflicts = null;
|
|
91
|
+
let current = null;
|
|
92
|
+
for (const line of lines) {
|
|
93
|
+
if (line.fenced) {
|
|
94
|
+
if (current?.kind === 'conflicts') conflicts.body.push(line.raw);
|
|
95
|
+
else if (current?.kind === 'adr') current.entry.body.push(line.raw);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const adr = ADR_RE.exec(line.raw);
|
|
99
|
+
if (adr) {
|
|
100
|
+
current = { kind: 'adr', entry: { id: adr[1], title: adr[2], scope: null, body: [], line: line.n } };
|
|
101
|
+
adrs.push(current.entry);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const h3 = /^###\s+(.+?)\s*$/.exec(line.raw);
|
|
105
|
+
if (h3) {
|
|
106
|
+
if (fold(h3[1]) === fold(CONFLICTS_HEADING)) {
|
|
107
|
+
conflicts = { line: line.n, body: [] };
|
|
108
|
+
current = { kind: 'conflicts' };
|
|
109
|
+
} else {
|
|
110
|
+
current = null;
|
|
111
|
+
}
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (current?.kind === 'adr') {
|
|
115
|
+
current.entry.body.push(line.raw);
|
|
116
|
+
const scope = current.entry.scope === null && SCOPE_RE.exec(line.raw);
|
|
117
|
+
if (scope) current.entry.scope = scope[1].replace(/`/g, '').trim().toLowerCase();
|
|
118
|
+
} else if (current?.kind === 'conflicts') {
|
|
119
|
+
conflicts.body.push(line.raw);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
for (const a of adrs) a.body = a.body.join('\n').trim();
|
|
123
|
+
return {
|
|
124
|
+
adrs,
|
|
125
|
+
conflicts: conflicts && { line: conflicts.line, text: conflicts.body.join('\n').trim() },
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function parseReuse(lines) {
|
|
130
|
+
const rows = [];
|
|
131
|
+
let header = null;
|
|
132
|
+
for (const line of lines) {
|
|
133
|
+
if (line.fenced || !isTableLine(line.raw) || SEPARATOR_ROW_RE.test(line.raw)) continue;
|
|
134
|
+
const cells = tableCells(line.raw);
|
|
135
|
+
if (!header) { header = cells.map(fold); continue; }
|
|
136
|
+
const at = (name) => cells[header.indexOf(name)] ?? '';
|
|
137
|
+
rows.push({
|
|
138
|
+
concept: at('conceito'),
|
|
139
|
+
verdict: at('veredito').replace(/`|\*/g, '').trim().toUpperCase(),
|
|
140
|
+
target: at('alvo'),
|
|
141
|
+
why: at('why'),
|
|
142
|
+
line: line.n,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return { header, rows };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function parseSlices(lines) {
|
|
149
|
+
const slices = [];
|
|
150
|
+
let current = null;
|
|
151
|
+
for (const line of lines) {
|
|
152
|
+
if (line.fenced) continue;
|
|
153
|
+
const m = SLICE_RE.exec(line.raw);
|
|
154
|
+
if (m) {
|
|
155
|
+
current = {
|
|
156
|
+
id: m[1], title: m[2], persona: null, surface: null,
|
|
157
|
+
standards: [], files: [], depends: [], covers: [], unknownKeys: [], line: line.n,
|
|
158
|
+
};
|
|
159
|
+
slices.push(current);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (/^###\s/.test(line.raw)) { current = null; continue; }
|
|
163
|
+
if (!current) continue;
|
|
164
|
+
const kv = SLICE_LINE_RE.exec(line.raw);
|
|
165
|
+
if (!kv) continue;
|
|
166
|
+
const key = SLICE_KEYS[fold(kv[1])] ?? SLICE_KEYS[kv[1].toLowerCase()];
|
|
167
|
+
if (!key) { current.unknownKeys.push(kv[1]); continue; }
|
|
168
|
+
if (key === 'persona' || key === 'surface') {
|
|
169
|
+
const value = kv[2].replace(/`/g, '').trim();
|
|
170
|
+
current[key] = value && !/^[—–-]$/.test(value) ? value : null;
|
|
171
|
+
} else {
|
|
172
|
+
current[key] = listOf(kv[2]);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return slices;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Pure: no validation, no I/O; `checks.js` judges what this returns. */
|
|
179
|
+
export function parsePlan(text) {
|
|
180
|
+
const lines = markdownLines(text);
|
|
181
|
+
const sections = splitSections(lines);
|
|
182
|
+
const byName = new Map();
|
|
183
|
+
for (const s of sections) {
|
|
184
|
+
const key = Object.keys(SECTIONS).find((k) => fold(SECTIONS[k]) === fold(s.name));
|
|
185
|
+
if (key && !byName.has(key)) byName.set(key, s);
|
|
186
|
+
}
|
|
187
|
+
const body = (key) => byName.get(key)?.lines ?? [];
|
|
188
|
+
const decisions = parseDecisions(body('decisions'));
|
|
189
|
+
const reuse = parseReuse(body('reuse'));
|
|
190
|
+
const errata = byName.get('errata');
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
sections: sections.map((s) => ({ name: s.name, line: s.line, key: [...byName].find(([, v]) => v === s)?.[0] ?? null })),
|
|
194
|
+
present: Object.fromEntries(Object.keys(SECTIONS).map((k) => [k, byName.has(k)])),
|
|
195
|
+
scenarios: parseScenarios(body('scenarios')),
|
|
196
|
+
adrs: decisions.adrs,
|
|
197
|
+
conflicts: decisions.conflicts,
|
|
198
|
+
reuse: reuse.rows,
|
|
199
|
+
reuseHeader: reuse.header,
|
|
200
|
+
slices: parseSlices(body('slices')),
|
|
201
|
+
errata: errata ? errata.lines.map((l) => l.raw).join('\n').trim() : null,
|
|
202
|
+
sectionsAfterErrata: errata ? sections.filter((s) => s.line > errata.line).map((s) => s.name) : [],
|
|
203
|
+
};
|
|
204
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { templateSourcePath, writeIfAbsent } from '../../core/templates/template-seed.js';
|
|
3
|
+
import { getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
|
|
4
|
+
|
|
5
|
+
const FEATURE_PLACEHOLDER = /\{\{FEATURE_NAME\}\}/g;
|
|
6
|
+
|
|
7
|
+
export function planTemplatePath(cwd = process.cwd()) {
|
|
8
|
+
return templateSourcePath('plan', cwd);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function renderPlanSkeleton(featureName, cwd = process.cwd()) {
|
|
12
|
+
return readFileSync(planTemplatePath(cwd), 'utf8').replace(FEATURE_PLACEHOLDER, featureName);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** @returns {{ path: string, written: boolean }} `written: false` when the file already existed. */
|
|
16
|
+
export function writePlanSkeleton(featureName, cwd = process.cwd()) {
|
|
17
|
+
return writeIfAbsent(getAbsoluteOutputPath(cwd, featureName, 'plan'), renderPlanSkeleton(featureName, cwd));
|
|
18
|
+
}
|
|
@@ -1,19 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* portfolio reads. Nothing else records it: each project only knows its own
|
|
4
|
-
* `.morph/`.
|
|
5
|
-
*
|
|
6
|
-
* Three writers register a project as a side note of what they already do —
|
|
7
|
-
* `init`, `update` (never under `--dry-run`) and `dashboard` — and the
|
|
8
|
-
* dashboard adds and removes on request. Registering is fail-open: a registry
|
|
9
|
-
* that cannot be read or written never fails the command that tried.
|
|
10
|
-
*
|
|
11
|
-
* The file lives in the user's home (`~/.morph-spec/projects.json`, or
|
|
12
|
-
* `MORPH_PROJECTS_REGISTRY`), read through `os.homedir()` at call time so a
|
|
13
|
-
* suite that isolates the home also isolates this file. Paths under the
|
|
14
|
-
* system temp dir are never registered: the test suites run `init`/`update`
|
|
15
|
-
* there by the hundred, including through spawned CLIs that keep the real home.
|
|
16
|
-
*/
|
|
1
|
+
// Paths under the system temp dir are never registered: test suites run `init`/`update` there by the
|
|
2
|
+
// hundred, through spawned CLIs that keep the real home. The home is read at call time so isolating it works.
|
|
17
3
|
|
|
18
4
|
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
19
5
|
import { homedir, tmpdir } from 'os';
|
|
@@ -35,7 +21,6 @@ export function projectKey(dir) {
|
|
|
35
21
|
|
|
36
22
|
const sameDir = (a, b) => (process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b);
|
|
37
23
|
|
|
38
|
-
/** A folder is a morph-spec project when morph-spec installed its config there. */
|
|
39
24
|
export function isMorphProject(dir) {
|
|
40
25
|
return existsSync(join(dir, '.morph', 'config', 'config.json'));
|
|
41
26
|
}
|
|
@@ -61,18 +46,13 @@ function projectName(dir) {
|
|
|
61
46
|
}
|
|
62
47
|
}
|
|
63
48
|
|
|
64
|
-
/**
|
|
65
|
-
* The registered projects that still exist. A folder that was deleted or lost
|
|
66
|
-
* its `.morph/` drops out on read — the file is rewritten on the next write.
|
|
67
|
-
*
|
|
68
|
-
* @returns {{ version: number, projects: Array<{ dir: string, name: string, addedAt: string, lastSeenAt: string, source: string }> }}
|
|
69
|
-
*/
|
|
49
|
+
/** Folders that were deleted or lost `.morph/` drop out on read. */
|
|
70
50
|
export function readRegistry() {
|
|
71
51
|
let data = null;
|
|
72
52
|
try {
|
|
73
53
|
const path = registryPath();
|
|
74
54
|
if (existsSync(path)) data = JSON.parse(readFileSync(path, 'utf8'));
|
|
75
|
-
} catch {
|
|
55
|
+
} catch {}
|
|
76
56
|
const projects = Array.isArray(data?.projects) ? data.projects : [];
|
|
77
57
|
return {
|
|
78
58
|
version: REGISTRY_VERSION,
|
|
@@ -84,16 +64,7 @@ function write(projects) {
|
|
|
84
64
|
atomicWriteJson(registryPath(), { version: REGISTRY_VERSION, projects });
|
|
85
65
|
}
|
|
86
66
|
|
|
87
|
-
/**
|
|
88
|
-
* Note that `dir` is a morph-spec project. Idempotent; refreshes `lastSeenAt`.
|
|
89
|
-
* Never throws.
|
|
90
|
-
*
|
|
91
|
-
* @param {string} dir
|
|
92
|
-
* @param {'init'|'update'|'dashboard'|'manual'} source
|
|
93
|
-
* @param {{ tempRoot?: string }} [opts] - test seam: the folder whose
|
|
94
|
-
* projects are never registered (the system temp dir)
|
|
95
|
-
* @returns {boolean} true when the project is in the registry afterwards
|
|
96
|
-
*/
|
|
67
|
+
/** Idempotent, never throws; `tempRoot` is the test seam for the never-registered folder. */
|
|
97
68
|
export function registerProject(dir, source, { tempRoot = tmpdir() } = {}) {
|
|
98
69
|
try {
|
|
99
70
|
const key = projectKey(dir);
|
|
@@ -115,7 +86,7 @@ export function registerProject(dir, source, { tempRoot = tmpdir() } = {}) {
|
|
|
115
86
|
}
|
|
116
87
|
}
|
|
117
88
|
|
|
118
|
-
/**
|
|
89
|
+
/** Never throws; the project's files are not touched. */
|
|
119
90
|
export function unregisterProject(dir) {
|
|
120
91
|
try {
|
|
121
92
|
const key = projectKey(dir);
|
|
@@ -128,15 +99,7 @@ export function unregisterProject(dir) {
|
|
|
128
99
|
}
|
|
129
100
|
}
|
|
130
101
|
|
|
131
|
-
/**
|
|
132
|
-
* morph-spec projects directly under each root that are not registered yet —
|
|
133
|
-
* the portfolio offers them, the person decides. One level deep: a repos
|
|
134
|
-
* folder is the expected root, and a deeper walk would enter node_modules and
|
|
135
|
-
* worktrees for nothing.
|
|
136
|
-
*
|
|
137
|
-
* @param {string[]} roots
|
|
138
|
-
* @returns {Array<{ dir: string, name: string }>}
|
|
139
|
-
*/
|
|
102
|
+
/** Unregistered projects one level under each root: deeper would walk node_modules and worktrees. */
|
|
140
103
|
export function discoverProjects(roots) {
|
|
141
104
|
const registered = readRegistry().projects.map((p) => p.dir);
|
|
142
105
|
const out = [];
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { OUTPUT_SCHEMA, getAbsoluteOutputPath } from '../core/paths/output-schema.js';
|
|
4
|
+
import { PARTIAL_MARKER } from '../../framework/hooks/shared/slice-progress.js';
|
|
5
|
+
|
|
6
|
+
export const VERDICT_TOKEN = '**Verdict:**';
|
|
7
|
+
export const REVIEWED_SHA_TOKEN = '**Reviewed-SHA:**';
|
|
8
|
+
export const VERDICTS = ['SHIP', 'FIX'];
|
|
9
|
+
export const BLOCKER_TYPES = ['check', 'teste', 'regressao', 'seguranca', 'escopo', 'arquitetura', 'performance'];
|
|
10
|
+
export const CHECK_STATUSES = ['PROVADO', 'NÃO PROVADO', 'FALHA'];
|
|
11
|
+
|
|
12
|
+
export const REVIEW_ROLES = {
|
|
13
|
+
verifier: { output: 'verifierReport', persona: 'morph-verifier' },
|
|
14
|
+
reviewer: { output: 'reviewerReport', persona: 'morph-reviewer' },
|
|
15
|
+
security: { output: 'securityReport', persona: 'morph-security' },
|
|
16
|
+
};
|
|
17
|
+
const ROLE_ORDER = Object.keys(REVIEW_ROLES);
|
|
18
|
+
|
|
19
|
+
export const reviewFileName = (role) => OUTPUT_SCHEMA[REVIEW_ROLES[role].output].filename;
|
|
20
|
+
|
|
21
|
+
// Append-only: uma reavaliação acrescenta ao fim, e vale o ÚLTIMO token/seção.
|
|
22
|
+
const VERDICT_RE = /\*\*Verdict:\*\*\s*([A-Za-z]+)/g;
|
|
23
|
+
const REVIEWED_SHA_RE = /\*\*Reviewed-SHA:\*\*\s*`?([0-9a-f]{7,40})/gi;
|
|
24
|
+
const sectionRe = (title) => new RegExp(`^##\\s+${title}\\s*$`, 'gim');
|
|
25
|
+
const BLOCKERS_SECTION_RE = sectionRe('Bloqueadores');
|
|
26
|
+
const CHECKS_SECTION_RE = sectionRe('Checks');
|
|
27
|
+
const MEDIA_SECTION_RE = /^##\s+M[íi]dia\s+gerada\s*$/m;
|
|
28
|
+
const SCREEN_CHECKLIST_SECTION_RE = sectionRe('Checklist\\s+de\\s+tela');
|
|
29
|
+
|
|
30
|
+
/** Hotfix: só o verificador, escrito pelo orquestrador sem dispatch. */
|
|
31
|
+
export function requiredReviewRoles({ workType, securityRequired = false } = {}) {
|
|
32
|
+
if (workType === 'hotfix') return ['verifier'];
|
|
33
|
+
return securityRequired ? ['verifier', 'reviewer', 'security'] : ['verifier', 'reviewer'];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function sectionBodies(text, re) {
|
|
37
|
+
return [...text.matchAll(re)].map((m) => {
|
|
38
|
+
const rest = text.slice(m.index + m[0].length);
|
|
39
|
+
const next = rest.search(/^#{1,2}\s/m);
|
|
40
|
+
return next === -1 ? rest : rest.slice(0, next);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function lastSectionBody(text, re) {
|
|
45
|
+
const bodies = sectionBodies(text, re);
|
|
46
|
+
return bodies.length ? bodies[bodies.length - 1] : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const normalize = (s) => String(s ?? '').toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/\s+/g, ' ').trim();
|
|
50
|
+
const cleanId = (s) => String(s ?? '').replace(/[*`_]/g, '').trim().toUpperCase();
|
|
51
|
+
|
|
52
|
+
function tableRows(body) {
|
|
53
|
+
const rows = [];
|
|
54
|
+
for (const line of body.split(/\r?\n/)) {
|
|
55
|
+
if (!/^\s*\|/.test(line)) continue;
|
|
56
|
+
const cells = line.trim().replace(/^\||\|$/g, '').split('|').map((c) => c.trim());
|
|
57
|
+
if (cells.every((c) => /^:?-{2,}:?$/.test(c) || c === '')) continue;
|
|
58
|
+
rows.push(cells);
|
|
59
|
+
}
|
|
60
|
+
return rows;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// `| id | tipo | onde | evidência |`; `untyped` = tipo fora de BLOCKER_TYPES.
|
|
64
|
+
function parseBlockers(body) {
|
|
65
|
+
const blockers = [];
|
|
66
|
+
const untyped = [];
|
|
67
|
+
for (const cells of tableRows(body)) {
|
|
68
|
+
if (/^#$|^id$/i.test(cells[0]) && /^tipo$/i.test(cells[1] ?? '')) continue;
|
|
69
|
+
const row = { id: cleanId(cells[0]), type: normalize(cells[1]), where: cells[2] ?? '', evidence: cells[3] ?? '' };
|
|
70
|
+
(BLOCKER_TYPES.includes(row.type) ? blockers : untyped).push(row);
|
|
71
|
+
}
|
|
72
|
+
return { blockers, untyped };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const STATUS_BY_KEY = Object.fromEntries(CHECK_STATUSES.map((s) => [normalize(s), s]));
|
|
76
|
+
|
|
77
|
+
// Vale a última linha de cada Cn em todas as seções: uma confirmação traz só os Cn que mudou.
|
|
78
|
+
function parseChecks(bodies) {
|
|
79
|
+
const byId = new Map();
|
|
80
|
+
for (const cells of bodies.flatMap(tableRows)) {
|
|
81
|
+
const id = cleanId(cells[0]);
|
|
82
|
+
if (!/^C\d+$/.test(id)) continue;
|
|
83
|
+
const raw = String(cells[1] ?? '').replace(/[*`]/g, '').trim();
|
|
84
|
+
byId.delete(id);
|
|
85
|
+
byId.set(id, { id, status: STATUS_BY_KEY[normalize(raw)] ?? raw, evidence: cells[2] ?? '' });
|
|
86
|
+
}
|
|
87
|
+
return [...byId.values()];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Um relatório de review lido do texto. `checks` é `null` sem a seção `## Checks`. */
|
|
91
|
+
export function parseReviewReport(text) {
|
|
92
|
+
if (typeof text !== 'string') {
|
|
93
|
+
return { partial: false, verdict: undefined, blockers: [], untyped: [], superseded: [], checks: null, reviewedSha: null };
|
|
94
|
+
}
|
|
95
|
+
const firstLine = text.split(/\r?\n/).find((l) => l.trim() !== '') ?? '';
|
|
96
|
+
const verdicts = [...text.matchAll(VERDICT_RE)].map((m) => m[1].toUpperCase());
|
|
97
|
+
const blockersBody = lastSectionBody(text, BLOCKERS_SECTION_RE);
|
|
98
|
+
const checksBodies = sectionBodies(text, CHECKS_SECTION_RE);
|
|
99
|
+
const shas = [...text.matchAll(REVIEWED_SHA_RE)].map((m) => m[1]);
|
|
100
|
+
return {
|
|
101
|
+
partial: PARTIAL_MARKER.test(firstLine),
|
|
102
|
+
verdict: verdicts.length ? verdicts[verdicts.length - 1] : undefined,
|
|
103
|
+
...(blockersBody === null ? { blockers: [], untyped: [] } : parseBlockers(blockersBody)),
|
|
104
|
+
superseded: verdicts.slice(0, -1),
|
|
105
|
+
checks: checksBodies.length === 0 ? null : parseChecks(checksBodies),
|
|
106
|
+
reviewedSha: shas.length ? shas[shas.length - 1] : null,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// `partial`: `PARCIAL` na 1ª linha, com ou sem veredito. `ok` só diz legível: quem decide é o risk-detector.
|
|
111
|
+
function stateOf(path, text) {
|
|
112
|
+
if (text === null) return { status: 'missing', path };
|
|
113
|
+
const parsed = parseReviewReport(text);
|
|
114
|
+
if (parsed.partial) return { status: 'partial', path, ...parsed };
|
|
115
|
+
if (!VERDICTS.includes(parsed.verdict)) return { status: 'unverdicted', path, ...parsed };
|
|
116
|
+
return { status: 'ok', path, ...parsed };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function readText(path) {
|
|
120
|
+
try {
|
|
121
|
+
return existsSync(path) ? readFileSync(path, 'utf8') : null;
|
|
122
|
+
} catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Os três relatórios a partir do diretório da feature — sem git; o dashboard lê árvores arquivadas. */
|
|
128
|
+
export function reviewReportsFromDir(featureDir) {
|
|
129
|
+
const out = {};
|
|
130
|
+
for (const role of ROLE_ORDER) {
|
|
131
|
+
const path = join(featureDir, '4-review', reviewFileName(role));
|
|
132
|
+
out[role] = stateOf(path, readText(path));
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function reviewReportPath(cwd, featureName, role) {
|
|
138
|
+
return getAbsoluteOutputPath(cwd, featureName, REVIEW_ROLES[role].output);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function reviewReports(cwd, featureName) {
|
|
142
|
+
return reviewReportsFromDir(join(cwd, '.morph', 'features', featureName));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Todo Cn precisa de uma linha, e só `PROVADO` cobre. */
|
|
146
|
+
export function checksCoverage(checkIds, rows) {
|
|
147
|
+
const byId = new Map((rows ?? []).map((r) => [r.id, r]));
|
|
148
|
+
const missing = [];
|
|
149
|
+
const notProven = [];
|
|
150
|
+
for (const id of checkIds.map(cleanId)) {
|
|
151
|
+
const row = byId.get(id);
|
|
152
|
+
if (!row) missing.push(id);
|
|
153
|
+
else if (row.status !== 'PROVADO') notProven.push({ id, status: row.status || 'vazio' });
|
|
154
|
+
}
|
|
155
|
+
return { missing, notProven };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** O sinal que `reviewPause` lê; `checkIds: null` = cobertura não conferível. */
|
|
159
|
+
export function reviewSignal(reports, { required, checkIds }) {
|
|
160
|
+
const out = { required, reports: {} };
|
|
161
|
+
for (const role of ROLE_ORDER) {
|
|
162
|
+
const r = reports[role];
|
|
163
|
+
if (!required.includes(role) && r.status === 'missing') continue;
|
|
164
|
+
out.reports[role] = r.status === 'missing'
|
|
165
|
+
? { status: 'missing' }
|
|
166
|
+
: {
|
|
167
|
+
status: r.status,
|
|
168
|
+
verdict: r.verdict ?? null,
|
|
169
|
+
blockers: r.blockers.length,
|
|
170
|
+
blockerIds: r.blockers.map((b) => b.id),
|
|
171
|
+
untyped: r.untyped.length,
|
|
172
|
+
superseded: r.superseded,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
out.checks = checkIds === null
|
|
176
|
+
? { status: 'no-checks', missing: [], notProven: [] }
|
|
177
|
+
: { status: 'ok', total: checkIds.length, ...checksCoverage(checkIds, reports.verifier.checks) };
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Veredito combinado para leitura (dashboard, list): FIX se qualquer relatório
|
|
183
|
+
* presente diz FIX ou tem bloqueador; SHIP só com todo exigido em SHIP.
|
|
184
|
+
*/
|
|
185
|
+
export function combinedVerdict(reports, required) {
|
|
186
|
+
const present = ROLE_ORDER.filter((r) => reports[r].status !== 'missing');
|
|
187
|
+
if (present.some((r) => reports[r].verdict === 'FIX' || reports[r].blockers.length > 0)) return 'FIX';
|
|
188
|
+
return required.every((r) => reports[r].status === 'ok' && reports[r].verdict === 'SHIP') ? 'SHIP' : null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function allBlockers(reports) {
|
|
192
|
+
return ROLE_ORDER.flatMap((role) => (reports[role].blockers ?? []).map((b) => ({ ...b, role })));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function verifierMediaSection(cwd, featureName) {
|
|
196
|
+
const text = readText(reviewReportPath(cwd, featureName, 'verifier'));
|
|
197
|
+
return text !== null && MEDIA_SECTION_RE.test(text);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function verifierScreenChecklist(cwd, featureName) {
|
|
201
|
+
const text = readText(reviewReportPath(cwd, featureName, 'verifier'));
|
|
202
|
+
const body = text === null ? null : lastSectionBody(text, SCREEN_CHECKLIST_SECTION_RE);
|
|
203
|
+
return body === null ? { section: false, body: '' } : { section: true, body };
|
|
204
|
+
}
|