@opengsd/gsd-core 1.12.0 → 1.14.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-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +12 -0
- package/agents/gsd-advisor-researcher.compact.md +85 -0
- package/agents/gsd-ai-researcher.compact.md +96 -0
- package/agents/gsd-assumptions-analyzer.compact.md +81 -0
- package/agents/gsd-code-fixer.compact.md +458 -0
- package/agents/gsd-code-fixer.md +5 -5
- package/agents/gsd-code-reviewer.compact.md +269 -0
- package/agents/gsd-code-reviewer.md +15 -3
- package/agents/gsd-codebase-mapper.compact.md +760 -0
- package/agents/gsd-debug-session-manager.compact.md +345 -0
- package/agents/gsd-doc-classifier.compact.md +192 -0
- package/agents/gsd-doc-synthesizer.compact.md +200 -0
- package/agents/gsd-doc-verifier.compact.md +143 -0
- package/agents/gsd-doc-writer.compact.md +440 -0
- package/agents/gsd-dom-verifier.compact.md +138 -0
- package/agents/gsd-domain-researcher.compact.md +141 -0
- package/agents/gsd-eval-auditor.compact.md +160 -0
- package/agents/gsd-eval-planner.compact.md +137 -0
- package/agents/gsd-executor.md +63 -35
- package/agents/gsd-framework-selector.compact.md +82 -0
- package/agents/gsd-integration-checker.compact.md +245 -0
- package/agents/gsd-intel-updater.compact.md +226 -0
- package/agents/gsd-mempalace-curator.compact.md +45 -0
- package/agents/gsd-nyquist-auditor.compact.md +179 -0
- package/agents/gsd-pattern-mapper.compact.md +275 -0
- package/agents/gsd-plan-checker.md +76 -57
- package/agents/gsd-planner.md +14 -0
- package/agents/gsd-project-researcher.compact.md +587 -0
- package/agents/gsd-research-synthesizer.compact.md +212 -0
- package/agents/gsd-roadmapper.compact.md +454 -0
- package/agents/gsd-roadmapper.md +13 -0
- package/agents/gsd-security-auditor.compact.md +162 -0
- package/agents/gsd-ui-auditor.compact.md +404 -0
- package/agents/gsd-ui-checker.compact.md +277 -0
- package/agents/gsd-ui-checker.md +19 -3
- package/agents/gsd-ui-researcher.compact.md +282 -0
- package/agents/gsd-ui-researcher.md +29 -0
- package/agents/gsd-user-profiler.compact.md +108 -0
- package/agents/gsd-verifier.md +23 -1
- package/bin/install.js +444 -134
- package/commands/gsd/cleanup.md +1 -0
- package/commands/gsd/code-review.md +2 -1
- package/commands/gsd/complete-milestone.md +1 -0
- package/commands/gsd/config.md +1 -0
- package/commands/gsd/debug.md +1 -0
- package/commands/gsd/execute-phase.md +1 -1
- package/commands/gsd/graphify.md +1 -0
- package/commands/gsd/health.md +1 -0
- package/commands/gsd/mempalace-capture.md +1 -0
- package/commands/gsd/mempalace-recall.md +1 -0
- package/commands/gsd/new-milestone.md +1 -0
- package/commands/gsd/new-project.md +1 -0
- package/commands/gsd/next.md +1 -0
- package/commands/gsd/ns-workflow.md +2 -1
- package/commands/gsd/pause-work.md +1 -0
- package/commands/gsd/phase.md +2 -1
- package/commands/gsd/pr-branch.md +1 -0
- package/commands/gsd/quick-batch.md +105 -0
- package/commands/gsd/resume-work.md +1 -0
- package/commands/gsd/review-backlog.md +1 -0
- package/commands/gsd/settings.md +2 -1
- package/commands/gsd/stats.md +1 -0
- package/commands/gsd/surface.md +18 -8
- package/commands/gsd/thread.md +1 -0
- package/commands/gsd/workspace.md +1 -0
- package/commands/gsd/workstreams.md +1 -0
- package/gsd-core/bin/check-latest-version.cjs +8 -3
- package/gsd-core/bin/gsd-tools.cjs +532 -174
- package/gsd-core/bin/lib/adr-parser.cjs +1 -1
- package/gsd-core/bin/lib/artifacts.cjs +2 -1
- package/gsd-core/bin/lib/audit.cjs +39 -22
- package/gsd-core/bin/lib/broken-windows.cjs +168 -49
- package/gsd-core/bin/lib/capability-activation.cjs +27 -0
- package/gsd-core/bin/lib/capability-lifecycle.cjs +10 -6
- package/gsd-core/bin/lib/capability-loader.cjs +135 -1
- package/gsd-core/bin/lib/capability-registry.cjs +528 -116
- package/gsd-core/bin/lib/capability-source.cjs +19 -2
- package/gsd-core/bin/lib/capability-state.cjs +7 -1
- package/gsd-core/bin/lib/capability-validator.cjs +134 -5
- package/gsd-core/bin/lib/capability-writer.cjs +14 -4
- package/gsd-core/bin/lib/check-command-router.cjs +198 -38
- package/gsd-core/bin/lib/claude-orchestration.cjs +10 -25
- package/gsd-core/bin/lib/clusters.cjs +1 -0
- package/gsd-core/bin/lib/code-review-depth.cjs +2 -2
- package/gsd-core/bin/lib/command-aliases.cjs +16 -0
- package/gsd-core/bin/lib/commands.cjs +981 -79
- package/gsd-core/bin/lib/config-loader.cjs +4 -0
- package/gsd-core/bin/lib/config.cjs +153 -38
- package/gsd-core/bin/lib/core-utils.cjs +34 -7
- package/gsd-core/bin/lib/coverage.cjs +1 -1
- package/gsd-core/bin/lib/decisions.cjs +343 -28
- package/gsd-core/bin/lib/edge-probe.cjs +14 -1
- package/gsd-core/bin/lib/external-descriptor-trust.cjs +29 -14
- package/gsd-core/bin/lib/file-overlap-partitioner.cjs +74 -0
- package/gsd-core/bin/lib/frontmatter.cjs +137 -23
- package/gsd-core/bin/lib/gap-checker.cjs +22 -13
- package/gsd-core/bin/lib/git-base-branch.cjs +10 -2
- package/gsd-core/bin/lib/gsd2-import.cjs +1 -2
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +8 -2
- package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +54 -11
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +87 -23
- package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +1 -1
- package/gsd-core/bin/lib/host-integration.cjs +57 -5
- package/gsd-core/bin/lib/init-command-router.cjs +14 -0
- package/gsd-core/bin/lib/init.cjs +539 -60
- package/gsd-core/bin/lib/install-engine.cjs +199 -14
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +45 -0
- package/gsd-core/bin/lib/install-profiles.cjs +36 -14
- package/gsd-core/bin/lib/installer-migration-report.cjs +1 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +33 -4
- package/gsd-core/bin/lib/io.cjs +35 -0
- package/gsd-core/bin/lib/loop-resolver.cjs +64 -39
- package/gsd-core/bin/lib/markdown-table.cjs +123 -0
- package/gsd-core/bin/lib/mcp-catalog.cjs +2 -2
- package/gsd-core/bin/lib/milestone.cjs +41 -10
- package/gsd-core/bin/lib/model-resolver.cjs +101 -10
- package/gsd-core/bin/lib/phase-command-router.cjs +20 -7
- package/gsd-core/bin/lib/phase-id.cjs +412 -31
- package/gsd-core/bin/lib/phase-lifecycle.cjs +61 -0
- package/gsd-core/bin/lib/phase.cjs +941 -98
- package/gsd-core/bin/lib/plan-document.cjs +10 -0
- package/gsd-core/bin/lib/planning-inspect.cjs +34 -18
- package/gsd-core/bin/lib/planning-snapshot.cjs +206 -30
- package/gsd-core/bin/lib/planning-workspace.cjs +153 -29
- package/gsd-core/bin/lib/pristine-baseline.cjs +182 -0
- package/gsd-core/bin/lib/prohibition-enforcement.cjs +91 -4
- package/gsd-core/bin/lib/quick-batch-command-router.cjs +285 -0
- package/gsd-core/bin/lib/quick-batch-dispatch.cjs +250 -0
- package/gsd-core/bin/lib/quick-batch.cjs +840 -0
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +61 -2
- package/gsd-core/bin/lib/research-store.cjs +11 -12
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +53 -5
- package/gsd-core/bin/lib/review-lane-invocation.cjs +96 -1
- package/gsd-core/bin/lib/review-lane-runner.cjs +136 -10
- package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +337 -0
- package/gsd-core/bin/lib/roadmap-parser.cjs +555 -41
- package/gsd-core/bin/lib/roadmap.cjs +292 -69
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +260 -43
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +28 -20
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +294 -108
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +408 -47
- package/gsd-core/bin/lib/security.cjs +126 -7
- package/gsd-core/bin/lib/shell-command-projection.cjs +4 -0
- package/gsd-core/bin/lib/smart-entry.cjs +7 -9
- package/gsd-core/bin/lib/state-document.cjs +159 -32
- package/gsd-core/bin/lib/state-md-schema.cjs +44 -27
- package/gsd-core/bin/lib/state-transition.cjs +465 -62
- package/gsd-core/bin/lib/state.cjs +906 -151
- package/gsd-core/bin/lib/surface.cjs +83 -10
- package/gsd-core/bin/lib/task-command-router.cjs +12 -6
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +133 -0
- package/gsd-core/bin/lib/uat.cjs +1420 -516
- package/gsd-core/bin/lib/update-context.cjs +36 -26
- package/gsd-core/bin/lib/validate.cjs +230 -12
- package/gsd-core/bin/lib/vendor/js-yaml.cjs +11 -3
- package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
- package/gsd-core/bin/lib/verification.cjs +316 -23
- package/gsd-core/bin/lib/verify-command-grounding.cjs +1 -1
- package/gsd-core/bin/lib/verify-command-router.cjs +1 -0
- package/gsd-core/bin/lib/verify.cjs +531 -36
- package/gsd-core/bin/lib/workstream-inventory.cjs +21 -2
- package/gsd-core/bin/lib/worktree-safety.cjs +21 -7
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +13 -0
- package/gsd-core/bin/verify-reapply-patches.cjs +507 -81
- package/gsd-core/references/agent-contracts.md +3 -3
- package/gsd-core/references/compact-content-gate.md +66 -0
- package/gsd-core/references/edge-probe.md +17 -13
- package/gsd-core/references/execute-mvp-tdd.md +18 -16
- package/gsd-core/references/execute-phase-response-language.md +6 -0
- package/gsd-core/references/executor-examples.md +42 -0
- package/gsd-core/references/few-shot-examples/plan-checker.md +15 -15
- package/gsd-core/references/loop-hook-dispatch.md +18 -0
- package/gsd-core/references/model-profiles.md +12 -3
- package/gsd-core/references/mvp-concepts.md +2 -2
- package/gsd-core/references/plan-checker-examples.md +41 -0
- package/gsd-core/references/planner-antipatterns.md +25 -0
- package/gsd-core/references/planner-chunked.md +5 -1
- package/gsd-core/references/planner-coupling.md +42 -0
- package/gsd-core/references/planner-quick-batch.md +71 -0
- package/gsd-core/references/planner-reviews.md +47 -0
- package/gsd-core/references/planner-revision.md +75 -2
- package/gsd-core/references/planning-config.md +5 -1
- package/gsd-core/references/response-language-directive.md +9 -0
- package/gsd-core/references/revision-loop.md +118 -11
- package/gsd-core/references/tdd.md +17 -9
- package/gsd-core/references/thinking-models-planning.md +18 -2
- package/gsd-core/references/verification-patterns.md +17 -4
- package/gsd-core/references/verifier-evidence-gate.md +160 -0
- package/gsd-core/references/worktree-path-safety.md +112 -2
- package/gsd-core/templates/README.md +7 -1
- package/gsd-core/templates/phase-prompt.md +4 -0
- package/gsd-core/templates/state.md +6 -3
- package/gsd-core/templates/summary.compact.md +212 -0
- package/gsd-core/templates/user-setup.compact.md +199 -0
- package/gsd-core/templates/user-setup.md +0 -9
- package/gsd-core/templates/verification-report.md +5 -0
- package/gsd-core/workflows/add-backlog.md +2 -0
- package/gsd-core/workflows/add-phase.md +2 -0
- package/gsd-core/workflows/add-tests.md +1 -1
- package/gsd-core/workflows/add-todo.md +4 -3
- package/gsd-core/workflows/ai-integration-phase.md +1 -1
- package/gsd-core/workflows/analyze-dependencies.md +2 -0
- package/gsd-core/workflows/audit-fix.md +2 -0
- package/gsd-core/workflows/audit-milestone.md +2 -0
- package/gsd-core/workflows/audit-uat.md +2 -0
- package/gsd-core/workflows/autonomous.md +15 -10
- package/gsd-core/workflows/check-todos.md +5 -3
- package/gsd-core/workflows/cleanup.md +4 -2
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +22 -13
- package/gsd-core/workflows/code-review-fix.md +5 -3
- package/gsd-core/workflows/code-review.md +211 -43
- package/gsd-core/workflows/complete-milestone/detail/elaboration.md +274 -0
- package/gsd-core/workflows/complete-milestone.md +40 -254
- package/gsd-core/workflows/debug.md +1 -1
- package/gsd-core/workflows/diagnose-issues.md +5 -1
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/all.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/analyze.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/auto.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/batch.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/default.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/power.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/text.md +2 -0
- package/gsd-core/workflows/discuss-phase/templates/context.md +2 -0
- package/gsd-core/workflows/discuss-phase/templates/discussion-log.md +2 -0
- package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
- package/gsd-core/workflows/discuss-phase-power.md +2 -0
- package/gsd-core/workflows/discuss-phase.md +1 -1
- package/gsd-core/workflows/do.md +43 -13
- package/gsd-core/workflows/docs-update/detail/elaboration.md +179 -0
- package/gsd-core/workflows/docs-update.md +15 -156
- package/gsd-core/workflows/edit-phase.md +2 -0
- package/gsd-core/workflows/eval-review.md +1 -1
- package/gsd-core/workflows/execute-phase/detail/elaboration.md +124 -0
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +20 -3
- package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +56 -0
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +24 -3
- package/gsd-core/workflows/execute-phase/steps/executor-progress-policy.md +43 -0
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +8 -2
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -0
- package/gsd-core/workflows/execute-phase/steps/sequential-root-pin.md +35 -0
- package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +25 -0
- package/gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md +2 -0
- package/gsd-core/workflows/execute-phase.md +78 -159
- package/gsd-core/workflows/execute-plan.md +28 -15
- package/gsd-core/workflows/explore.md +2 -0
- package/gsd-core/workflows/extract-learnings.md +2 -0
- package/gsd-core/workflows/fast.md +6 -0
- package/gsd-core/workflows/forensics.md +2 -0
- package/gsd-core/workflows/graduation.md +1 -1
- package/gsd-core/workflows/health.md +1 -1
- package/gsd-core/workflows/help/modes/brief.md +2 -0
- package/gsd-core/workflows/help/modes/default.md +2 -0
- package/gsd-core/workflows/help/modes/full.compact.md +398 -0
- package/gsd-core/workflows/help/modes/full.md +12 -0
- package/gsd-core/workflows/help/modes/topic.md +2 -0
- package/gsd-core/workflows/help.md +3 -1
- package/gsd-core/workflows/import.md +3 -3
- package/gsd-core/workflows/inbox.md +1 -1
- package/gsd-core/workflows/ingest-docs.md +1 -1
- package/gsd-core/workflows/insert-phase.md +2 -0
- package/gsd-core/workflows/list-phase-assumptions.md +2 -0
- package/gsd-core/workflows/list-seeds.md +2 -0
- package/gsd-core/workflows/list-workspaces.md +2 -0
- package/gsd-core/workflows/manager.md +3 -3
- package/gsd-core/workflows/map-codebase.md +52 -3
- package/gsd-core/workflows/milestone-summary.md +2 -0
- package/gsd-core/workflows/mvp-phase.md +1 -1
- package/gsd-core/workflows/new-milestone.md +55 -13
- package/gsd-core/workflows/new-project/detail/elaboration.md +216 -0
- package/gsd-core/workflows/new-project.md +37 -205
- package/gsd-core/workflows/new-workspace.md +1 -1
- package/gsd-core/workflows/next.md +2 -0
- package/gsd-core/workflows/node-repair.md +2 -0
- package/gsd-core/workflows/note.md +2 -0
- package/gsd-core/workflows/onboard.md +1 -1
- package/gsd-core/workflows/pause-work.md +19 -4
- package/gsd-core/workflows/plan-phase/detail/elaboration.md +209 -0
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +100 -18
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +2 -0
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +9 -0
- package/gsd-core/workflows/plan-phase.md +144 -185
- package/gsd-core/workflows/plan-review-convergence.md +102 -10
- package/gsd-core/workflows/plant-seed.md +1 -1
- package/gsd-core/workflows/pr-branch.md +30 -10
- package/gsd-core/workflows/profile-user.md +1 -1
- package/gsd-core/workflows/progress/steps/forensic-audit.md +1 -1
- package/gsd-core/workflows/progress.md +25 -3
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +37 -2
- package/gsd-core/workflows/quick/steps/research-phase.md +3 -3
- package/gsd-core/workflows/quick-batch/steps/batch-init.md +55 -0
- package/gsd-core/workflows/quick-batch/steps/completion.md +65 -0
- package/gsd-core/workflows/quick-batch/steps/merge-wave.md +100 -0
- package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +147 -0
- package/gsd-core/workflows/quick-batch/steps/planner-wave.md +158 -0
- package/gsd-core/workflows/quick-batch/steps/research-phase.md +95 -0
- package/gsd-core/workflows/quick-batch/steps/resume-mode.md +49 -0
- package/gsd-core/workflows/quick-batch/steps/verification-wave.md +73 -0
- package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +169 -0
- package/gsd-core/workflows/quick-batch.md +203 -0
- package/gsd-core/workflows/quick.md +21 -4
- package/gsd-core/workflows/reapply-patches.md +79 -3
- package/gsd-core/workflows/remove-phase.md +2 -0
- package/gsd-core/workflows/remove-workspace.md +1 -1
- package/gsd-core/workflows/resume-project.md +6 -2
- package/gsd-core/workflows/review.md +215 -10
- package/gsd-core/workflows/scan.md +2 -0
- package/gsd-core/workflows/section-manifest.json +12 -0
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/session-report.md +2 -0
- package/gsd-core/workflows/settings-advanced.md +2 -0
- package/gsd-core/workflows/settings-integrations.md +9 -8
- package/gsd-core/workflows/settings.md +19 -6
- package/gsd-core/workflows/ship.md +10 -10
- package/gsd-core/workflows/sketch-wrap-up.md +2 -0
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/smart-entry.md +1 -1
- package/gsd-core/workflows/spec-phase.md +24 -19
- package/gsd-core/workflows/spike-wrap-up.md +2 -0
- package/gsd-core/workflows/spike.md +1 -1
- package/gsd-core/workflows/stats.md +2 -0
- package/gsd-core/workflows/sync-skills.md +12 -4
- package/gsd-core/workflows/thread.md +2 -0
- package/gsd-core/workflows/transition.md +2 -0
- package/gsd-core/workflows/ui-phase.md +26 -5
- package/gsd-core/workflows/ui-review.md +1 -1
- package/gsd-core/workflows/ultraplan-phase.md +2 -0
- package/gsd-core/workflows/undo.md +1 -1
- package/gsd-core/workflows/update.md +48 -43
- package/gsd-core/workflows/validate-phase.md +1 -1
- package/gsd-core/workflows/verify-work/detail/elaboration.md +230 -0
- package/gsd-core/workflows/verify-work.md +68 -182
- package/hooks/dist/gsd-agent-isolation-guard.js +42 -16
- package/hooks/dist/gsd-check-update-worker.js +19 -2
- package/hooks/dist/gsd-context-monitor.js +371 -27
- package/hooks/dist/gsd-cursor-subagent-start.js +34 -14
- package/hooks/dist/gsd-node-runner.sh +1 -0
- package/hooks/dist/gsd-prompt-guard.js +30 -5
- package/hooks/dist/gsd-read-guard.js +2 -0
- package/hooks/dist/gsd-read-injection-scanner.js +5 -5
- package/hooks/dist/gsd-secret-read-guard.js +1105 -0
- package/hooks/dist/gsd-statusline.js +18 -10
- package/hooks/dist/gsd-validate-commit.sh +474 -7
- package/hooks/dist/gsd-workflow-guard.js +2 -1
- package/hooks/dist/gsd-worktree-path-guard.js +25 -14
- package/hooks/dist/gsd-write-guard.js +46 -1
- package/hooks/dist/lib/dispatch-identity.js +187 -0
- package/hooks/dist/lib/filename-classification.js +64 -0
- package/hooks/dist/lib/git-cmd.js +210 -1
- package/hooks/dist/lib/injection-patterns.js +36 -6
- package/hooks/dist/lib/isolation-deny-reason.js +53 -1
- package/hooks/dist/lib/isolation-sentinel.js +58 -19
- package/hooks/dist/managed-hooks-registry.cjs +1 -0
- package/hooks/gsd-agent-isolation-guard.js +42 -16
- package/hooks/gsd-check-update-worker.js +19 -2
- package/hooks/gsd-context-monitor.js +371 -27
- package/hooks/gsd-cursor-subagent-start.js +34 -14
- package/hooks/gsd-node-runner.sh +1 -0
- package/hooks/gsd-prompt-guard.js +30 -5
- package/hooks/gsd-read-guard.js +2 -0
- package/hooks/gsd-read-injection-scanner.js +5 -5
- package/hooks/gsd-secret-read-guard.js +1105 -0
- package/hooks/gsd-statusline.js +18 -10
- package/hooks/gsd-validate-commit.sh +474 -7
- package/hooks/gsd-workflow-guard.js +2 -1
- package/hooks/gsd-worktree-path-guard.js +25 -14
- package/hooks/gsd-write-guard.js +46 -1
- package/hooks/hooks.json +6 -0
- package/hooks/lib/dispatch-identity.js +187 -0
- package/hooks/lib/filename-classification.js +64 -0
- package/hooks/lib/git-cmd.js +210 -1
- package/hooks/lib/injection-patterns.js +36 -6
- package/hooks/lib/isolation-deny-reason.js +53 -1
- package/hooks/lib/isolation-sentinel.js +58 -19
- package/hooks/managed-hooks-registry.cjs +1 -0
- package/package.json +13 -9
- package/scripts/benchmark-compact-content-variants.cjs +298 -0
- package/scripts/benchmark-compact-content.cjs +368 -0
- package/scripts/build-hooks.js +11 -4
- package/scripts/check-contract-drift.cjs +4 -1
- package/scripts/check-env.cjs +36 -8
- package/scripts/check-glossary-refs.cjs +25 -21
- package/scripts/ci-next-health.cjs +271 -0
- package/scripts/ci-prepare-test-scope.cjs +7 -7
- package/scripts/ci-test-scope.cjs +133 -20
- package/scripts/ci-timeout-report.cjs +1 -1
- package/scripts/diff-touches-shipped-paths.cjs +1 -1
- package/scripts/docs-guard-registry.cjs +17 -2
- package/scripts/gen-adr-index.cjs +8 -2
- package/scripts/gen-inventory-manifest.cjs +12 -0
- package/scripts/gen-loop-host-contract.cjs +67 -15
- package/scripts/gen-platform-conformance-tier.cjs +557 -0
- package/scripts/lib/drift-scan.cjs +1 -1
- package/scripts/lib/macos-conformance-tier.generated.cjs +210 -0
- package/scripts/lib/npm-version-check-diagnosis.cjs +59 -0
- package/scripts/lib/platform-conformance-tier.generated.cjs +276 -0
- package/scripts/lib/shellcheck-fetch.cjs +247 -0
- package/scripts/lib/suite-detection.cjs +32 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -6
- package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
- package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
- package/scripts/lint-allowed-tools-parity.cjs +221 -0
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +24 -2
- package/scripts/lint-phase-enumeration-drift.cjs +24 -6
- package/scripts/lint-phase-id-drift.cjs +465 -15
- package/scripts/lint-portable-grep.cjs +176 -0
- package/scripts/lint-response-language-coverage.cjs +530 -0
- package/scripts/lint-source-test-name-collision.cjs +1 -1
- package/scripts/lint-test-file-count.allowlist.json +4 -1
- package/scripts/lint-vendored-deps.cjs +128 -17
- package/scripts/lint-workflow-shellcheck-baseline.json +1112 -0
- package/scripts/lint-workflow-shellcheck.cjs +614 -0
- package/scripts/npm-audit-baseline.cjs +376 -0
- package/scripts/prompt-injection-scan.sh +22 -0
- package/scripts/require-issue-link-policy.cjs +16 -1
- package/scripts/workflow-size.cjs +139 -0
- package/skills/gsd-cleanup/SKILL.md +1 -0
- package/skills/gsd-code-review/SKILL.md +2 -1
- package/skills/gsd-complete-milestone/SKILL.md +1 -0
- package/skills/gsd-config/SKILL.md +1 -0
- package/skills/gsd-debug/SKILL.md +1 -0
- package/skills/gsd-execute-phase/SKILL.md +1 -1
- package/skills/gsd-graphify/SKILL.md +1 -0
- package/skills/gsd-health/SKILL.md +1 -0
- package/skills/gsd-mempalace-capture/SKILL.md +1 -0
- package/skills/gsd-mempalace-recall/SKILL.md +1 -0
- package/skills/gsd-new-milestone/SKILL.md +1 -0
- package/skills/gsd-new-project/SKILL.md +1 -0
- package/skills/gsd-next/SKILL.md +1 -0
- package/skills/gsd-ns-workflow/SKILL.md +1 -0
- package/skills/gsd-pause-work/SKILL.md +1 -0
- package/skills/gsd-phase/SKILL.md +2 -1
- package/skills/gsd-pr-branch/SKILL.md +1 -0
- package/skills/gsd-quick-batch/SKILL.md +105 -0
- package/skills/gsd-resume-work/SKILL.md +1 -0
- package/skills/gsd-review-backlog/SKILL.md +1 -0
- package/skills/gsd-settings/SKILL.md +2 -1
- package/skills/gsd-stats/SKILL.md +1 -0
- package/skills/gsd-surface/SKILL.md +18 -8
- package/skills/gsd-thread/SKILL.md +1 -0
- package/skills/gsd-workspace/SKILL.md +1 -0
- package/skills/gsd-workstreams/SKILL.md +1 -0
- package/vscode/package.json +1 -1
- package/gsd-core/templates/claude-md.md +0 -145
- package/gsd-core/templates/codebase/concerns.md +0 -310
- package/gsd-core/templates/codebase/conventions.md +0 -307
- package/gsd-core/templates/codebase/integrations.md +0 -280
- package/gsd-core/templates/codebase/structure.md +0 -285
- package/gsd-core/templates/codebase/testing.md +0 -480
- package/gsd-core/templates/debug-subagent-prompt.md +0 -91
- package/gsd-core/templates/discovery.md +0 -146
|
@@ -158,7 +158,156 @@ const BRACKET_PHASE_TOKEN_SOURCE = `\\d+[A-Z]?` +
|
|
|
158
158
|
// followed by a `Phase ` label) or a bare `Phase ` label; a bare number is NOT
|
|
159
159
|
// a phase-heading intro. The `[^\]]{1,200}` bound mirrors the existing
|
|
160
160
|
// roadmap-parser heading regexes (ReDoS-safe: a header is one short line).
|
|
161
|
+
//
|
|
162
|
+
// Retained as PR-1 shipped it. PR-2 does not consume it — see the gated
|
|
163
|
+
// selector below, which supersedes it — and it is left byte-identical so an
|
|
164
|
+
// already-merged epic export does not change its accepted language.
|
|
161
165
|
const PHASE_HEADING_PREFIX_SRC = '(?:\\[[^\\]]{1,200}\\]\\s*(?:Phase\\s+)?|Phase\\s+)';
|
|
166
|
+
// ── #612 PR-2: the ONE bracket identity grammar ─────────────────────────────
|
|
167
|
+
// A bracket phase-ID prefix is `{CODE}.{MM}`. Before this, three spellings of
|
|
168
|
+
// that shape lived in this file and disagreed: extractPhaseToken's bracket
|
|
169
|
+
// branch (`\d+`, case-sensitive), bracketQualifiedKey (`\d+`, mixed-case), and
|
|
170
|
+
// the dir prefix (`\d{2,}`). `GSD.2-05-feature` was simultaneously "not a phase
|
|
171
|
+
// directory" and "phase 05", depending on which one a caller reached. They are
|
|
172
|
+
// now one source.
|
|
173
|
+
//
|
|
174
|
+
// The milestone width mirrors the EMIT grammar rather than accepting any digit
|
|
175
|
+
// run: pad2() emits at least two digits, so two digits — or three-plus with no
|
|
176
|
+
// leading zero — is what toDir can produce. A bare `0` is NOT admitted: the
|
|
177
|
+
// padded `00` is the backlog sentinel's canonical identity and `\d{2}` already
|
|
178
|
+
// covers it, so nothing needs the unpadded spelling. (An earlier revision of
|
|
179
|
+
// this comment claimed the opposite; the constant below has always rejected it
|
|
180
|
+
// — #2867 review, Minor 2.) `[GSD.2] 05:` is therefore NOT a bracket id —
|
|
181
|
+
// which is the point: it is the shape that made the three spellings disagree.
|
|
182
|
+
// Reconciled with BRACKET_CANONICAL_NUMERIC_SOURCE above — the width toDir
|
|
183
|
+
// actually emits (pad2: 2 digits, or 3+ with no leading zero). The earlier
|
|
184
|
+
// `(?:\d{2,}|0)` diverged from it in both directions: it admitted `002`, which
|
|
185
|
+
// the emit validator rejects, and a bare `0` that pad2 never produces. Every
|
|
186
|
+
// bracket-milestone recognizer now derives from this one constant — the section
|
|
187
|
+
// recognizers previously spelled `0*N` or `\d+` and accepted `[GSD.2]`, which
|
|
188
|
+
// SCOPED a milestone no phase heading could then resolve into, recreating the
|
|
189
|
+
// on-disk-count fallback this PR exists to remove.
|
|
190
|
+
//
|
|
191
|
+
// An unpadded bracket is therefore MALFORMED, uniformly: it scopes nothing,
|
|
192
|
+
// bounds nothing, sections nothing, and is not a phase id. W005 on its
|
|
193
|
+
// directories is the signal that surfaces it.
|
|
194
|
+
const BRACKET_MILESTONE_NUMERIC_SRC = BRACKET_CANONICAL_NUMERIC_SOURCE;
|
|
195
|
+
// #2761 M3 (trek-e review): the bracket PROJECT-CODE class, as a named source
|
|
196
|
+
// rather than a class this file (and three readers outside it) each re-typed.
|
|
197
|
+
// The re-typed copies were the exact drift #2761's own gate forbids — "no token
|
|
198
|
+
// literal outside src/phase-id.cts" — and `check:phase-id-drift` did not see
|
|
199
|
+
// them, because its detector only knew the phase-NUMBER token grammar. The
|
|
200
|
+
// guard now carries a bracket rule too (scripts/lint-phase-id-drift.cjs).
|
|
201
|
+
const BRACKET_PROJECT_CODE_SRC = '[A-Z][A-Z0-9_]*';
|
|
202
|
+
const BRACKET_ID_SRC = `${BRACKET_PROJECT_CODE_SRC}\\.${BRACKET_MILESTONE_NUMERIC_SRC}`;
|
|
203
|
+
// The bracket MILESTONE INTRO — `[CODE.MM]` — in the two shapes its readers
|
|
204
|
+
// need. Both were re-typed verbatim outside this module before #2761 M3:
|
|
205
|
+
//
|
|
206
|
+
// * PINNED, to one already-resolved milestone integer. This owns the pad2
|
|
207
|
+
// spelling rule as well as the grammar, so "canonical spelling only, not
|
|
208
|
+
// `0*N`" (the rule that keeps `[GSD.2]` from scoping a milestone no phase
|
|
209
|
+
// heading can resolve into) lives in ONE place instead of being restated
|
|
210
|
+
// beside every regex. `roadmap-parser`'s bracket-fallback selector and
|
|
211
|
+
// `state`'s `isMilestoneBounded` both consume this.
|
|
212
|
+
//
|
|
213
|
+
// * CAPTURING, over any milestone, putting the milestone digits in a group.
|
|
214
|
+
// `validate`'s `checkBracketCoherence` consumes this.
|
|
215
|
+
//
|
|
216
|
+
// The milestone argument is expected to be a safe integer — every caller
|
|
217
|
+
// resolves it through `Number.isSafeInteger` first. A non-integer yields a
|
|
218
|
+
// regex source that is still well-formed and simply matches nothing, which is
|
|
219
|
+
// the same safe degrade the callers' own guards produce.
|
|
220
|
+
function bracketMilestoneIntroSrcFor(milestone) {
|
|
221
|
+
return `\\[${BRACKET_PROJECT_CODE_SRC}\\.${String(milestone).padStart(2, '0')}\\]`;
|
|
222
|
+
}
|
|
223
|
+
const BRACKET_MILESTONE_INTRO_CAPTURING_SRC = `\\[${BRACKET_PROJECT_CODE_SRC}\\.(${BRACKET_MILESTONE_NUMERIC_SRC})\\]`;
|
|
224
|
+
// Recognition is case-INSENSITIVE (every reader compiles `/i`), but the identity
|
|
225
|
+
// helpers this file owns — isSentinelPhaseId, getMilestoneFromPhaseId,
|
|
226
|
+
// bracketQualifiedKey — match `[A-Z]` case-SENSITIVELY. A lowercase bracket id
|
|
227
|
+
// captured by a reader and handed straight to them silently fails every identity
|
|
228
|
+
// test, which is how `### [gsd.999] 07:` leaked into phase counts as a real
|
|
229
|
+
// phase. Fold before any identity operation; never fold for display.
|
|
230
|
+
function foldBracketId(bracketId) {
|
|
231
|
+
return String(bracketId).toUpperCase();
|
|
232
|
+
}
|
|
233
|
+
// The identity recognizers every bracket helper in this file shares.
|
|
234
|
+
// `BRACKET_ID_PREFIX_RE` is applied to an ALREADY-FOLDED string, so it needs no
|
|
235
|
+
// `/i`; the other two see raw dir names and carry it.
|
|
236
|
+
// The milestone field must END at the phase separator. Without the boundary the
|
|
237
|
+
// width alternation matched a PREFIX of a malformed run — `GSD.002-01` matched
|
|
238
|
+
// its leading `00` and read as a sentinel.
|
|
239
|
+
const BRACKET_ID_PREFIX_RE = new RegExp(`^${BRACKET_PROJECT_CODE_SRC}\\.(${BRACKET_MILESTONE_NUMERIC_SRC})(?=-|$)`);
|
|
240
|
+
const BRACKET_DIR_PREFIX_SRC = `${BRACKET_ID_SRC}-`;
|
|
241
|
+
// The trailing `(?=-|$)` is what makes the recognizer and the resolver agree on
|
|
242
|
+
// REJECTED input, not just accepted input. Without it `GSD.02-12A-hotfix`
|
|
243
|
+
// resolves to token `12` here while the directory recognizer calls the name
|
|
244
|
+
// malformed — so W005 reports it malformed in the same run that the
|
|
245
|
+
// milestone-complete check treats it as a real phase directory.
|
|
246
|
+
const BRACKET_DIR_TOKEN_RE = new RegExp(`^${BRACKET_DIR_PREFIX_SRC}(\\d+(?:\\.\\d+)?)(?=-|$)`, 'i');
|
|
247
|
+
// Same width rule, same `(?=-|$)` boundary and same single-sub-phase shape as
|
|
248
|
+
// BRACKET_DIR_TOKEN_RE. Without them a qualified query `GSD.02-12` matched the
|
|
249
|
+
// directory `GSD.02-12A-hotfix` — which isPhaseDirName calls malformed — and
|
|
250
|
+
// phaseTokenMatches returns UNCONDITIONALLY on a qualified hit, so that
|
|
251
|
+
// disagreement would have been final rather than a fall-through.
|
|
252
|
+
const BRACKET_QUALIFIED_KEY_RE = new RegExp(`^(${BRACKET_PROJECT_CODE_SRC})\\.(${BRACKET_MILESTONE_NUMERIC_SRC})-(\\d+(?:\\.\\d+)?)(?=-|$)`, 'i');
|
|
253
|
+
// ── #612 PR-2: gated heading-intro selection ────────────────────────────────
|
|
254
|
+
// The two intro spellings that exist upstream TODAY, transcribed verbatim from
|
|
255
|
+
// the call sites. A repo that has not opted into the bracket convention
|
|
256
|
+
// compiles exactly these — not a superset of them, THEM — so its reads are
|
|
257
|
+
// structurally identical to the base build rather than argued equivalent.
|
|
258
|
+
// tests/adr-612-bracket-heading-selection.test.cjs asserts that byte-equality
|
|
259
|
+
// against its own independently transcribed copies of the call-site literals.
|
|
260
|
+
const BASE_ANY_BRACKET_HEADING_PREFIX_SRC = '(?:\\[[^\\]]{1,200}\\]\\s*)?Phase\\s+';
|
|
261
|
+
const BASE_PHASE_LABEL_PREFIX_SRC = 'Phase\\s+';
|
|
262
|
+
// Which of those two a site spells at base. Passed explicitly rather than
|
|
263
|
+
// inferred, because the choice is a fact about the call site's history that no
|
|
264
|
+
// amount of looking at the widened pattern can recover.
|
|
265
|
+
const PHASE_HEADING_BASELINE = Object.freeze({
|
|
266
|
+
/** Site already tolerates `[anything] Phase N` — roadmap headings, validate's heading scanner. */
|
|
267
|
+
ANY_BRACKET: 'any-bracket',
|
|
268
|
+
/** Site spells a bare `Phase N` with no bracket tolerance — checklist bullets, the counters. */
|
|
269
|
+
LABEL_ONLY: 'label-only',
|
|
270
|
+
});
|
|
271
|
+
/**
|
|
272
|
+
* The heading-intro source a site should compile, given the resolved
|
|
273
|
+
* `phase_id_convention`.
|
|
274
|
+
*
|
|
275
|
+
* NON-bracket conventions (null, undefined, 'milestone-prefixed', or any
|
|
276
|
+
* unrecognized value) return the site's BASE spelling unchanged. This is the
|
|
277
|
+
* whole design: PR-2 originally widened these reads ungated and argued the new
|
|
278
|
+
* shape "cannot occur in a legacy ROADMAP", which is false — `### [RFC.2119] 5:`,
|
|
279
|
+
* `### [v1.0] 2024:` and `### [ADR.612] 3:` are all legal legacy headings that
|
|
280
|
+
* the widened form claims as phases, moving phase_count, total_phases and W006
|
|
281
|
+
* on repos that never opted in. Selection at construction time removes the
|
|
282
|
+
* argument entirely: there is nothing to reason about, because a non-bracket
|
|
283
|
+
* repo compiles the same source string it compiled before.
|
|
284
|
+
*
|
|
285
|
+
* `capturing` adds EXACTLY ONE group, at position 1, holding the bracket id —
|
|
286
|
+
* `undefined` whenever a non-bracket alternative matched. Sites that filter
|
|
287
|
+
* sentinels need it: READING-B puts the sentinel milestone in the bracket, so
|
|
288
|
+
* testing the phase token alone is blind to `### [GSD.999] 01:`.
|
|
289
|
+
*
|
|
290
|
+
* Pure: takes the resolved convention, never reads config.
|
|
291
|
+
*/
|
|
292
|
+
function phaseHeadingPrefixSrcFor(baseline, convention, capturing = false) {
|
|
293
|
+
const base = baseline === PHASE_HEADING_BASELINE.ANY_BRACKET
|
|
294
|
+
? BASE_ANY_BRACKET_HEADING_PREFIX_SRC
|
|
295
|
+
: BASE_PHASE_LABEL_PREFIX_SRC;
|
|
296
|
+
if (convention !== 'bracket')
|
|
297
|
+
return base;
|
|
298
|
+
const id = capturing ? `(${BRACKET_ID_SRC})` : BRACKET_ID_SRC;
|
|
299
|
+
// `[ \t]*` not `\s*`: `\s` spans newlines, so a bracket-terminated heading
|
|
300
|
+
// followed by a blank line and a digit-leading prose line read as one phase.
|
|
301
|
+
// BOTH bracket forms are admitted at both baselines, and both CAPTURE. The
|
|
302
|
+
// any-bracket base already matches `[GSD.999] Phase 07:` on its own — but
|
|
303
|
+
// through the base alternative, which captures nothing, so the reader saw
|
|
304
|
+
// `bracketId === undefined`, fell back to the legacy leading-integer rule, and
|
|
305
|
+
// counted a labeled icebox heading as a real phase while the label-less form
|
|
306
|
+
// beside it was excluded. Two derivations of one ROADMAP disagreed. The
|
|
307
|
+
// bracket alternative is tried FIRST so it wins the capture.
|
|
308
|
+
const bracketAlt = `\\[${id}\\][ \\t]*(?:Phase\\s+|(?=\\d))`;
|
|
309
|
+
return `(?:${bracketAlt}|${base})`;
|
|
310
|
+
}
|
|
162
311
|
function stripProjectCodePrefix(value, caseInsensitive = true) {
|
|
163
312
|
const input = String(value);
|
|
164
313
|
const re = caseInsensitive ? PROJECT_CODE_PREFIX_STRIP_RE_I : PROJECT_CODE_PREFIX_STRIP_RE;
|
|
@@ -200,10 +349,10 @@ function getMilestoneFromPhaseId(phaseId, convention) {
|
|
|
200
349
|
// pure (no config read) and backward-compatible: every existing single-arg
|
|
201
350
|
// caller resolves to the unchanged READING-A body.
|
|
202
351
|
if (convention === 'bracket') {
|
|
203
|
-
const b =
|
|
352
|
+
const b = foldBracketId(phaseId).match(BRACKET_ID_PREFIX_RE);
|
|
204
353
|
if (!b)
|
|
205
354
|
return null;
|
|
206
|
-
const mm = parseInt(b[
|
|
355
|
+
const mm = parseInt(b[1], 10);
|
|
207
356
|
if (SENTINEL_RANGES.includes(mm))
|
|
208
357
|
return null; // sentinel milestones have no real milestone
|
|
209
358
|
return `v${mm}.0`;
|
|
@@ -383,9 +532,36 @@ function isSentinelPhaseId(phaseId, convention) {
|
|
|
383
532
|
// convention-less caller uses the legacy/bare leading-int rule below, so no
|
|
384
533
|
// existing reader gains a false positive; the bracket reading is opt-in.
|
|
385
534
|
if (convention === 'bracket') {
|
|
386
|
-
|
|
535
|
+
// #612 PR-2: fold before matching. Readers recognize headings under `/i`, so
|
|
536
|
+
// a lowercase `[gsd.999]` arrives verbatim; the case-sensitive class below
|
|
537
|
+
// then failed to match and an icebox item counted as a real phase.
|
|
538
|
+
const bracket = foldBracketId(s).match(BRACKET_ID_PREFIX_RE);
|
|
387
539
|
if (bracket)
|
|
388
540
|
return SENTINEL_RANGES.includes(parseInt(bracket[1], 10));
|
|
541
|
+
// #2761 round-12: NO bracket tag matched — a bare/untagged id under
|
|
542
|
+
// bracket convention (`0-bootstrap`, a directory with no `{CODE}.{MM}-`
|
|
543
|
+
// prefix, or a legacy-spelled `### Phase 0:` heading routed here without
|
|
544
|
+
// its own bracketId guard). Falling through to the LEGACY leading-int rule
|
|
545
|
+
// below would read this bare `0` as sentinel milestone 0 — but under
|
|
546
|
+
// bracket convention milestone 0 is expressed ONLY via an explicit
|
|
547
|
+
// bracket tag, so an untagged leading `0` is a real phase token, not a
|
|
548
|
+
// sentinel. This mirrors the two HEADING-side counters that already carry
|
|
549
|
+
// this exact carve-out: state.cts's `countRoadmapPhaseHeadings` guards its
|
|
550
|
+
// bare-0 exclusion with `bracketId &&` (so an untagged `Phase 0:` heading
|
|
551
|
+
// is counted), and roadmap-parser.cts's `scanMilestonePhaseIds` composes
|
|
552
|
+
// the bare-token rule as 999-only, deliberately NOT this predicate, for
|
|
553
|
+
// the identical reason (#3185: the leading-int rule also swallows the
|
|
554
|
+
// #2554 decimal ids — `0.5-bootstrap` is a real phase, not milestone 0).
|
|
555
|
+
// The 999/icebox reading stays universal either way — an untagged `999`
|
|
556
|
+
// is still backlog under every convention — so only that half of
|
|
557
|
+
// SENTINEL_RANGES applies here.
|
|
558
|
+
//
|
|
559
|
+
// `isSentinelPhaseDir` remains deliberately convention-blind for its
|
|
560
|
+
// warning-only disk guards and may conservatively classify a bare `0`
|
|
561
|
+
// directory as sentinel. This branch has an explicit resolved convention
|
|
562
|
+
// and feeds counts/archives, so it must retain the more precise reading.
|
|
563
|
+
const bare = stripProjectCodePrefix(s).match(/^0*(\d+)/);
|
|
564
|
+
return bare !== null && parseInt(bare[1], 10) === 999;
|
|
389
565
|
}
|
|
390
566
|
const legacy = stripProjectCodePrefix(s).match(/^0*(\d+)/); // legacy/bare: leading int
|
|
391
567
|
if (!legacy)
|
|
@@ -599,7 +775,10 @@ function extractPhaseToken(dirName, convention) {
|
|
|
599
775
|
// config read). The captured token is dot-only (`PP[.SS]`); the milestone↔phase
|
|
600
776
|
// hyphen and any trailing plan/slug are excluded.
|
|
601
777
|
if (convention === 'bracket') {
|
|
602
|
-
|
|
778
|
+
// #612 PR-2: built from the ONE bracket identity grammar, not a private
|
|
779
|
+
// spelling. Case-insensitive to match how the readers recognize headings and
|
|
780
|
+
// directories; the milestone width is the emit grammar's.
|
|
781
|
+
const bracketDir = dirName.match(BRACKET_DIR_TOKEN_RE);
|
|
603
782
|
if (bracketDir)
|
|
604
783
|
return bracketDir[1];
|
|
605
784
|
}
|
|
@@ -705,19 +884,42 @@ function extractPhaseToken(dirName, convention) {
|
|
|
705
884
|
* cannot resolve a genuinely different phase's artifact (`02.1-...` still
|
|
706
885
|
* fails every `01`-rooted candidate).
|
|
707
886
|
*
|
|
708
|
-
* BRACKET CONVENTION (review item 7): a
|
|
709
|
-
* (`P0.3-2-slug`) is string-INDISTINGUISHABLE
|
|
710
|
-
* (`extractPhaseToken` above, gated on
|
|
711
|
-
*
|
|
712
|
-
*
|
|
713
|
-
*
|
|
714
|
-
*
|
|
715
|
-
* (`
|
|
716
|
-
*
|
|
717
|
-
*
|
|
718
|
-
*
|
|
719
|
-
*
|
|
720
|
-
*
|
|
887
|
+
* BRACKET CONVENTION (review item 7; reworked by #612/#2761): a
|
|
888
|
+
* letter-prefixed-decimal dir (`P0.3-2-slug`) is string-INDISTINGUISHABLE
|
|
889
|
+
* from a bracket-dir token (`extractPhaseToken` above, gated on
|
|
890
|
+
* `convention === 'bracket'`) without an explicit convention signal. This
|
|
891
|
+
* predicate now takes the same optional trailing `convention` its sibling
|
|
892
|
+
* helpers do (ADR-2121 additive shape): when a call site threads
|
|
893
|
+
* `'bracket'` and the dir is a WELL-FORMED bracket dir
|
|
894
|
+
* (`BRACKET_DIR_TOKEN_RE` — the one grammar owner, shared with
|
|
895
|
+
* `extractPhaseToken`'s bracket branch), the dir's real phase token is
|
|
896
|
+
* readable and membership is scoped through the identical candidate
|
|
897
|
+
* comparison its legacy twin gets — so `GSD.02-02-two` reads exactly what
|
|
898
|
+
* `02-two` reads, which is PR-2's core invariant. Convention-less calls,
|
|
899
|
+
* bracket-MALFORMED names (1-digit milestone, letter-suffixed token — the
|
|
900
|
+
* shapes W005 reports), and legacy-shaped dirs inside a bracket repo all
|
|
901
|
+
* resolve to the unchanged body below.
|
|
902
|
+
*
|
|
903
|
+
* A filename carrying its own bracket-qualified stem is compared on the
|
|
904
|
+
* qualified key — see the branch body. An M-NN-stem artifact name
|
|
905
|
+
* (`02-01-VERIFICATION.md` in `GSD.02-01-one`) is excluded under the bracket
|
|
906
|
+
* convention, deliberately: that is exactly what the legacy twin does with
|
|
907
|
+
* the same file, twin-parity is this PR's contract, and migration-window
|
|
908
|
+
* artifact renaming belongs to the migrator slice per ADR-612 — the trade is
|
|
909
|
+
* pinned in tests/adr-612-bracket-phase-counting.test.cjs.
|
|
910
|
+
*
|
|
911
|
+
* For those unthreaded/unparseable cases the include-everything trade
|
|
912
|
+
* stands: rather than guess a reading it cannot know is active and risk
|
|
913
|
+
* excluding the phase's OWN artifact (the exact defect class this rework
|
|
914
|
+
* exists to fix), the ambiguous families widen to include-everything,
|
|
915
|
+
* accepted in trade for never dropping the phase's own report. Note the
|
|
916
|
+
* bracket family splits across BOTH fail-safes, not just one: a short
|
|
917
|
+
* digit-bearing project code (`A1.02-…`) lands in `firstLetterPrefixed`,
|
|
918
|
+
* but a 3+-letter code (`GSD.02-…`) derives ZERO token segments —
|
|
919
|
+
* `PROJECT_CODE_PREFIX_CAPTURE_RE_I` strips `{CODE}-`, not `{CODE}.` — so
|
|
920
|
+
* the zero-token fail-safe fires first. A fixer patching only the
|
|
921
|
+
* `firstLetterPrefixed` branch covers neither; the convention gate above
|
|
922
|
+
* covers both.
|
|
721
923
|
*
|
|
722
924
|
* FAIL-SAFE (#3511, unchanged): when dirName's own leading segment carries no
|
|
723
925
|
* phase-number token at all (`derivePhaseTokenSegments` finds zero segments —
|
|
@@ -754,7 +956,44 @@ function extractPhaseToken(dirName, convention) {
|
|
|
754
956
|
* the original pre-#3357 "alphabetically first of ALL dashed candidates"
|
|
755
957
|
* behavior, exactly as it always did for that directory shape.
|
|
756
958
|
*/
|
|
757
|
-
function isPhaseArtifact(fileName, phaseDirName) {
|
|
959
|
+
function isPhaseArtifact(fileName, phaseDirName, convention) {
|
|
960
|
+
// #612/#2761: the bracket branch — see "BRACKET CONVENTION" in the docblock.
|
|
961
|
+
// Gated on the same explicit signal AND the same grammar
|
|
962
|
+
// (`BRACKET_DIR_TOKEN_RE`) as `extractPhaseToken`'s bracket branch, so the
|
|
963
|
+
// two surfaces can never disagree about which names are bracket dirs. A
|
|
964
|
+
// name that fails the grammar falls through to the unchanged body below.
|
|
965
|
+
if (convention === 'bracket') {
|
|
966
|
+
const bracketDir = phaseDirName.match(BRACKET_DIR_TOKEN_RE);
|
|
967
|
+
if (bracketDir) {
|
|
968
|
+
// The convention must gate the FILENAME reading, not just the directory
|
|
969
|
+
// reading — a milestone-qualified artifact name (the layout the
|
|
970
|
+
// read-tolerance suite models) derives zero legacy token segments, so
|
|
971
|
+
// without this it enters FIX 2's token-less containment and any bracket
|
|
972
|
+
// dir admits it. Compared on the qualified key: the phase's own
|
|
973
|
+
// qualified artifact stays included — the exact key, and the dotted
|
|
974
|
+
// sub-phase continuation (`GSD.02-05.1-…` is `GSD.02-05`'s own file,
|
|
975
|
+
// the same dash-OR-dot rule matchesPhaseTokenCandidates applies; a
|
|
976
|
+
// dash-continuation never reaches this comparison, the phase group's
|
|
977
|
+
// `(?=-|$)` boundary already stopped the key before it) — while a
|
|
978
|
+
// WELL-FORMED qualified stem naming a different phase or milestone is
|
|
979
|
+
// excluded. A qualified-SHAPED but grammar-malformed stem (letter
|
|
980
|
+
// suffix, 1-digit milestone, 2-level dot — the W005 shapes) yields a
|
|
981
|
+
// null fileKey and keeps the module's documented include-everything
|
|
982
|
+
// fail-safe via FIX 2 below, same as every other undecidable name.
|
|
983
|
+
const fileKey = bracketQualifiedKey(fileName, convention);
|
|
984
|
+
if (fileKey !== null) {
|
|
985
|
+
const dirKey = bracketQualifiedKey(phaseDirName, convention);
|
|
986
|
+
if (dirKey !== null)
|
|
987
|
+
return fileKey === dirKey || fileKey.startsWith(`${dirKey}.`);
|
|
988
|
+
}
|
|
989
|
+
// Reached when the filename carries no well-formed qualified stem (the
|
|
990
|
+
// common case — every unqualified name), or when the DIR's own key is
|
|
991
|
+
// null despite matching the dir grammar (an unsafe-integer milestone or
|
|
992
|
+
// phase — bracketQualifiedKey refuses both rather than collide).
|
|
993
|
+
// Either way the candidate path decides, deliberately.
|
|
994
|
+
return matchesPhaseTokenCandidates(fileName, [bracketDir[1]]);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
758
997
|
const { tokenSegments, firstLetterPrefixed } = derivePhaseTokenSegments(phaseDirName);
|
|
759
998
|
if (tokenSegments.length === 0)
|
|
760
999
|
return true;
|
|
@@ -763,6 +1002,22 @@ function isPhaseArtifact(fileName, phaseDirName) {
|
|
|
763
1002
|
const strippedToken = strippedDir !== phaseDirName ? extractPhaseToken(strippedDir) : literalToken;
|
|
764
1003
|
const leadingRunMatch = strippedDir.match(LEADING_DIGIT_RUN_RE);
|
|
765
1004
|
const rawCandidates = [literalToken, strippedToken, leadingRunMatch?.[1]].filter((t) => Boolean(t));
|
|
1005
|
+
if (matchesPhaseTokenCandidates(fileName, rawCandidates))
|
|
1006
|
+
return true;
|
|
1007
|
+
// Bracket-convention ambiguity fail-safe — see docblock above.
|
|
1008
|
+
if (firstLetterPrefixed)
|
|
1009
|
+
return true;
|
|
1010
|
+
return false;
|
|
1011
|
+
}
|
|
1012
|
+
/**
|
|
1013
|
+
* Candidate-comparison core shared by `isPhaseArtifact`'s two entry paths —
|
|
1014
|
+
* the legacy segment-derived readings and the #612 bracket-dir token. One
|
|
1015
|
+
* comparison rule, not two that agree today and drift tomorrow: the padded/
|
|
1016
|
+
* de-padded expansion, the separator class, and FIX 2 apply identically
|
|
1017
|
+
* whichever path produced the candidates, which is what makes a bracket dir
|
|
1018
|
+
* read exactly what its legacy twin reads.
|
|
1019
|
+
*/
|
|
1020
|
+
function matchesPhaseTokenCandidates(fileName, rawCandidates) {
|
|
766
1021
|
// Each reading is compared in BOTH its padded and de-padded form: files are
|
|
767
1022
|
// written padded by `normalizePhaseName` (`cmdScaffold`) while directories
|
|
768
1023
|
// are often not (`1-unpadded`), and legacy trees carry the reverse pairing.
|
|
@@ -802,12 +1057,7 @@ function isPhaseArtifact(fileName, phaseDirName) {
|
|
|
802
1057
|
}
|
|
803
1058
|
// FIX 2: token-less filename (bare "VERIFICATION.md"/"UAT.md") — containment
|
|
804
1059
|
// in this phase's own directory listing is sufficient.
|
|
805
|
-
|
|
806
|
-
return true;
|
|
807
|
-
// Bracket-convention ambiguity fail-safe — see docblock above.
|
|
808
|
-
if (firstLetterPrefixed)
|
|
809
|
-
return true;
|
|
810
|
-
return false;
|
|
1060
|
+
return derivePhaseTokenSegments(fileName).tokenSegments.length === 0;
|
|
811
1061
|
}
|
|
812
1062
|
/**
|
|
813
1063
|
* #3511: scope `fileNames` to the subset that passes
|
|
@@ -856,14 +1106,66 @@ function isPhaseArtifact(fileName, phaseDirName) {
|
|
|
856
1106
|
* re-derived per site (CLAUDE.md's Generative Fix Divergence class).
|
|
857
1107
|
* `isPhaseArtifact` stays exported for single-item membership questions and
|
|
858
1108
|
* its own unit tests.
|
|
1109
|
+
*
|
|
1110
|
+
* #612/#2761: both helpers take the same optional trailing `convention` their
|
|
1111
|
+
* sibling primitives (`extractPhaseToken`, `phaseTokenMatches`) do — the
|
|
1112
|
+
* ADR-2121 additive shape, so every existing two-argument call resolves to
|
|
1113
|
+
* the unchanged legacy body. A call site that already holds the resolved
|
|
1114
|
+
* convention threads it and bracket dirs scope exactly like their legacy
|
|
1115
|
+
* twins; convention-less call sites keep the documented include-everything
|
|
1116
|
+
* fail-safes and are the follow-up-slice work (they cannot scope a bracket
|
|
1117
|
+
* dir until they can resolve the convention, and guessing is the one thing
|
|
1118
|
+
* this predicate must never do).
|
|
859
1119
|
*/
|
|
860
|
-
function scopeToPhase(fileNames, phaseDirName) {
|
|
861
|
-
return fileNames.filter((f) => isPhaseArtifact(f, phaseDirName));
|
|
1120
|
+
function scopeToPhase(fileNames, phaseDirName, convention) {
|
|
1121
|
+
return fileNames.filter((f) => isPhaseArtifact(f, phaseDirName, convention));
|
|
1122
|
+
}
|
|
1123
|
+
/**
|
|
1124
|
+
* Canonical comparable key for a milestone-qualified bracket id or dir name.
|
|
1125
|
+
* Lifts the milestone out of the `{CODE}.{MM}-` prefix so a flat multi-milestone
|
|
1126
|
+
* layout disambiguates: `CK.03-02` resolves to its OWN milestone's directory,
|
|
1127
|
+
* never the first same-numbered directory of another milestone.
|
|
1128
|
+
*
|
|
1129
|
+
* Returns null for UNQUALIFIED ids (`02`, `HQ-11`, `11.01`) so callers fall back
|
|
1130
|
+
* to bare-token matching unchanged, and GATED on convention === 'bracket' for
|
|
1131
|
+
* the same reason as extractPhaseToken: the qualified key is padding-
|
|
1132
|
+
* INSENSITIVE where the legacy token path is padding-SENSITIVE, so ungated it
|
|
1133
|
+
* silently widens matching on legacy repos.
|
|
1134
|
+
*/
|
|
1135
|
+
function bracketQualifiedKey(s, convention) {
|
|
1136
|
+
if (convention !== 'bracket')
|
|
1137
|
+
return null;
|
|
1138
|
+
const m = String(s).match(BRACKET_QUALIFIED_KEY_RE);
|
|
1139
|
+
if (!m)
|
|
1140
|
+
return null;
|
|
1141
|
+
const milestone = parseInt(m[2], 10);
|
|
1142
|
+
// A milestone integer past Number's exact range collapses to Infinity, and
|
|
1143
|
+
// every such id would then share one key. Refuse rather than collide.
|
|
1144
|
+
if (!Number.isSafeInteger(milestone))
|
|
1145
|
+
return null;
|
|
1146
|
+
const phase = m[3].split('.').map(n => parseInt(n, 10));
|
|
1147
|
+
if (phase.some(n => !Number.isSafeInteger(n)))
|
|
1148
|
+
return null;
|
|
1149
|
+
return `${foldBracketId(m[1])}.${milestone}-${phase.join('.')}`;
|
|
862
1150
|
}
|
|
863
1151
|
/**
|
|
864
1152
|
* Check if a directory name's phase token matches the normalized phase exactly.
|
|
1153
|
+
*
|
|
1154
|
+
* The optional `convention` is the ADR-2121 additive shape: every existing
|
|
1155
|
+
* two-argument call site resolves to the unchanged legacy body.
|
|
865
1156
|
*/
|
|
866
|
-
function phaseTokenMatches(dirName, normalized) {
|
|
1157
|
+
function phaseTokenMatches(dirName, normalized, convention) {
|
|
1158
|
+
if (convention === 'bracket') {
|
|
1159
|
+
// A milestone-qualified query compares on the full qualified key, and
|
|
1160
|
+
// returns unconditionally: falling through on a miss would re-admit the
|
|
1161
|
+
// cross-milestone match the qualification exists to prevent.
|
|
1162
|
+
const qKey = bracketQualifiedKey(normalized, convention);
|
|
1163
|
+
if (qKey)
|
|
1164
|
+
return bracketQualifiedKey(dirName, convention) === qKey;
|
|
1165
|
+
const bracketToken = extractPhaseToken(dirName, convention);
|
|
1166
|
+
if (bracketToken.toUpperCase() === normalized.toUpperCase())
|
|
1167
|
+
return true;
|
|
1168
|
+
}
|
|
867
1169
|
const token = extractPhaseToken(dirName);
|
|
868
1170
|
if (token.toUpperCase() === normalized.toUpperCase())
|
|
869
1171
|
return true;
|
|
@@ -960,8 +1262,8 @@ const unpad = (digits) => digits.replace(/^0+(?=\d)/, '');
|
|
|
960
1262
|
* the directory's leading digit run instead of `extractPhaseToken` (whose
|
|
961
1263
|
* token for these dirs is the mis-absorbed multi-segment form).
|
|
962
1264
|
*/
|
|
963
|
-
function matchPhaseDirs(dirs, normalized) {
|
|
964
|
-
const primary = dirs.filter(d => phaseTokenMatches(d, normalized));
|
|
1265
|
+
function matchPhaseDirs(dirs, normalized, convention) {
|
|
1266
|
+
const primary = dirs.filter(d => phaseTokenMatches(d, normalized, convention));
|
|
965
1267
|
if (primary.length > 0)
|
|
966
1268
|
return { matches: primary, usedBareFallback: false };
|
|
967
1269
|
const bare = String(normalized);
|
|
@@ -1021,9 +1323,18 @@ function phaseKeyFromToken(token) {
|
|
|
1021
1323
|
/**
|
|
1022
1324
|
* Canonical key for a phase DIRECTORY name (`"05-schedule-8"` → `"05"`,
|
|
1023
1325
|
* `"PROJ-5-x"` → `"05"`, `"30.1-follow-up"` → `"30.1"`).
|
|
1326
|
+
*
|
|
1327
|
+
* #612: `convention` is forwarded to `extractPhaseToken`, which needs that signal
|
|
1328
|
+
* to read a bracket directory (`"GSD.02-05-delta"` → `"05"`) — a bracket dir is
|
|
1329
|
+
* string-indistinguishable from the legacy letter-prefixed-decimal family, so the
|
|
1330
|
+
* extractor refuses to guess. Optional and defaulted-absent, so every pre-#612
|
|
1331
|
+
* call site resolves byte-identically to prior behaviour. Without it a bracket dir
|
|
1332
|
+
* yields its whole name as the key and never matches the ROADMAP entry it names —
|
|
1333
|
+
* #2562's own defect class reached from the other side: both sides of a comparison
|
|
1334
|
+
* must be derived not merely by the same function but under the same convention.
|
|
1024
1335
|
*/
|
|
1025
|
-
function phaseKeyFromDir(dirName) {
|
|
1026
|
-
return phaseKeyFromToken(extractPhaseToken(dirName));
|
|
1336
|
+
function phaseKeyFromDir(dirName, convention) {
|
|
1337
|
+
return phaseKeyFromToken(extractPhaseToken(dirName, convention));
|
|
1027
1338
|
}
|
|
1028
1339
|
/**
|
|
1029
1340
|
* Canonical key for a phase referenced in PROSE — a ROADMAP `## Progress` table
|
|
@@ -1156,6 +1467,63 @@ function isForeignPrefixedPhaseQuery(phase, projectCode) {
|
|
|
1156
1467
|
* form so a canonical heading (`### Phase 117:`) is preferred over a drifted
|
|
1157
1468
|
* prefixed one (`### Phase MANIFOLD-117:`) when both exist in one ROADMAP.
|
|
1158
1469
|
*/
|
|
1470
|
+
// Separator characters a branch-name template may plausibly use to join
|
|
1471
|
+
// `{slug}` to its neighbours — the four this repo's shipped templates and
|
|
1472
|
+
// docs/CONFIGURATION.md's custom-template examples use (`/`, `-`, `_`, `.`).
|
|
1473
|
+
const BRANCH_TEMPLATE_SEP_RE = /[-_./]/;
|
|
1474
|
+
const BRANCH_TEMPLATE_SEP_RUN_RE = /([-_./])\1+/g;
|
|
1475
|
+
const BRANCH_TEMPLATE_EDGE_SEP_RE = /^[-_./]+|[-_./]+$/g;
|
|
1476
|
+
/**
|
|
1477
|
+
* #4126: the ONE branch-name-template renderer for the `phase` branching
|
|
1478
|
+
* strategy, shared by `cmdCommit` (src/commands.cts) and
|
|
1479
|
+
* `cmdInitExecutePhase` (src/init.cts) so the two call sites cannot diverge
|
|
1480
|
+
* on how an undeliverable `phaseSlug` degrades (they previously each
|
|
1481
|
+
* independently substituted the literal word `phase`, producing a
|
|
1482
|
+
* non-identifying branch name like `gsd/phase-08-phase` that contradicts an
|
|
1483
|
+
* honestly-reported `phase_slug: null`).
|
|
1484
|
+
*
|
|
1485
|
+
* `{project}` is deliberately NOT handled here — init.cts substitutes it
|
|
1486
|
+
* separately, before calling this, because it is a config-level field with
|
|
1487
|
+
* its own fallback (`''`), not a phase-derived one.
|
|
1488
|
+
*
|
|
1489
|
+
* When `phaseSlug` is a non-empty string, `{slug}` substitutes normally
|
|
1490
|
+
* (unchanged from prior behavior). When it is empty or not a string (e.g.
|
|
1491
|
+
* `null`, `undefined`, a number), the `{slug}` token is DROPPED — along with
|
|
1492
|
+
* one adjacent separator character — rather than replaced with a placeholder
|
|
1493
|
+
* word: a dropped token keeps the branch name honest about what it does not
|
|
1494
|
+
* know, where a placeholder reads as a real (but wrong) name. The result is
|
|
1495
|
+
* always a syntactically plausible git-ref fragment: no leading, trailing, or
|
|
1496
|
+
* doubled separator, for both the shipped default template
|
|
1497
|
+
* (`gsd/phase-{phase}-{slug}`) and a differently-shaped custom one (this
|
|
1498
|
+
* field is user-configurable per docs/CONFIGURATION.md).
|
|
1499
|
+
*/
|
|
1500
|
+
function renderPhaseBranchName(template, phaseNumber, phaseSlug) {
|
|
1501
|
+
const withPhase = template.replace('{phase}', normalizePhaseName(phaseNumber));
|
|
1502
|
+
const slug = typeof phaseSlug === 'string' ? phaseSlug : '';
|
|
1503
|
+
if (slug)
|
|
1504
|
+
return withPhase.replace('{slug}', slug);
|
|
1505
|
+
const at = withPhase.indexOf('{slug}');
|
|
1506
|
+
if (at === -1)
|
|
1507
|
+
return withPhase;
|
|
1508
|
+
let before = withPhase.slice(0, at);
|
|
1509
|
+
let after = withPhase.slice(at + '{slug}'.length);
|
|
1510
|
+
// Drop exactly ONE adjacent separator — preferring the one immediately
|
|
1511
|
+
// before the token — so `a-{slug}` and `{slug}-a` both degrade to `a`
|
|
1512
|
+
// rather than leaving a dangling `a-` / `-a`.
|
|
1513
|
+
if (before && BRANCH_TEMPLATE_SEP_RE.test(before[before.length - 1])) {
|
|
1514
|
+
before = before.slice(0, -1);
|
|
1515
|
+
}
|
|
1516
|
+
else if (after && BRANCH_TEMPLATE_SEP_RE.test(after[0])) {
|
|
1517
|
+
after = after.slice(1);
|
|
1518
|
+
}
|
|
1519
|
+
// Collapse any doubled separator the drop can leave behind (e.g. a
|
|
1520
|
+
// `feature//{slug}` template dropping to `feature/`→`feature`), then trim
|
|
1521
|
+
// a resulting leading/trailing separator — valid for the shipped default
|
|
1522
|
+
// template AND any user-configured shape, not just the one this repo ships.
|
|
1523
|
+
const joined = before + after;
|
|
1524
|
+
const collapsed = joined.replace(BRANCH_TEMPLATE_SEP_RUN_RE, '$1').replace(BRANCH_TEMPLATE_EDGE_SEP_RE, '');
|
|
1525
|
+
return collapsed || null;
|
|
1526
|
+
}
|
|
1159
1527
|
function roadmapPhaseLookupSources(phaseNum) {
|
|
1160
1528
|
const sources = [];
|
|
1161
1529
|
const exactSource = phaseMarkdownRegexSourceExact(phaseNum);
|
|
@@ -1176,6 +1544,18 @@ module.exports = {
|
|
|
1176
1544
|
isPhaseContinuationSegment,
|
|
1177
1545
|
BRACKET_PHASE_TOKEN_SOURCE,
|
|
1178
1546
|
PHASE_HEADING_PREFIX_SRC,
|
|
1547
|
+
BRACKET_ID_SRC,
|
|
1548
|
+
BRACKET_PROJECT_CODE_SRC,
|
|
1549
|
+
bracketMilestoneIntroSrcFor,
|
|
1550
|
+
BRACKET_MILESTONE_INTRO_CAPTURING_SRC,
|
|
1551
|
+
BRACKET_MILESTONE_NUMERIC_SRC,
|
|
1552
|
+
BRACKET_DIR_PREFIX_SRC,
|
|
1553
|
+
BASE_ANY_BRACKET_HEADING_PREFIX_SRC,
|
|
1554
|
+
BASE_PHASE_LABEL_PREFIX_SRC,
|
|
1555
|
+
PHASE_HEADING_BASELINE,
|
|
1556
|
+
phaseHeadingPrefixSrcFor,
|
|
1557
|
+
foldBracketId,
|
|
1558
|
+
bracketQualifiedKey,
|
|
1179
1559
|
stripProjectCodePrefix,
|
|
1180
1560
|
normalizePhaseName,
|
|
1181
1561
|
getMilestoneFromPhaseId,
|
|
@@ -1203,4 +1583,5 @@ module.exports = {
|
|
|
1203
1583
|
stripConfiguredProjectCodePrefix,
|
|
1204
1584
|
isForeignPrefixedPhaseQuery,
|
|
1205
1585
|
roadmapPhaseLookupSources,
|
|
1586
|
+
renderPhaseBranchName,
|
|
1206
1587
|
};
|
|
@@ -25,6 +25,8 @@ exports.locateProgressTable = locateProgressTable;
|
|
|
25
25
|
exports.deriveProgressFromRoadmap = deriveProgressFromRoadmap;
|
|
26
26
|
exports.clampPercentFromFraction = clampPercentFromFraction;
|
|
27
27
|
exports.clampPercent = clampPercent;
|
|
28
|
+
exports.progressBarFilledCells = progressBarFilledCells;
|
|
29
|
+
exports.renderProgressBar = renderProgressBar;
|
|
28
30
|
const markdown_table_cjs_1 = require("./markdown-table.cjs");
|
|
29
31
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-id.cjs is an export= CommonJS module
|
|
30
32
|
const phaseIdMod = require("./phase-id.cjs");
|
|
@@ -140,3 +142,62 @@ function clampPercent(completed, total) {
|
|
|
140
142
|
return 0;
|
|
141
143
|
return clampPercentFromFraction(completed / total);
|
|
142
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* How many cells of a `width`-cell progress bar are filled for `percent`.
|
|
147
|
+
*
|
|
148
|
+
* #4294: the RENDER half's kernel — the counterpart of `clampPercentFromFraction`
|
|
149
|
+
* one layer down. That function owns `fraction -> integer percent`; this one
|
|
150
|
+
* owns `integer percent -> filled cells`, and is the single place that rounding
|
|
151
|
+
* is expressed. Five inline copies of the rounding carried it before this
|
|
152
|
+
* change — three in `commands.cts`, and one each in `gsd2-import.cts` and
|
|
153
|
+
* `formatProgressMachineSegment` here; the latter two used `/ 10` with the
|
|
154
|
+
* width already substituted (`pct` in `gsd2-import.cts`, `clamped` in the
|
|
155
|
+
* formatter). (#4294 counts SIX call sites because it counts
|
|
156
|
+
* `cmdStateUpdateProgress` and `syncCore` separately; #4231 had already routed
|
|
157
|
+
* both through `formatProgressMachineSegment` — as it does
|
|
158
|
+
* `applyPostSyncPreservation` — so by this branch's base they share one copy.)
|
|
159
|
+
* Every copy saturated: at width 10 that rounds to a full bar from 95 up, at
|
|
160
|
+
* width 20 from 98 up, so a project at 19/20 plans drew the same bar as a
|
|
161
|
+
* shipped one beside a number that said otherwise.
|
|
162
|
+
*
|
|
163
|
+
* Contract:
|
|
164
|
+
* - A FULL bar is reserved for an actual 100. Below 100 the fill is held one
|
|
165
|
+
* cell short of the width. This is the only departure from the old formula:
|
|
166
|
+
* at width 10 exactly 95-99 move (10 -> 9), at width 20 exactly 98-99
|
|
167
|
+
* (20 -> 19); every other percent in 0-100 rounds as before.
|
|
168
|
+
* - `null` / `undefined` / non-finite renders an empty bar, matching the
|
|
169
|
+
* `percent === null ? 0 : ...` guard the `progress` renderers already carried.
|
|
170
|
+
* - Out-of-range input is clamped to 0-100 before rounding, so the count is
|
|
171
|
+
* always within `[0, width]` and a `'░'.repeat(width - filled)` can never
|
|
172
|
+
* be handed a negative count (the old inline form threw `RangeError` at
|
|
173
|
+
* 120%). A non-positive width yields 0.
|
|
174
|
+
*
|
|
175
|
+
* Callers wanting the glyph run call `renderProgressBar`; this is exported so
|
|
176
|
+
* the rounding rule can be pinned directly against the legacy curve.
|
|
177
|
+
*/
|
|
178
|
+
function progressBarFilledCells(percent, width) {
|
|
179
|
+
const cells = Number.isFinite(width) && width > 0 ? Math.floor(width) : 0;
|
|
180
|
+
if (cells === 0)
|
|
181
|
+
return 0;
|
|
182
|
+
if (typeof percent !== 'number' || !Number.isFinite(percent))
|
|
183
|
+
return 0;
|
|
184
|
+
const clamped = Math.max(0, Math.min(100, percent));
|
|
185
|
+
if (clamped >= 100)
|
|
186
|
+
return cells;
|
|
187
|
+
// Scale by the WIDTH, not by 100 — this is cells-from-percent, not the
|
|
188
|
+
// completion-ratio derivation lint-completion-ratio-drift.cjs guards.
|
|
189
|
+
const rounded = Math.round((clamped / 100) * cells);
|
|
190
|
+
return Math.min(rounded, cells - 1);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Render the glyph run of a `width`-cell progress bar for `percent` —
|
|
194
|
+
* `'█'` for each filled cell, `'░'` for the rest, always exactly `width`
|
|
195
|
+
* glyphs (an empty string for a non-positive width). Brackets, the printed
|
|
196
|
+
* percent and any suffix stay with the caller; the fill rule is
|
|
197
|
+
* `progressBarFilledCells` (#4294).
|
|
198
|
+
*/
|
|
199
|
+
function renderProgressBar(percent, width) {
|
|
200
|
+
const filled = progressBarFilledCells(percent, width);
|
|
201
|
+
const cells = Number.isFinite(width) && width > 0 ? Math.floor(width) : 0;
|
|
202
|
+
return '█'.repeat(filled) + '░'.repeat(cells - filled);
|
|
203
|
+
}
|