@opengsd/gsd-core 1.14.0 → 1.16.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 +85 -5
- package/README.ja-JP.md +3 -3
- package/README.ko-KR.md +3 -3
- package/README.pt-BR.md +3 -3
- package/README.zh-CN.md +3 -3
- package/agents/gsd-code-fixer.compact.md +7 -6
- package/agents/gsd-code-fixer.md +9 -8
- package/agents/gsd-code-reviewer.compact.md +5 -3
- package/agents/gsd-code-reviewer.md +8 -6
- package/agents/gsd-debug-session-manager.compact.md +17 -2
- package/agents/gsd-debug-session-manager.md +17 -2
- package/agents/gsd-debugger.md +3 -3
- package/agents/gsd-eval-auditor.compact.md +1 -1
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +17 -12
- package/agents/gsd-intel-updater.compact.md +1 -1
- package/agents/gsd-intel-updater.md +1 -1
- package/agents/gsd-mempalace-curator.md +2 -2
- package/agents/gsd-phase-researcher.md +19 -11
- package/agents/gsd-plan-checker.md +15 -9
- package/agents/gsd-planner.md +15 -11
- package/agents/gsd-project-researcher.compact.md +1 -1
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.compact.md +1 -1
- package/agents/gsd-research-synthesizer.md +1 -1
- package/agents/gsd-ui-auditor.compact.md +21 -30
- package/agents/gsd-ui-auditor.md +166 -37
- package/agents/gsd-ui-researcher.compact.md +1 -1
- package/agents/gsd-ui-researcher.md +1 -1
- package/agents/gsd-verifier.md +37 -14
- package/bin/install.js +764 -287
- package/commands/gsd/add-tests.md +6 -1
- package/commands/gsd/ai-integration-phase.md +6 -1
- package/commands/gsd/audit-fix.md +5 -0
- package/commands/gsd/audit-milestone.md +6 -1
- package/commands/gsd/autonomous.md +7 -2
- package/commands/gsd/capture.md +9 -5
- package/commands/gsd/code-review.md +7 -2
- package/commands/gsd/complete-milestone.md +4 -0
- package/commands/gsd/config.md +7 -3
- package/commands/gsd/debug.md +11 -7
- package/commands/gsd/discuss-phase.md +7 -3
- package/commands/gsd/docs-update.md +12 -7
- package/commands/gsd/eval-review.md +6 -1
- package/commands/gsd/execute-phase.md +12 -7
- package/commands/gsd/extract-learnings.md +5 -0
- package/commands/gsd/fast.md +4 -0
- package/commands/gsd/forensics.md +5 -1
- package/commands/gsd/graphify.md +10 -6
- package/commands/gsd/health.md +5 -0
- package/commands/gsd/help.md +7 -2
- package/commands/gsd/import.md +7 -3
- package/commands/gsd/inbox.md +5 -0
- package/commands/gsd/ingest-docs.md +5 -1
- package/commands/gsd/manager.md +6 -1
- package/commands/gsd/map-codebase.md +7 -3
- package/commands/gsd/mempalace-capture.md +12 -4
- package/commands/gsd/mempalace-recall.md +5 -1
- package/commands/gsd/milestone-summary.md +5 -1
- package/commands/gsd/mvp-phase.md +8 -3
- package/commands/gsd/new-milestone.md +6 -1
- package/commands/gsd/new-project.md +5 -0
- package/commands/gsd/next.md +6 -1
- package/commands/gsd/ns-context.md +4 -0
- package/commands/gsd/ns-ideate.md +4 -0
- package/commands/gsd/ns-manage.md +4 -0
- package/commands/gsd/ns-project.md +4 -0
- package/commands/gsd/ns-review.md +4 -0
- package/commands/gsd/ns-workflow.md +4 -0
- package/commands/gsd/onboard.md +6 -1
- package/commands/gsd/pause-work.md +5 -1
- package/commands/gsd/phase.md +8 -4
- package/commands/gsd/plan-phase.md +6 -1
- package/commands/gsd/plan-review-convergence.md +11 -7
- package/commands/gsd/pr-branch.md +4 -0
- package/commands/gsd/profile-user.md +5 -1
- package/commands/gsd/progress.md +7 -2
- package/commands/gsd/quick-batch.md +21 -9
- package/commands/gsd/quick.md +12 -7
- package/commands/gsd/review.md +7 -4
- package/commands/gsd/secure-phase.md +6 -1
- package/commands/gsd/ship.md +5 -0
- package/commands/gsd/sketch.md +7 -2
- package/commands/gsd/spec-phase.md +5 -1
- package/commands/gsd/spike.md +8 -3
- package/commands/gsd/surface.md +5 -1
- package/commands/gsd/thread.md +4 -0
- package/commands/gsd/ui-phase.md +6 -1
- package/commands/gsd/ui-review.md +6 -1
- package/commands/gsd/ultraplan-phase.md +5 -1
- package/commands/gsd/undo.md +5 -1
- package/commands/gsd/update.md +6 -2
- package/commands/gsd/validate-phase.md +6 -1
- package/commands/gsd/verify-work.md +6 -1
- package/commands/gsd/workspace.md +7 -3
- package/gsd-core/bin/gsd-tools.cjs +477 -78
- package/gsd-core/bin/lib/active-workstream-store.cjs +15 -0
- package/gsd-core/bin/lib/adr-parser.cjs +3 -1
- package/gsd-core/bin/lib/agent-install-check.cjs +4 -1
- package/gsd-core/bin/lib/audit.cjs +144 -42
- package/gsd-core/bin/lib/broken-windows.cjs +13 -13
- package/gsd-core/bin/lib/capability-activation.cjs +9 -4
- package/gsd-core/bin/lib/capability-registry.cjs +197 -222
- package/gsd-core/bin/lib/capability-validator.cjs +16 -1
- package/gsd-core/bin/lib/check-auto-mode.cjs +35 -0
- package/gsd-core/bin/lib/check-command-router.cjs +164 -1625
- package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +13 -2
- package/gsd-core/bin/lib/cli-exit.cjs +12 -0
- package/gsd-core/bin/lib/codex-agent-toml.cjs +32 -33
- package/gsd-core/bin/lib/command-aliases.cjs +7 -0
- package/gsd-core/bin/lib/command-routing-hub.cjs +48 -1
- package/gsd-core/bin/lib/commands.cjs +343 -207
- package/gsd-core/bin/lib/complexity-trigger.cjs +8 -7
- package/gsd-core/bin/lib/config-loader.cjs +65 -4
- package/gsd-core/bin/lib/config.cjs +76 -19
- package/gsd-core/bin/lib/core-utils.cjs +6 -1
- package/gsd-core/bin/lib/coverage.cjs +4 -8
- package/gsd-core/bin/lib/decision-coverage-support.cjs +259 -0
- package/gsd-core/bin/lib/decisions.cjs +30 -14
- package/gsd-core/bin/lib/drift.cjs +177 -42
- package/gsd-core/bin/lib/frontmatter-fence.cjs +90 -0
- package/gsd-core/bin/lib/frontmatter-splice.cjs +494 -0
- package/gsd-core/bin/lib/frontmatter.cjs +426 -234
- package/gsd-core/bin/lib/gap-checker.cjs +72 -29
- package/gsd-core/bin/lib/gate-api-coverage-verify-pre.cjs +381 -0
- package/gsd-core/bin/lib/gate-args.cjs +53 -0
- package/gsd-core/bin/lib/gate-codebase-drift.cjs +285 -0
- package/gsd-core/bin/lib/gate-config.cjs +46 -0
- package/gsd-core/bin/lib/gate-context-drift.cjs +141 -0
- package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +169 -0
- package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +126 -0
- package/gsd-core/bin/lib/gate-evaluation-scope.cjs +555 -0
- package/gsd-core/bin/lib/gate-evidence.cjs +138 -0
- package/gsd-core/bin/lib/gate-exit.cjs +27 -0
- package/gsd-core/bin/lib/gate-gap-analysis-plan-post.cjs +61 -0
- package/gsd-core/bin/lib/gate-phase-context.cjs +170 -0
- package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +1 -1
- package/gsd-core/bin/lib/gate-predicate.cjs +165 -0
- package/gsd-core/bin/lib/gate-prohibition-enforcement.cjs +94 -0
- package/gsd-core/bin/lib/gate-schema-drift.cjs +165 -0
- package/gsd-core/bin/lib/gate-tdd-red-evidence.cjs +100 -0
- package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +182 -0
- package/gsd-core/bin/lib/gate-ui-plan.cjs +86 -0
- package/gsd-core/bin/lib/gate-ui-safety.cjs +80 -0
- package/gsd-core/bin/lib/gate-verdict.cjs +64 -0
- package/gsd-core/bin/lib/gate-verify-command-paths.cjs +78 -0
- package/gsd-core/bin/lib/gate-verify-failure-directions.cjs +41 -0
- package/gsd-core/bin/lib/graphify.cjs +10 -2
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +43 -47
- package/gsd-core/bin/lib/health-diagnostic.cjs +45 -8
- package/gsd-core/bin/lib/host-runtime-detection.cjs +9 -0
- package/gsd-core/bin/lib/init.cjs +364 -132
- package/gsd-core/bin/lib/install-engine.cjs +30 -33
- package/gsd-core/bin/lib/install-profiles.cjs +7 -4
- package/gsd-core/bin/lib/installer-migrations.cjs +8 -1
- package/gsd-core/bin/lib/io.cjs +122 -3
- package/gsd-core/bin/lib/loop-resolver.cjs +95 -0
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +75 -1
- package/gsd-core/bin/lib/milestone.cjs +37 -6
- package/gsd-core/bin/lib/model-resolver.cjs +171 -62
- package/gsd-core/bin/lib/observability/event.cjs +1 -1
- package/gsd-core/bin/lib/observability/logger.cjs +46 -1
- package/gsd-core/bin/lib/pattern.cjs +10 -0
- package/gsd-core/bin/lib/phase-command-router.cjs +20 -5
- package/gsd-core/bin/lib/phase-estimation.cjs +5 -4
- package/gsd-core/bin/lib/phase-id-card.cjs +32 -0
- package/gsd-core/bin/lib/phase-id-display.cjs +78 -0
- package/gsd-core/bin/lib/phase-id.cjs +110 -8
- package/gsd-core/bin/lib/phase-lifecycle.cjs +9 -2
- package/gsd-core/bin/lib/phase-locator.cjs +29 -10
- package/gsd-core/bin/lib/phase-status.cjs +360 -0
- package/gsd-core/bin/lib/phase.cjs +489 -88
- package/gsd-core/bin/lib/plan-document.cjs +142 -20
- package/gsd-core/bin/lib/plan-drift-guard.cjs +5 -0
- package/gsd-core/bin/lib/planning-document.cjs +692 -0
- package/gsd-core/bin/lib/planning-inspect.cjs +60 -9
- package/gsd-core/bin/lib/planning-snapshot.cjs +18 -0
- package/gsd-core/bin/lib/planning-workspace.cjs +83 -55
- package/gsd-core/bin/lib/pr-branch-patterns.cjs +57 -0
- package/gsd-core/bin/lib/pristine-baseline.cjs +10 -0
- package/gsd-core/bin/lib/probe-core.cjs +7 -1
- package/gsd-core/bin/lib/profile-output.cjs +6 -3
- package/gsd-core/bin/lib/prohibition-enforcement.cjs +0 -55
- package/gsd-core/bin/lib/project-root.cjs +41 -2
- package/gsd-core/bin/lib/quick-batch-command-router.cjs +35 -9
- package/gsd-core/bin/lib/quick-batch-dispatch.cjs +11 -8
- package/gsd-core/bin/lib/real-home-guard.cjs +9 -1
- package/gsd-core/bin/lib/report-parser.cjs +269 -0
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +10 -30
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +2 -2
- package/gsd-core/bin/lib/roadmap-command-router.cjs +25 -19
- package/gsd-core/bin/lib/roadmap-parser.cjs +242 -15
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +1653 -65
- package/gsd-core/bin/lib/roadmap.cjs +405 -88
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +373 -187
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +5 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +57 -1
- package/gsd-core/bin/lib/runtime-homes.cjs +14 -7
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +522 -624
- package/gsd-core/bin/lib/runtime-name-policy.cjs +246 -21
- package/gsd-core/bin/lib/runtime-slash.cjs +47 -30
- package/gsd-core/bin/lib/shell-command-projection.cjs +47 -8
- package/gsd-core/bin/lib/smart-entry.cjs +19 -3
- package/gsd-core/bin/lib/stale-bake-guard.cjs +32 -48
- package/gsd-core/bin/lib/state-contract.cjs +15 -18
- package/gsd-core/bin/lib/state-document.cjs +100 -22
- package/gsd-core/bin/lib/state-transition.cjs +39 -2
- package/gsd-core/bin/lib/state.cjs +256 -105
- package/gsd-core/bin/lib/surface.cjs +19 -2
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +48 -79
- package/gsd-core/bin/lib/uat-predicate.cjs +359 -38
- package/gsd-core/bin/lib/uat.cjs +432 -7
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +15 -2
- package/gsd-core/bin/lib/ui-frontend-evidence.cjs +167 -40
- package/gsd-core/bin/lib/undo-commit-selection.cjs +131 -0
- package/gsd-core/bin/lib/vendor/README.md +31 -9
- package/gsd-core/bin/lib/vendor/saxes.cjs +1934 -0
- package/gsd-core/bin/lib/vendor/saxes.cjs.LICENSE.txt +92 -0
- package/gsd-core/bin/lib/vendor/tap-parser.cjs +8927 -0
- package/gsd-core/bin/lib/vendor/tap-parser.cjs.LICENSE.txt +152 -0
- package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
- package/gsd-core/bin/lib/verification.cjs +1378 -274
- package/gsd-core/bin/lib/verify-command-grounding.cjs +46 -2
- package/gsd-core/bin/lib/verify-command-router.cjs +18 -7
- package/gsd-core/bin/lib/verify.cjs +357 -531
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +14 -6
- package/gsd-core/bin/lib/workstream-inventory.cjs +31 -21
- package/gsd-core/bin/lib/workstream-name-policy.cjs +31 -1
- package/gsd-core/bin/lib/workstream.cjs +11 -2
- package/gsd-core/bin/lib/worktree-base-ref.cjs +482 -73
- package/gsd-core/bin/lib/worktree-safety.cjs +784 -51
- package/gsd-core/bin/shared/config-defaults.manifest.json +8 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +5 -0
- package/gsd-core/references/autonomous-smart-discuss.md +2 -1
- package/gsd-core/references/autonomous-ui-design-contract.md +3 -3
- package/gsd-core/references/checkpoints.md +5 -3
- package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe.md +195 -21
- package/gsd-core/references/execute-mvp-tdd.md +5 -10
- package/gsd-core/references/execute-phase-between-wave-reset.md +10 -6
- package/gsd-core/references/execute-phase-response-language.md +1 -1
- package/gsd-core/references/execute-phase-wave-guard.md +22 -11
- package/gsd-core/references/gsd-run-resolver.md +1 -1
- package/gsd-core/references/loop-hook-dispatch.md +7 -1
- package/gsd-core/references/model-profiles.md +1 -1
- package/gsd-core/references/offer-next.md +1 -1
- package/gsd-core/references/phase-argument-parsing.md +9 -7
- package/gsd-core/references/phase-id-convention.md +28 -0
- package/gsd-core/references/planner-gap-closure.md +2 -0
- package/gsd-core/references/planner-load-graph-context.md +24 -13
- package/gsd-core/references/planner-verify-command-grounding.md +14 -0
- package/gsd-core/references/planning-config.md +12 -3
- package/gsd-core/references/spidr-splitting.md +1 -1
- package/gsd-core/references/tdd.md +37 -8
- package/gsd-core/references/ui-consideration-probe.md +10 -5
- package/gsd-core/references/verifier-phase-gates.md +5 -2
- package/gsd-core/references/verify-command-path-resolvability.md +10 -2
- package/gsd-core/references/verify-mvp-mode.md +2 -2
- package/gsd-core/references/workstream-flag.md +33 -3
- package/gsd-core/references/worktree-path-safety.md +321 -0
- package/gsd-core/templates/README.md +1 -1
- package/gsd-core/templates/UAT.md +17 -1
- package/gsd-core/templates/config.json +2 -11
- package/gsd-core/templates/verification-report.md +1 -1
- package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/gsd-core/workflows/add-backlog.md +1 -1
- package/gsd-core/workflows/add-phase.md +8 -7
- package/gsd-core/workflows/add-tests.md +4 -3
- package/gsd-core/workflows/add-todo.md +6 -5
- package/gsd-core/workflows/ai-integration-phase.md +13 -4
- package/gsd-core/workflows/audit-fix.md +1 -1
- package/gsd-core/workflows/audit-milestone.md +4 -3
- package/gsd-core/workflows/audit-uat.md +1 -1
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +9 -18
- package/gsd-core/workflows/autonomous.md +43 -23
- package/gsd-core/workflows/check-todos.md +7 -6
- package/gsd-core/workflows/cleanup.md +2 -2
- package/gsd-core/workflows/code-review/steps/dispatch-fix.md +4 -3
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +20 -13
- package/gsd-core/workflows/code-review-fix.md +112 -25
- package/gsd-core/workflows/code-review.md +146 -135
- package/gsd-core/workflows/complete-milestone/detail/elaboration.md +4 -3
- package/gsd-core/workflows/complete-milestone.md +13 -8
- package/gsd-core/workflows/debug.md +32 -7
- package/gsd-core/workflows/diagnose-issues.md +3 -2
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +4 -3
- package/gsd-core/workflows/discuss-phase.md +4 -3
- package/gsd-core/workflows/do.md +2 -2
- package/gsd-core/workflows/docs-update.md +6 -5
- package/gsd-core/workflows/edit-phase.md +4 -3
- package/gsd-core/workflows/eval-review.md +14 -5
- package/gsd-core/workflows/execute-phase/detail/elaboration.md +2 -2
- package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +1019 -0
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +15 -4
- package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +10 -7
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +37 -3
- package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +3 -1
- package/gsd-core/workflows/execute-phase/steps/partial-wave.md +2 -2
- package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +46 -9
- package/gsd-core/workflows/execute-phase/steps/protected-branch.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/ready-wave-gate.md +37 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +2 -0
- package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/threat-id-gate.md +28 -0
- package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +187 -0
- package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +2 -3
- package/gsd-core/workflows/execute-phase/steps/worktree-base-check.md +25 -0
- package/gsd-core/workflows/execute-phase.md +96 -178
- package/gsd-core/workflows/execute-plan.md +18 -23
- package/gsd-core/workflows/explore.md +4 -4
- package/gsd-core/workflows/extract-learnings.md +4 -2
- package/gsd-core/workflows/fast.md +1 -1
- package/gsd-core/workflows/forensics.md +1 -1
- package/gsd-core/workflows/graduation.md +1 -1
- package/gsd-core/workflows/health.md +3 -2
- package/gsd-core/workflows/help/modes/full.compact.md +3 -3
- package/gsd-core/workflows/help/modes/full.md +5 -5
- package/gsd-core/workflows/help/modes/topic.md +15 -5
- package/gsd-core/workflows/import.md +4 -3
- package/gsd-core/workflows/inbox.md +2 -2
- package/gsd-core/workflows/ingest-docs.md +3 -3
- package/gsd-core/workflows/insert-phase.md +4 -3
- package/gsd-core/workflows/list-seeds.md +1 -1
- package/gsd-core/workflows/list-workspaces.md +1 -1
- package/gsd-core/workflows/manager.md +6 -4
- package/gsd-core/workflows/map-codebase.md +5 -4
- package/gsd-core/workflows/milestone-summary.md +3 -2
- package/gsd-core/workflows/mvp-phase.md +14 -14
- package/gsd-core/workflows/new-milestone.md +11 -11
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +3 -3
- package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +1 -1
- package/gsd-core/workflows/new-project.md +7 -7
- package/gsd-core/workflows/new-workspace.md +2 -2
- package/gsd-core/workflows/next.md +1 -1
- package/gsd-core/workflows/note.md +1 -1
- package/gsd-core/workflows/onboard.md +1 -1
- package/gsd-core/workflows/pause-work.md +2 -2
- package/gsd-core/workflows/plan-phase/detail/elaboration.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +17 -5
- package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +23 -5
- package/gsd-core/workflows/plan-phase.md +50 -24
- package/gsd-core/workflows/plan-review-convergence.md +21 -5
- package/gsd-core/workflows/plant-seed.md +62 -20
- package/gsd-core/workflows/pr-branch.md +113 -13
- package/gsd-core/workflows/profile-user.md +2 -2
- package/gsd-core/workflows/progress.md +19 -49
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +27 -0
- package/gsd-core/workflows/quick/steps/quick-verification.md +4 -4
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +30 -11
- package/gsd-core/workflows/quick-batch/steps/batch-init.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/completion.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/merge-wave.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/planner-wave.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/research-phase.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/resume-mode.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/verification-wave.md +9 -3
- package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +1 -1
- package/gsd-core/workflows/quick-batch.md +15 -10
- package/gsd-core/workflows/quick.md +62 -39
- package/gsd-core/workflows/reapply-patches.md +9 -3
- package/gsd-core/workflows/remove-phase.md +3 -2
- package/gsd-core/workflows/remove-workspace.md +2 -2
- package/gsd-core/workflows/resume-project.md +3 -2
- package/gsd-core/workflows/review.md +33 -17
- package/gsd-core/workflows/scan.md +3 -2
- package/gsd-core/workflows/secure-phase.md +13 -13
- package/gsd-core/workflows/settings-advanced.md +30 -10
- package/gsd-core/workflows/settings-integrations.md +2 -3
- package/gsd-core/workflows/settings.md +4 -4
- package/gsd-core/workflows/ship.md +14 -13
- package/gsd-core/workflows/sketch-wrap-up.md +1 -1
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/smart-entry.md +2 -2
- package/gsd-core/workflows/spec-phase.md +15 -5
- package/gsd-core/workflows/spike-wrap-up.md +1 -1
- package/gsd-core/workflows/spike.md +1 -1
- package/gsd-core/workflows/stats.md +1 -1
- package/gsd-core/workflows/sync-skills.md +5 -5
- package/gsd-core/workflows/thread.md +2 -2
- package/gsd-core/workflows/transition.md +13 -23
- package/gsd-core/workflows/ui-phase.md +48 -11
- package/gsd-core/workflows/ui-review.md +21 -6
- package/gsd-core/workflows/ultraplan-phase.md +3 -2
- package/gsd-core/workflows/undo.md +339 -20
- package/gsd-core/workflows/update.md +7 -7
- package/gsd-core/workflows/validate-phase.md +12 -13
- package/gsd-core/workflows/verify-work/detail/elaboration.md +43 -3
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +1 -1
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +5 -3
- package/gsd-core/workflows/verify-work.md +129 -55
- package/hooks/dist/gsd-agent-isolation-guard.js +32 -0
- package/hooks/dist/gsd-check-update-worker.js +8 -0
- package/hooks/dist/gsd-check-update.js +8 -0
- package/hooks/dist/gsd-context-monitor.js +31 -8
- package/hooks/dist/gsd-cursor-subagent-start.js +8 -0
- package/hooks/dist/gsd-secret-read-guard.js +161 -4
- package/hooks/dist/gsd-statusline.js +85 -20
- package/hooks/dist/gsd-update-banner.js +8 -0
- package/hooks/dist/gsd-validate-commit.sh +63 -4
- package/hooks/dist/gsd-windsurf-pre-write.js +11 -2
- package/hooks/dist/gsd-workflow-guard.js +5 -4
- package/hooks/dist/gsd-worktree-path-guard.js +6 -2
- package/hooks/dist/lib/cli-exit.js +12 -0
- package/hooks/dist/lib/git-probe.js +17 -1
- package/hooks/dist/lib/isolation-sentinel.js +2 -2
- package/hooks/gsd-agent-isolation-guard.js +32 -0
- package/hooks/gsd-check-update-worker.js +8 -0
- package/hooks/gsd-check-update.js +8 -0
- package/hooks/gsd-context-monitor.js +31 -8
- package/hooks/gsd-cursor-subagent-start.js +8 -0
- package/hooks/gsd-secret-read-guard.js +161 -4
- package/hooks/gsd-statusline.js +85 -20
- package/hooks/gsd-update-banner.js +8 -0
- package/hooks/gsd-validate-commit.sh +63 -4
- package/hooks/gsd-windsurf-pre-write.js +11 -2
- package/hooks/gsd-workflow-guard.js +5 -4
- package/hooks/gsd-worktree-path-guard.js +6 -2
- package/hooks/hooks.json +5 -5
- package/hooks/lib/cli-exit.js +12 -0
- package/hooks/lib/git-probe.js +17 -1
- package/hooks/lib/isolation-sentinel.js +2 -2
- package/package.json +22 -4
- package/scripts/build-hooks.js +15 -6
- package/scripts/changeset/parse.cjs +52 -4
- package/scripts/check-contract-drift.cjs +127 -11
- package/scripts/ci-timeout-report.cjs +770 -4
- package/scripts/command-contract-helpers.cjs +15 -8
- package/scripts/docs-guard-registry.cjs +34 -0
- package/scripts/gen-features.cjs +13 -8
- package/scripts/gen-hooks-cli-exit.cjs +12 -28
- package/scripts/gen-loop-host-contract.cjs +79 -1
- package/scripts/gen-platform-conformance-tier.cjs +187 -1
- package/scripts/gen-plugin-skills.cjs +87 -1
- package/scripts/gen-research-agents.cjs +24 -31
- package/scripts/gen-scripts-cli-exit.cjs +30 -3
- package/scripts/gen-test-timings.cjs +32 -7
- package/scripts/lib/cli-exit.cjs +12 -0
- package/scripts/lib/macos-conformance-tier.generated.cjs +34 -2
- package/scripts/lib/ndjson-reporter.cjs +31 -5
- package/scripts/lib/platform-conformance-tier.generated.cjs +45 -5
- package/scripts/lib/registration-ledger-preload.cjs +155 -0
- package/scripts/lib/vendor-bundle.cjs +59 -0
- package/scripts/lib/vendor-licenses/saxes-6.0.0.txt +64 -0
- 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-completion-predicate-drift.cjs +18 -19
- package/scripts/lint-descriptions.cjs +7 -3
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +38 -1
- package/scripts/lint-eslint-glob-coverage.allowlist.json +20 -0
- package/scripts/lint-frontmatter-fence-drift.cjs +313 -0
- package/scripts/lint-frontmatter-scalar-broad-grep.cjs +122 -16
- package/scripts/lint-phase-arg-assignment.cjs +257 -0
- package/scripts/lint-phase-enumeration-drift.cjs +12 -8
- package/scripts/lint-phase-id-drift.cjs +319 -5
- package/scripts/lint-planning-document-positive-control.cjs +329 -0
- package/scripts/lint-pr-branch-pattern-drift.cjs +148 -0
- package/scripts/lint-response-language-coverage.cjs +3 -0
- package/scripts/lint-retired-runtime-name.cjs +619 -0
- package/scripts/lint-skill-deps.cjs +7 -3
- package/scripts/lint-state-write-path-drift.cjs +93 -0
- package/scripts/lint-test-file-count.allowlist.json +38 -9
- package/scripts/lint-test-file-count.cjs +34 -1
- package/scripts/lint-vendored-deps.cjs +41 -5
- package/scripts/lint-workflow-shellcheck-baseline.json +25 -10
- package/scripts/mutation-matrix.cjs +50 -5
- package/scripts/prompt-injection-scan.sh +4 -0
- package/scripts/release-tarball-smoke.cjs +194 -1
- package/scripts/require-issue-link-policy.cjs +6 -2
- package/scripts/sync-runtime-launcher.cjs +184 -2
- package/scripts/verify-npm-publish.cjs +76 -20
- package/skills/gsd-add-tests/SKILL.md +6 -1
- package/skills/gsd-ai-integration-phase/SKILL.md +6 -1
- package/skills/gsd-audit-fix/SKILL.md +5 -0
- package/skills/gsd-audit-milestone/SKILL.md +6 -1
- package/skills/gsd-autonomous/SKILL.md +7 -2
- package/skills/gsd-capture/SKILL.md +9 -5
- package/skills/gsd-code-review/SKILL.md +7 -2
- package/skills/gsd-complete-milestone/SKILL.md +4 -0
- package/skills/gsd-config/SKILL.md +8 -4
- package/skills/gsd-debug/SKILL.md +11 -7
- package/skills/gsd-discuss-phase/SKILL.md +7 -3
- package/skills/gsd-docs-update/SKILL.md +12 -7
- package/skills/gsd-eval-review/SKILL.md +6 -1
- package/skills/gsd-execute-phase/SKILL.md +12 -7
- package/skills/gsd-extract-learnings/SKILL.md +5 -0
- package/skills/gsd-fast/SKILL.md +4 -0
- package/skills/gsd-forensics/SKILL.md +5 -1
- package/skills/gsd-graphify/SKILL.md +10 -6
- package/skills/gsd-health/SKILL.md +5 -0
- package/skills/gsd-help/SKILL.md +7 -2
- package/skills/gsd-import/SKILL.md +7 -3
- package/skills/gsd-inbox/SKILL.md +5 -0
- package/skills/gsd-ingest-docs/SKILL.md +5 -1
- package/skills/gsd-manager/SKILL.md +6 -1
- package/skills/gsd-map-codebase/SKILL.md +7 -3
- package/skills/gsd-mempalace-capture/SKILL.md +12 -4
- package/skills/gsd-mempalace-recall/SKILL.md +5 -1
- package/skills/gsd-milestone-summary/SKILL.md +5 -1
- package/skills/gsd-mvp-phase/SKILL.md +8 -3
- package/skills/gsd-new-milestone/SKILL.md +6 -1
- package/skills/gsd-new-project/SKILL.md +5 -0
- package/skills/gsd-next/SKILL.md +6 -1
- package/skills/gsd-ns-context/SKILL.md +4 -0
- package/skills/gsd-ns-ideate/SKILL.md +4 -0
- package/skills/gsd-ns-manage/SKILL.md +4 -0
- package/skills/gsd-ns-project/SKILL.md +4 -0
- package/skills/gsd-ns-review/SKILL.md +4 -0
- package/skills/gsd-ns-workflow/SKILL.md +4 -0
- package/skills/gsd-onboard/SKILL.md +6 -1
- package/skills/gsd-pause-work/SKILL.md +5 -1
- package/skills/gsd-phase/SKILL.md +8 -4
- package/skills/gsd-plan-phase/SKILL.md +6 -1
- package/skills/gsd-plan-review-convergence/SKILL.md +10 -6
- package/skills/gsd-pr-branch/SKILL.md +4 -0
- package/skills/gsd-profile-user/SKILL.md +5 -1
- package/skills/gsd-progress/SKILL.md +7 -2
- package/skills/gsd-quick/SKILL.md +16 -10
- package/skills/gsd-quick-batch/SKILL.md +21 -9
- package/skills/gsd-review/SKILL.md +7 -4
- package/skills/gsd-review-backlog/SKILL.md +3 -2
- package/skills/gsd-secure-phase/SKILL.md +6 -1
- package/skills/gsd-ship/SKILL.md +5 -0
- package/skills/gsd-sketch/SKILL.md +7 -2
- package/skills/gsd-spec-phase/SKILL.md +5 -1
- package/skills/gsd-spike/SKILL.md +8 -3
- package/skills/gsd-surface/SKILL.md +5 -1
- package/skills/gsd-thread/SKILL.md +4 -0
- package/skills/gsd-ui-phase/SKILL.md +6 -1
- package/skills/gsd-ui-review/SKILL.md +6 -1
- package/skills/gsd-ultraplan-phase/SKILL.md +5 -1
- package/skills/gsd-undo/SKILL.md +5 -1
- package/skills/gsd-update/SKILL.md +6 -2
- package/skills/gsd-validate-phase/SKILL.md +6 -1
- package/skills/gsd-verify-work/SKILL.md +6 -1
- package/skills/gsd-workspace/SKILL.md +7 -3
- package/skills/gsd-workstreams/SKILL.md +6 -6
- package/vscode/package.json +1 -1
|
@@ -0,0 +1,1019 @@
|
|
|
1
|
+
Apply response_language to all user-facing prose — narration between tool calls, status updates, progress notes, and findings included; preserve code, paths, and identifiers.
|
|
2
|
+
|
|
3
|
+
# `code_review_gate` — report the review and record a per-finding disposition
|
|
4
|
+
|
|
5
|
+
Read and executed by the `code_review_gate` step of `execute-phase/steps/verify-phase-goal.md` (the shared verification action `execute-phase.md`'s `verify_phase_goal` runs), immediately after code review
|
|
6
|
+
returns. It consumes `PHASE_DIR` and `PHASE_NUMBER` and derives everything else.
|
|
7
|
+
|
|
8
|
+
It lives here rather than inline in the parent because `execute-phase.md` sits against two size
|
|
9
|
+
ceilings — the XL hard cap in `tests/workflow-size-budget.test.cjs` and the frozen ADR-857
|
|
10
|
+
pre-phase-6 ceiling in `tests/claude-orchestration.test.cjs` — and both are red lines to be kept
|
|
11
|
+
under, not budgets to spend.
|
|
12
|
+
|
|
13
|
+
**What it is for.** The counts the gate prints say how many findings there were, not what happened
|
|
14
|
+
to any of them. Without a record, the phase directory carries no answer to *what happened to CR-01*
|
|
15
|
+
and a phase can reach `phase.complete` with a Critical standing and no trace it was ever seen.
|
|
16
|
+
|
|
17
|
+
**Why a sibling artifact rather than a section inside REVIEW.md.** `--auto`'s re-review loop
|
|
18
|
+
rewrites REVIEW.md on every iteration, so a ledger kept inside it would not survive the next pass;
|
|
19
|
+
and REVIEW.md has a single writer, `gsd-code-reviewer`, which this step is not.
|
|
20
|
+
|
|
21
|
+
**Advisory — it never blocks.** Every failure path reports and steps over.
|
|
22
|
+
|
|
23
|
+
**Check results using deterministic path (not glob):**
|
|
24
|
+
```bash
|
|
25
|
+
# PADDED must survive a DOTTED phase number, of ANY segment count. This step is dispatched from
|
|
26
|
+
# exactly TWO places: `execute-phase.md` (`code_review_gate`) and `code-review-fix.md`
|
|
27
|
+
# (`record_disposition`). Only the second validates anything -- `code-review-fix.md`'s PADDED_PHASE validator anchors
|
|
28
|
+
# `^[0-9]+[A-Z]?(\.[0-9]+)*$`, an unbounded `*` widened by #4568 and a letter axis widened by #4744, so it accepts `03.1`, `23.1.2` AND `12A`.
|
|
29
|
+
# `execute-phase.md` applies NO shape gate at all, so this fence is not mirroring an upstream
|
|
30
|
+
# guarantee; it IS the guarantee. (`code-review.md`'s own PADDED_PHASE validator is identical but never
|
|
31
|
+
# dispatches this step. It was cited here as a caller for several rounds and is not one.) And
|
|
32
|
+
# `printf "%02d"` cannot format one: bash prints `invalid number` and exits 1, which under
|
|
33
|
+
# `set -euo pipefail` aborts this step on its FIRST line -- the loudest possible failure from
|
|
34
|
+
# the gate that promises never to block, and it takes the whole phase's review reporting with
|
|
35
|
+
# it. Pad the integer part only and carry the sub-number verbatim, so 3.1 -> 03.1, 23.1.2 ->
|
|
36
|
+
# 23.1.2 and 3 -> 03. The segment count is deliberately NOT bounded here: the canonical
|
|
37
|
+
# grammar in src/phase-id.cts (`PHASE_NUMBER_TOKEN_SOURCE`, #2128) is unbounded in segments,
|
|
38
|
+
# and #4568 widened the one dispatcher that validates to match it on that axis, so a guard
|
|
39
|
+
# narrower than that dispatcher means no ledger for a phase id the dispatcher already accepted.
|
|
40
|
+
# On failure NO path is built and the fence refuses by name: advisory means advisory, and it
|
|
41
|
+
# also means never probing a path assembled out of a value we just rejected.
|
|
42
|
+
# VALIDATE, THEN FORMAT -- never format and fall back on failure. `printf "%02d" abc` writes
|
|
43
|
+
# `00` to stdout BEFORE it fails, so a `$(printf ... || printf %s ...)` fallback CONCATENATES
|
|
44
|
+
# the two and yields `00abc`; `08` fails the same way as invalid octal, giving `0008.1` for a
|
|
45
|
+
# legitimate `08.1`. Both were driven. `${PHASE_NUMBER:-}` because an UNSET input must not trip
|
|
46
|
+
# `set -u` in a step that promises not to abort. Those printf failures are why this block VALIDATES
|
|
47
|
+
# instead of formatting; the pad itself performs NO arithmetic since round 14 (see the
|
|
48
|
+
# `case "${#_dig}"` line below), so it has no octal hazard to guard and needs no `10#`. `10#`
|
|
49
|
+
# survives in this step only where it still belongs -- on the severity COUNTS, which really are
|
|
50
|
+
# numbers being added.
|
|
51
|
+
# VALIDATE THE WHOLE VALUE, then format -- and on failure build NO path at all.
|
|
52
|
+
# Carrying an unusable value verbatim was the first draft and it was worse than the bug it
|
|
53
|
+
# replaced: PHASE_NUMBER is interpolated into a file path, so `../../etc/passwd` produced
|
|
54
|
+
# `${PHASE_DIR}/../../etc/passwd-REVIEW.md`, where the old `printf "%02d"` had at least
|
|
55
|
+
# mangled it to `00`. `code-review-fix.md`'s PADDED_PHASE validator already checks `^[0-9]+[A-Z]?(\.[0-9]+)*$` against its
|
|
56
|
+
# own PADDED_PHASE -- the padded form, not the raw PHASE_NUMBER this step is handed -- while
|
|
57
|
+
# `execute-phase.md` validates nothing at all; this step has two call sites and validates for
|
|
58
|
+
# itself rather than trusting either. Anything else yields an EMPTY PADDED and the blocks
|
|
59
|
+
# below refuse to build a path from it.
|
|
60
|
+
# PHASE_DIR is checked for NON-EMPTINESS ONLY. Both inputs come from the caller's init query, so
|
|
61
|
+
# neither is raw user input; only PHASE_NUMBER has a SHAPE (`^[0-9]+[A-Z]?(\.[0-9]+)*$`) to check
|
|
62
|
+
# against. A filesystem path admits `..` and symlinked parents alike, so a shape
|
|
63
|
+
# check here rejects working setups and proves nothing. Residual: PHASE_DIR may itself be a symlink
|
|
64
|
+
# and the ledger is written through it -- left alone, and not a security boundary.
|
|
65
|
+
_pd="${PHASE_DIR:-}"
|
|
66
|
+
_pn="${PHASE_NUMBER:-}"
|
|
67
|
+
_ok=1
|
|
68
|
+
[ -n "$_pd" ] || _ok=0
|
|
69
|
+
case "$_pn" in
|
|
70
|
+
''|*[!0-9.A-Z]*) _ok=0 ;; # empty, or any character outside [0-9.A-Z] -- this is the traversal fence
|
|
71
|
+
.*|*.) _ok=0 ;; # leading or trailing dot
|
|
72
|
+
*..*) _ok=0 ;; # EMPTY SEGMENT. The three arms plus the letter-axis block below
|
|
73
|
+
# accept exactly digits[LETTER](.digits)* -- byte-congruent with the
|
|
74
|
+
# callers' ^[0-9]+[A-Z]?(\.[0-9]+)*$ -- rather than merely wider than
|
|
75
|
+
# the retired `*.*.*` arity bound, which masked `1..2` by accident.
|
|
76
|
+
esac
|
|
77
|
+
# THE LETTER AXIS. The canonical grammar (src/phase-id.cts) is digits, an OPTIONAL single uppercase
|
|
78
|
+
# letter, then dotted digit segments -- `12A`, `3A`, `23A.1.2`. #4744 (#4660) widened the six
|
|
79
|
+
# shell/markdown mirrors to it after this branch was cut, and its lint ratchet then flagged this
|
|
80
|
+
# step as the one letterless mirror left. The character class above admits the letter; these
|
|
81
|
+
# arms pin WHERE it may sit -- only as the last character of the integer part, at most once --
|
|
82
|
+
# so `23a`, `A23`, `2A3`, `23AB` and `23.1A` are all refused.
|
|
83
|
+
if [ "$_ok" = "1" ]; then
|
|
84
|
+
_int="${_pn%%.*}"
|
|
85
|
+
case "$_pn" in *.*) _sub=".${_pn#*.}" ;; *) _sub="" ;; esac
|
|
86
|
+
_let="${_int##*[0-9]}" # what trails the last digit: '' or the letter
|
|
87
|
+
_dig="${_int%"$_let"}"
|
|
88
|
+
case "$_dig" in ''|*[!0-9]*) _ok=0 ;; esac # the integer part must be digits first
|
|
89
|
+
case "$_let" in ''|[A-Z]) ;; *) _ok=0 ;; esac # at most ONE letter, uppercase
|
|
90
|
+
case "$_sub" in *[!0-9.]*) _ok=0 ;; esac # no letter in any later segment
|
|
91
|
+
fi
|
|
92
|
+
# LENGTH-BOUND EACH COMPONENT SEPARATELY -- and the REASON changed at round 14, so read this rather
|
|
93
|
+
# than inherit it. It used to be integer overflow: the pad ran `$((10#$_int))`, bash integers wrap at
|
|
94
|
+
# 2^64, and a 54-digit value yielded -7908320945662590977 SILENTLY as the padded phase. The pad is a
|
|
95
|
+
# string pad now and converts nothing, so that overflow is unreachable and its rationale is dead.
|
|
96
|
+
# WHAT THE BOUND STILL DOES, stated narrowly because the obvious wider claim is FALSE: it bounds each
|
|
97
|
+
# SEGMENT, and NOTHING here bounds the COMPOSITE. Every segment is joined into ONE filename
|
|
98
|
+
# component and depth is unbounded, so a per-segment bound does not enforce a filename limit --
|
|
99
|
+
# driven at round 14: thirty 8-digit segments yield a 278-character PADDED and a 294-character name
|
|
100
|
+
# against a NAME_MAX of 255. That is a real residual of this validator, it predates the pad change,
|
|
101
|
+
# and it is named here rather than papered over with a filesystem rationale the bound does not
|
|
102
|
+
# deliver. The bound belongs on the INTEGER PART: applied to the whole value it rejected
|
|
103
|
+
# `12345678.1`, whose integer part is a legal 8 digits, while accepting `1.123456` -- an accidental
|
|
104
|
+
# bound on the composite that was both too strict and too loose. Every later segment is bounded too,
|
|
105
|
+
# on the same narrow reading.
|
|
106
|
+
# THE LOOP IS THE POINT, and it is what makes the heading above TRUE. The earlier form bounded
|
|
107
|
+
# `${_pn#*.}` -- the WHOLE tail after the first dot -- which is one component only while the id
|
|
108
|
+
# has at most two. Once N-segment ids are accepted (see the shape arms), that form rejects
|
|
109
|
+
# `1.1234567.1`, whose every component is a legal 7-or-fewer digits, purely because the tail
|
|
110
|
+
# measures 9 characters. That is the composite bound this comment already called "too strict",
|
|
111
|
+
# surviving one level up. Walk the segments instead, so the rule is per-component in fact and
|
|
112
|
+
# not only in the heading. The loop terminates on any string the shape arms admit: each pass
|
|
113
|
+
# strips a leading `<seg>.`, and the no-dot pass clears $_rest.
|
|
114
|
+
if [ "$_ok" = "1" ]; then
|
|
115
|
+
_rest="$_pn"
|
|
116
|
+
while [ -n "$_rest" ]; do
|
|
117
|
+
case "$_rest" in
|
|
118
|
+
*.*) _seg="${_rest%%.*}"; _rest="${_rest#*.}" ;;
|
|
119
|
+
*) _seg="$_rest"; _rest="" ;;
|
|
120
|
+
esac
|
|
121
|
+
# The bound is on the DIGITS: a letter suffix is one character the digit bound has no
|
|
122
|
+
# stake in, so `12345678A` is within it exactly as `12345678` is.
|
|
123
|
+
case "${_seg%[A-Z]}" in ?????????*) _ok=0 ;; esac
|
|
124
|
+
done
|
|
125
|
+
fi
|
|
126
|
+
if [ "$_ok" = "1" ]; then
|
|
127
|
+
# padStart(2,'0'), EXACTLY, and as a STRING -- the canonical normalizer left-pads the digit run to a
|
|
128
|
+
# MINIMUM of two and otherwise preserves it, so arithmetic is the wrong tool. `printf "%02d"
|
|
129
|
+
# "$((10#$_dig))"` agreed on `8`/`08`/`09` and silently DISAGREED on every longer leading-zero run:
|
|
130
|
+
# `008` -> `08`, `0008A` -> `08A`, resolving a REVIEW.md path init never writes. Driven at round 14
|
|
131
|
+
# by the property that asserts agreement with `normalizePhaseName` over generated ids; the 13-shape
|
|
132
|
+
# matrix that preceded it sampled no run longer than two and could not see it. Dropping the
|
|
133
|
+
# arithmetic also RETIRES the octal hazard `10#` existed to work around, rather than guarding it.
|
|
134
|
+
# The letter and any dot segments ride along verbatim, as the canonical padder does: `3A` -> `03A`.
|
|
135
|
+
case "${#_dig}" in 1) PADDED="0${_dig}${_let}${_sub}" ;; *) PADDED="${_dig}${_let}${_sub}" ;; esac
|
|
136
|
+
else
|
|
137
|
+
PADDED=""
|
|
138
|
+
fi
|
|
139
|
+
# REFUSE BEFORE BUILDING ANY PATH. An unusable input yields an empty PADDED above, and the
|
|
140
|
+
# earlier placement -- after the assignments -- meant a rejected value still had
|
|
141
|
+
# `${_pd}/-REVIEW.md` assembled and stat'ed before the refusal fired. Nothing is constructed
|
|
142
|
+
# from a value we have already rejected.
|
|
143
|
+
if [ -z "$PADDED" ]; then
|
|
144
|
+
echo "Code review reporting skipped (unusable phase number or directory: '${PHASE_NUMBER:-}')"
|
|
145
|
+
return 0 2>/dev/null || exit 0
|
|
146
|
+
fi
|
|
147
|
+
REVIEW_FILE="${_pd}/${PADDED}-REVIEW.md"
|
|
148
|
+
DISPOSITION_FILE="${_pd}/${PADDED}-REVIEW-DISPOSITION.md"
|
|
149
|
+
# Extract ONLY the leading frontmatter block: `sed -n '/^---$/,/^---$/p'` re-opens its range
|
|
150
|
+
# on a body `---` and runs to EOF, which leaks body lines into the scan. That leak is benign
|
|
151
|
+
# for a key the frontmatter always carries (the first match still wins) but NOT for an
|
|
152
|
+
# optional one — a review with no `findings:` block and a body `total:` line would otherwise
|
|
153
|
+
# report the body's number as the count. Stop at the closing delimiter instead, and strip CR
|
|
154
|
+
# first so a CRLF-authored review neither breaks the delimiter match nor injects a carriage
|
|
155
|
+
# return into the message below (DEFECT.FRONTMATTER-SCALAR-BROAD-GREP).
|
|
156
|
+
# Buffered, and emitted only if the CLOSING delimiter was actually seen: an unterminated
|
|
157
|
+
# frontmatter block would otherwise run to EOF and hand the whole review body to the reads below,
|
|
158
|
+
# defeating the scoping entirely.
|
|
159
|
+
# Guarded and `|| true`: this step is advisory, so a REVIEW.md that is missing, a directory, or
|
|
160
|
+
# otherwise unreadable must leave the counts empty and let execution continue — never abort the
|
|
161
|
+
# step under `set -e`/`pipefail`.
|
|
162
|
+
# REVIEW_READ records that the file was actually OPENED, separately from what it yielded. An
|
|
163
|
+
# absent or unreadable review and a present-but-unparseable one both leave every value below
|
|
164
|
+
# empty, and the reporting arm used to treat the two identically -- silence -- so a REVIEW.md
|
|
165
|
+
# with three criticals and an unterminated frontmatter read exactly like a clean review. A
|
|
166
|
+
# malformed report must not read as a clean one; the arm below tells them apart on this flag.
|
|
167
|
+
REVIEW_FM=""
|
|
168
|
+
REVIEW_READ=0
|
|
169
|
+
if [ -f "$REVIEW_FILE" ] && [ -r "$REVIEW_FILE" ]; then
|
|
170
|
+
REVIEW_READ=1
|
|
171
|
+
REVIEW_FM=$(tr -d '\r' < "$REVIEW_FILE" 2>/dev/null | LC_ALL=C awk 'NR==1{if($0!="---") exit; next} /^---$/{closed=1; exit} {buf = buf $0 "\n"} END{if (closed) printf "%s", buf}' || true)
|
|
172
|
+
fi
|
|
173
|
+
# `|| true` on every read: under `pipefail` a non-matching `grep` exits 1, and an assignment
|
|
174
|
+
# whose command substitution fails aborts the step under `set -e`. An advisory gate must survive
|
|
175
|
+
# a REVIEW.md with no frontmatter at all.
|
|
176
|
+
# STATUS TAKES THE SAME PARSER AS THE COUNTS, and it is the read where truncation costs most. Under
|
|
177
|
+
# `cut -d: -f2` the valid YAML scalar `status: clean:junk` arrived as the bare `clean` -- so an
|
|
178
|
+
# unusable status SILENTLY took the clean arm, suppressing both the report and the ledger. `-f2-`
|
|
179
|
+
# keeps the whole scalar, `clean:junk` matches no arm, and the step reports. Found by the round's
|
|
180
|
+
# fourth adversarial pass as a sibling of the count-parser class, in the same file.
|
|
181
|
+
REVIEW_STATUS=$(echo "$REVIEW_FM" | LC_ALL=C grep -m1 "^status:" | cut -d: -f2- | LC_ALL=C sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//' || true)
|
|
182
|
+
# The counts belong to the `findings:` MAPPING, not merely to the frontmatter, and the scoping now
|
|
183
|
+
# goes all the way there. `^[[:space:]]*total:` matches any indented key anywhere in the block, so
|
|
184
|
+
# a top-level key later named `total:`, `info:` or `critical:` was picked up ahead of the nested
|
|
185
|
+
# one — the extensive comment above is about scoping the frontmatter, and the scoping stopped one
|
|
186
|
+
# level short of the mapping the values actually live in. `status:` was never exposed: it is
|
|
187
|
+
# anchored to column 0 because it IS top-level.
|
|
188
|
+
# The awk selects the `findings:` block and stops at the next column-0 key, so the reads below can
|
|
189
|
+
# only see keys nested under it. Block 2 derives REVIEW_TOTAL through the same filter.
|
|
190
|
+
# `blocker:` is the documented tier-equivalent of `critical:` (gsd-code-reviewer.md § "Label
|
|
191
|
+
# equivalence") — accept either, exactly as code-review.md's present_results already does.
|
|
192
|
+
REVIEW_FINDINGS_FM=$(echo "$REVIEW_FM" | LC_ALL=C awk '/^findings:[[:space:]]*$/{f=1; next} f&&/^[^[:space:]]/{exit} f' || true)
|
|
193
|
+
REVIEW_CRITICAL=$(echo "$REVIEW_FINDINGS_FM" | LC_ALL=C grep -E -m1 "^[[:space:]]*(critical|blocker):" | cut -d: -f2- | LC_ALL=C sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//' || true)
|
|
194
|
+
REVIEW_WARNING=$(echo "$REVIEW_FINDINGS_FM" | LC_ALL=C grep -E -m1 "^[[:space:]]*warning:" | cut -d: -f2- | LC_ALL=C sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//' || true)
|
|
195
|
+
REVIEW_INFO=$(echo "$REVIEW_FINDINGS_FM" | LC_ALL=C grep -E -m1 "^[[:space:]]*info:" | cut -d: -f2- | LC_ALL=C sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//' || true)
|
|
196
|
+
REVIEW_TOTAL=$(echo "$REVIEW_FINDINGS_FM" | LC_ALL=C grep -E -m1 "^[[:space:]]*total:" | cut -d: -f2- | LC_ALL=C sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//' || true)
|
|
197
|
+
# ONE PARSER FOR THE WHOLE STEP. These reads used `cut -d: -f2 | tr -d ' '`, which repairs a
|
|
198
|
+
# malformed scalar into a number twice over: `tr -d` deletes INTERNAL spaces (`1 0` -> `10`) and
|
|
199
|
+
# `-f2` keeps only the SECOND FIELD (`1: junk` -> `1`). Block 2 was tightened first, which left the
|
|
200
|
+
# two fences disagreeing about the same bytes -- driven: `critical: 1 0` made this block print
|
|
201
|
+
# `10 findings -- 10 critical` while the ledger recorded three rows and no shortfall. A console line
|
|
202
|
+
# and a ledger contradicting each other is the exact confusion this PR exists to remove, so the fix
|
|
203
|
+
# is one parser rather than a disclosed divergence. `-f2-` keeps the whole scalar; only the ends are
|
|
204
|
+
# trimmed. A repaired number is not a number.
|
|
205
|
+
# LC_ALL=C ON EVERY `grep`/`sed` IN THESE READS, and it is load-bearing rather than cosmetic: the
|
|
206
|
+
# POSIX classes are LOCALE-DEFINED, and glibc's C.UTF-8 puts U+2003 (and U+1680, U+2000-U+200A,
|
|
207
|
+
# U+205F, U+3000) in BOTH [[:space:]] and [[:blank:]], where C and en_US.UTF-8 put them in neither.
|
|
208
|
+
# Unpinned, `status: clean<U+2003>` trimmed to `clean` under one locale and stayed unusable under
|
|
209
|
+
# another -- the same silent suppression as the `clean:junk` truncation, reachable only on some
|
|
210
|
+
# machines. Pinned to C the class is exactly {space, tab, NL, VT, FF, CR}, which is what the mirror
|
|
211
|
+
# in tests/code-review-pipeline-regression.test.cjs spells out literally, so the two agree by
|
|
212
|
+
# construction rather than by coincidence of locale.
|
|
213
|
+
# The breakdown is reportable only when ALL FOUR counts are numbers. Deciding on REVIEW_TOTAL
|
|
214
|
+
# alone would still emit `6 findings — critical` for a review carrying a total and nothing else.
|
|
215
|
+
REVIEW_COUNTS_OK=1
|
|
216
|
+
for _c in "$REVIEW_TOTAL" "$REVIEW_CRITICAL" "$REVIEW_WARNING" "$REVIEW_INFO"; do
|
|
217
|
+
# Length-bounded as well as digit-only: bash integers wrap at 2^64, so a 20-digit count
|
|
218
|
+
# arrives at the sum below as 0 and an inconsistent breakdown passes. No real review
|
|
219
|
+
# reports nine digits of findings.
|
|
220
|
+
case "$_c" in ''|*[!0-9]*) REVIEW_COUNTS_OK=0 ;; ?????????*) REVIEW_COUNTS_OK=0 ;; esac
|
|
221
|
+
done
|
|
222
|
+
# Numeric is necessary and not sufficient. `total: 0` beside `critical: 1` is four valid numbers
|
|
223
|
+
# that render the self-contradicting line `0 findings — 1 critical, 0 warning, 0 info`. An
|
|
224
|
+
# inconsistent breakdown is unavailable for the same reason a partial one is: half-true is worse
|
|
225
|
+
# than withheld, and the countless form is already the documented fallback.
|
|
226
|
+
# `10#` on every operand: bash infers the base from a leading zero, so a review reporting
|
|
227
|
+
# `critical: 08` makes $(( )) fail with "value too great for base". The CONSEQUENCE stated here
|
|
228
|
+
# used to be "takes the whole advisory step down under `set -e`", and that is WRONG: the
|
|
229
|
+
# arithmetic sits inside an `if` condition, a TESTED context, where `set -e` is inert. What
|
|
230
|
+
# actually happens is that the consistency check is SKIPPED -- which the regression suite
|
|
231
|
+
# already records. Skipping it is still the wrong outcome for a check whose whole job is
|
|
232
|
+
# refusing a half-true breakdown, so `10#` stays; only the account of what it prevents is
|
|
233
|
+
# corrected. The values are already digit-only by the loop above.
|
|
234
|
+
if [ "$REVIEW_COUNTS_OK" = "1" ] \
|
|
235
|
+
&& [ "$((10#$REVIEW_CRITICAL + 10#$REVIEW_WARNING + 10#$REVIEW_INFO))" -ne "$((10#$REVIEW_TOTAL))" ]; then
|
|
236
|
+
REVIEW_COUNTS_OK=0
|
|
237
|
+
fi
|
|
238
|
+
# EMIT — inside the fence, on every reporting arm. Until this block existed, the fence computed six
|
|
239
|
+
# values and printed none of them, and the prose below then asked the agent to display four of them.
|
|
240
|
+
# The shell exits at the closing fence and the agent sees only stdout, so those values were
|
|
241
|
+
# unobtainable: the message could not be rendered, and the whole block was decorative. That is the
|
|
242
|
+
# rule block 2 states about itself — a prose-only gate on a value no later block can see is not a
|
|
243
|
+
# gate — applied to the block that is this step's primary deliverable rather than only to its
|
|
244
|
+
# sibling. The status arm is re-derived here, not left to the reader, for the same reason.
|
|
245
|
+
case "$REVIEW_STATUS" in
|
|
246
|
+
'')
|
|
247
|
+
# NO STATUS is not NO REVIEW. When the file was read and yielded no status -- unterminated
|
|
248
|
+
# frontmatter, no frontmatter, no `status:` key, a zero-byte file -- the review is
|
|
249
|
+
# UNPARSEABLE, and saying nothing would make it indistinguishable from a clean one. State it,
|
|
250
|
+
# without the breakdown (there is none to trust) and without the --fix suggestion (nothing
|
|
251
|
+
# here proves there are findings to fix). An absent or unreadable review stays silent: there
|
|
252
|
+
# is nothing to describe, and guessing is the failure the guard above exists to prevent.
|
|
253
|
+
if [ "$REVIEW_READ" = "1" ]; then
|
|
254
|
+
echo "Code review status unparsed: REVIEW.md is present but its frontmatter has no parseable status; severity counts unavailable."
|
|
255
|
+
fi
|
|
256
|
+
;;
|
|
257
|
+
clean|skipped) ;; # nothing to report; block 2 still reconciles an existing ledger
|
|
258
|
+
*)
|
|
259
|
+
if [ "$REVIEW_COUNTS_OK" = "1" ]; then
|
|
260
|
+
echo "Code review: ${REVIEW_TOTAL} findings — ${REVIEW_CRITICAL} critical, ${REVIEW_WARNING} warning, ${REVIEW_INFO} info."
|
|
261
|
+
else
|
|
262
|
+
# A REVIEW.md written without a `findings:` block has no counts to report, and any count that
|
|
263
|
+
# is empty, non-numeric, over-long or inconsistent makes the whole breakdown unavailable
|
|
264
|
+
# rather than half-filled. Half-true is worse than withheld.
|
|
265
|
+
echo "Code review found issues."
|
|
266
|
+
fi
|
|
267
|
+
echo "Consider running: /gsd:code-review ${PHASE_NUMBER:-} --fix"
|
|
268
|
+
;;
|
|
269
|
+
esac
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
**Display that block's stdout verbatim.** It prints the severity breakdown when all four counts are
|
|
273
|
+
numeric and mutually consistent, and the countless form otherwise; on a clean, skipped or absent
|
|
274
|
+
review it prints nothing and there is nothing to display. Do not re-derive any of it — a number the
|
|
275
|
+
shell computed and did not print is gone once the fence closes, which is precisely the defect this
|
|
276
|
+
arm exists to close.
|
|
277
|
+
|
|
278
|
+
**Record a per-finding disposition.** The counts say how many findings there were, not what
|
|
279
|
+
happened to any of them. On the same condition as the message above — REVIEW_STATUS not "clean",
|
|
280
|
+
not "skipped" and not empty — write `${DISPOSITION_FILE}`: one row per finding ID, defaulting to
|
|
281
|
+
`open`, reconciling `fixed`/`skipped` from REVIEW-FIX.md and preserving any disposition already
|
|
282
|
+
recorded, its stated reason included. It is a sibling artifact because `--auto` rewrites
|
|
283
|
+
REVIEW.md every iteration and `gsd-code-reviewer` is its single writer. Advisory like the rest of
|
|
284
|
+
the step — never blocks:
|
|
285
|
+
|
|
286
|
+
## Design notes for the embedded record-builder
|
|
287
|
+
|
|
288
|
+
These notes document the `node -e` script in the fence below. They live here rather than
|
|
289
|
+
as comments inside that script because the script is passed to `node -e` as a single
|
|
290
|
+
command-line argument, and Windows caps a command line at 32,767 characters
|
|
291
|
+
(`CreateProcess`); with the commentary inline the argument reached 33,353 characters and
|
|
292
|
+
the step failed to launch on Windows with `ENAMETOOLONG`. Each note names the line it
|
|
293
|
+
precedes, so the pairing survives the move.
|
|
294
|
+
|
|
295
|
+
**Before `(function main() {`**
|
|
296
|
+
|
|
297
|
+
EVERYTHING BELOW RUNS INSIDE main() AND LEAVES BY return, NEVER an explicit exit call. The script
|
|
298
|
+
prints its one-line verdict and then ends; with an explicit exit directly after console.log,
|
|
299
|
+
the exit can pre-empt the write when stdout is a pipe or socket (Node documents those writes
|
|
300
|
+
as asynchronous on POSIX), and the caller then sees an exit 0 with NO verdict line. A
|
|
301
|
+
hardening against that documented hazard, not a reproduced defect: the 'unchanged' branch was
|
|
302
|
+
the only one that exited explicitly, and the empty stdout that first pointed at it turned out to
|
|
303
|
+
be a reviewing sandbox's own. A function that returns lets the event loop drain stdout before
|
|
304
|
+
the process ends. Same exit status either way.
|
|
305
|
+
|
|
306
|
+
**Before `if (fs.existsSync(process.env.DISPOSITION_FILE) && !fs.lstatSync(process.env.DISPOSITION_FILE).isFile()) {`**
|
|
307
|
+
|
|
308
|
+
AN EXISTING LEDGER THAT IS NOT A REGULAR FILE IS NOT A LEDGER. Checked FIRST, before any read
|
|
309
|
+
or write of that path, and the ordering is the fix rather than a tidy-up:
|
|
310
|
+
* writeFileSync FOLLOWS a symlink, so a planted link replaced the contents of whatever it
|
|
311
|
+
pointed at -- outside the phase directory, link left intact so nothing looked wrong;
|
|
312
|
+
* a FIFO at that path made readFileSync BLOCK FOREVER, which is the one behaviour a gate
|
|
313
|
+
documented as advisory and non-blocking must never have;
|
|
314
|
+
* and the unchanged-run fast path read the file before the check, so a symlink whose
|
|
315
|
+
target already matched slipped through reporting 'unchanged'.
|
|
316
|
+
All three driven. lstatSync does not follow the link, which is why it is the right call.
|
|
317
|
+
NAMED RESIDUAL, not silently accepted: this is a check-then-write, so a symlink planted
|
|
318
|
+
between the lstat and the write still wins. Node exposes no portable O_NOFOLLOW write, and
|
|
319
|
+
an attacker who can write into the phase directory mid-run already has what the check would
|
|
320
|
+
protect. It narrows a real accident; it is not a security boundary, and the docs do not
|
|
321
|
+
claim one. A hard link likewise passes isFile() by construction.
|
|
322
|
+
|
|
323
|
+
**Before `let fence = null;`**
|
|
324
|
+
|
|
325
|
+
The OPEN fence's marker is remembered, not just the fact of being fenced. A bare toggle
|
|
326
|
+
treats every fence marker as interchangeable, so a ~~~ line inside a ` ` ` block CLOSES it
|
|
327
|
+
and the block's real close REOPENS one — which silently swaps a fenced example for the
|
|
328
|
+
real findings around it. Driven: a review quoting ~~~ inside a fenced example recorded
|
|
329
|
+
the EXAMPLE's id and dropped the real finding entirely. Per CommonMark, a fence closes
|
|
330
|
+
only on the same character, at least as long as the one that opened it.
|
|
331
|
+
|
|
332
|
+
**Before `const SECTION_SEV = [[/^##\s+Critical Issues\s*$/, 'critical'], [/^##\s+Warnings\s*$/, 'warning'], [/^##\s+Info\s*$/, 'info']];`**
|
|
333
|
+
|
|
334
|
+
SEVERITY COMES FROM THE SECTION FIRST, the recorded ledger value second, the id prefix
|
|
335
|
+
third (the full precedence is at sev(), below the identity check). The section heading is the
|
|
336
|
+
reviewer's OWN statement of a finding's severity -- gsd-code-reviewer.md emits findings under
|
|
337
|
+
'## Critical Issues' / '## Warnings' / '## Info' -- and this walker already visits every line,
|
|
338
|
+
so the signal was in hand and discarded. Deriving from the prefix alone means a reviewer who
|
|
339
|
+
mis-numbers a Critical as WR-04 while filing it under '## Critical Issues' gets a ledger row
|
|
340
|
+
reading 'warning', which then disagrees with the review it summarizes AND with the frontmatter
|
|
341
|
+
count line block 1 prints from findings.critical. The Severity column is the whole basis for
|
|
342
|
+
triaging the ledger, so it has to agree with the document it describes.
|
|
343
|
+
Matched WHOLE, exactly as the fix-report sections are: a prefix match would let a heading like
|
|
344
|
+
'## Critical Issues Verification' re-tier everything under it.
|
|
345
|
+
|
|
346
|
+
**Before `const declaredTotal = /^[0-9]+$/.test(process.env.REVIEW_TOTAL || '') ? Number(process.env.REVIEW_TOTAL) : null;`**
|
|
347
|
+
|
|
348
|
+
A review that reports nothing still has to reconcile an EXISTING ledger: its decided rows
|
|
349
|
+
and its untriaged rows are BOTH carried, marked. Exiting here would freeze a stale ledger
|
|
350
|
+
showing findings as open that the review no longer reports.
|
|
351
|
+
A fix report with no ledger is also something to record: a converged '--auto' run has neither,
|
|
352
|
+
and exiting here recorded nothing for a fully fixed phase.
|
|
353
|
+
A review that reports findings NONE of which this parser understood is also something to
|
|
354
|
+
record, and it is the case with the least evidence anywhere else. The shortfall is derived
|
|
355
|
+
HERE, above the guard, rather than at its old site beside the render: order is final from
|
|
356
|
+
the heading walk above and never grows again, so the value is the same either way -- but at
|
|
357
|
+
the old site it was computed AFTER this return had already fired, so it could not reach the
|
|
358
|
+
one exit that discards it. Partial shortfalls (some findings parsed, some not) always
|
|
359
|
+
reported, which is exactly why the total one read as covered.
|
|
360
|
+
|
|
361
|
+
**Before `const prior = new Map();`**
|
|
362
|
+
|
|
363
|
+
Prior rows: keep the disposition AND its source cell — the source is where a human writes
|
|
364
|
+
the reason a finding was deferred, and rewriting it would discard the very thing the
|
|
365
|
+
'set deferred by hand, with the reason' instruction asks for. The Source cell is the LAST
|
|
366
|
+
column, so it is captured through to the end of the line, less an optional trailing pipe:
|
|
367
|
+
a bare | inside it is prose, not a column break. The previous capture admitted a pipe only
|
|
368
|
+
when escaped, and the whole-line match then FAILED on a bare one -- a human who wrote
|
|
369
|
+
'waiting on team A | team B' as a deferral reason had the row not match at all, the finding
|
|
370
|
+
reset to open, and the reason destroyed: a triaged Critical rendered indistinguishable from
|
|
371
|
+
one never seen, off an ordinary typo in the one field this ledger asks a human to hand-edit.
|
|
372
|
+
The render below escapes a bare pipe on the next write, so the file converges to the escaped
|
|
373
|
+
form either way. The trailing pipe is optional so a hand-mangled row loses no decision.
|
|
374
|
+
|
|
375
|
+
**Before `const SEV_VOCAB = ['critical', 'warning', 'info'];`**
|
|
376
|
+
|
|
377
|
+
SEVERITY, READ BACK. The ledger has always WRITTEN a severity for every row -- in the table's
|
|
378
|
+
Severity cell and in the frontmatter's 'severity:' key -- and until this map existed nothing
|
|
379
|
+
read either back: the row regex discarded the cell as [^|]*, the frontmatter walk collected
|
|
380
|
+
only titles, and a CARRIED row was rebuilt through sev() from the id prefix, because
|
|
381
|
+
sectionSev holds only findings the CURRENT review reports. So a WR-04 the reviewer filed under
|
|
382
|
+
'## Critical Issues' was recorded 'critical', a human deferred it, and the next run -- the
|
|
383
|
+
review no longer reporting it -- silently re-recorded it 'warning'. The one artifact whose
|
|
384
|
+
purpose is remembering a finding's severity lost it on the second run, in the unsafe
|
|
385
|
+
direction. Driven by executing the shipped script twice (round 11).
|
|
386
|
+
The table cell is read first (it is the human-facing surface, and the one the disposition
|
|
387
|
+
already comes from); the frontmatter key is the fallback for a hand-mangled cell. Both are
|
|
388
|
+
ENUM-validated -- a value outside critical|warning|info is not a severity and is ignored, so
|
|
389
|
+
the row falls through to inference rather than carrying garbage (ADR-227, the same rule the
|
|
390
|
+
disposition column takes).
|
|
391
|
+
|
|
392
|
+
**Before `const m = l.match(/^\|\s*((?:CR|BL|WR|IN)-\d+)\s*\|\s*([^|]*?)\s*\|\s*(open|fixed|skipped|deferred)\s*\|\s*(.*?)\s*\|?\s*$/);`**
|
|
393
|
+
|
|
394
|
+
The disposition column is an ENUM, not 'any lowercase token'. ADR-227 requires a trust
|
|
395
|
+
boundary to validate semantic SHAPE, not merely type, and to coerce a failure to the
|
|
396
|
+
contract's safe default -- and this ledger is a trust boundary by construction, because
|
|
397
|
+
the rendered instruction tells a human to hand-edit it. Under the old ([a-z]+) capture a
|
|
398
|
+
single transposed character ('opne') was stored as a decision: it is not 'open', so it
|
|
399
|
+
beat the default, was excluded from the open: headline count, and was carried forward
|
|
400
|
+
forever. One typo and the ledger reported a phase fully triaged.
|
|
401
|
+
Note the asymmetry that made this a correctness bug rather than a style point: a typo
|
|
402
|
+
OUTSIDE [a-z] ('Deferred') already failed to match, lost the decision and reset the row
|
|
403
|
+
to open -- safe. A typo INSIDE [a-z] was unsafe. The parser failed open in the one
|
|
404
|
+
direction that matters. A row that does not match now yields no prior entry, so the row
|
|
405
|
+
falls back to 'open' -- the safe default, by the same path the capital-D case took.
|
|
406
|
+
The Severity cell is CAPTURED, not skipped: it is the value the carry-forward below has to
|
|
407
|
+
preserve, and skipping it (the previous [^|]*) is how a carried row lost its tier.
|
|
408
|
+
|
|
409
|
+
**Before `if (m) {`**
|
|
410
|
+
|
|
411
|
+
Strip the carried marker before storing: it is rendered from the carried flag, so
|
|
412
|
+
leaving it on the stored value would re-append it every run — the cell grows without
|
|
413
|
+
bound AND the file changes on every run, defeating the unchanged-run check below.
|
|
414
|
+
Strip AT MOST ONE trailing marker, unconditionally. Storing the cell verbatim looked
|
|
415
|
+
like the way to stop the strip eating human text, and it introduced a worse defect:
|
|
416
|
+
once the generated marker is stored it can never leave, so a carried finding that
|
|
417
|
+
REAPPEARS in a later review still renders 'not in the current review' -- a ledger that
|
|
418
|
+
is now factually wrong about its own contents. The residual ambiguity is irreducible
|
|
419
|
+
(a reason ending in exactly that phrase is indistinguishable from the marker) and it
|
|
420
|
+
costs nothing real: on a carried row the render puts the phrase straight back, and on a
|
|
421
|
+
current row the phrase was self-contradictory to begin with. The unbounded quantifier is
|
|
422
|
+
what had to go, not the strip itself.
|
|
423
|
+
|
|
424
|
+
**Before `const sameTitle = (a, b) => String(a === undefined ? '' : a).replace(/\s+/g, ' ').trim()`**
|
|
425
|
+
|
|
426
|
+
TITLE COMPARISON, and its FALSE-POSITIVE mode, which was previously unacknowledged.
|
|
427
|
+
The strict instinct is right -- ids are reused across re-reviews, so a stale REVIEW-FIX.md
|
|
428
|
+
must not mark a brand-new CR-01 as already fixed -- but gsd-code-fixer.md writes
|
|
429
|
+
'### {finding_id}: {title}' under no contract that the title is copied byte-for-byte from
|
|
430
|
+
REVIEW.md. A fixer that REFLOWS a long title produced a spurious stale note, left a
|
|
431
|
+
genuinely-fixed row 'open', and told the reader the fix report named a different finding.
|
|
432
|
+
Runs of whitespace are collapsed because re-spacing carries no information. The BOUND, stated
|
|
433
|
+
because it is easy to over-read this: a title WRAPPED across lines is NOT reconciled. A '###'
|
|
434
|
+
heading is one line by definition, so the continuation is a separate paragraph the heading
|
|
435
|
+
parser correctly never captures, and collapsing whitespace cannot reach across that boundary.
|
|
436
|
+
Not widened -- absorbing whatever follows a heading into the title would swallow arbitrary
|
|
437
|
+
prose and make this very check meaningless. Case changes and truncation stay strict too --
|
|
438
|
+
they are the shapes a genuinely different finding actually takes, and widening to them would
|
|
439
|
+
trade this false positive for the silent false NEGATIVE the strict match exists to prevent.
|
|
440
|
+
Residual, stated: a fixer that re-cases or truncates still produces a spurious note. That is
|
|
441
|
+
the safe direction (a visible note, not a silent wrong 'fixed'), and the note's wording below
|
|
442
|
+
no longer asserts which of the two it is.
|
|
443
|
+
|
|
444
|
+
**Before `if (h.id && sect && !applied.has(h.id)) {`**
|
|
445
|
+
|
|
446
|
+
First occurrence wins, so an id listed under BOTH sections is not decided by row order.
|
|
447
|
+
And the fix report must name the SAME finding: ids are reused across re-reviews, so a
|
|
448
|
+
stale REVIEW-FIX.md would otherwise mark a brand-new CR-01 as already fixed.
|
|
449
|
+
A title mismatch is the STALE-report case and must not pass silently: the id is
|
|
450
|
+
reused, the finding is not, and a reader who sees the row stay 'open' has no way to
|
|
451
|
+
tell that from 'the fix report never mentioned it'. Record it and say so below.
|
|
452
|
+
|
|
453
|
+
**Before `const sev = (id) => sectionSev.get(id) || (priorSev.has(id) && sameFinding(id) ? priorSev.get(id) : prefixSev(id));`**
|
|
454
|
+
|
|
455
|
+
SEVERITY PRECEDENCE: the current review's SECTION (the reviewer's own statement, this run),
|
|
456
|
+
then the severity this ledger RECORDED (an earlier reviewer's statement, persisted), then the
|
|
457
|
+
id PREFIX (an inference). A recorded value is inherited only while the id still names the
|
|
458
|
+
SAME finding -- the identity rule the disposition already obeys -- so a reused id starts from
|
|
459
|
+
its own review's section or its prefix, never from the finding it replaced. A carried row is
|
|
460
|
+
absent from the current review, so sameFinding() is true for it by construction and its
|
|
461
|
+
recorded severity is what it keeps. Defined here, below the identity check, because it
|
|
462
|
+
depends on it.
|
|
463
|
+
|
|
464
|
+
**Before `const carriedIds = [];`**
|
|
465
|
+
|
|
466
|
+
A prior finding the current review no longer reports is CARRIED, never dropped -- and that
|
|
467
|
+
now holds for UNTRIAGED rows too, which is the correction. Carrying only decided rows meant
|
|
468
|
+
an untriaged row for a dropped or renumbered finding disappeared without trace, and combined
|
|
469
|
+
with the reconciliation gap that left EVERY row untriaged, a re-review silently deleted the
|
|
470
|
+
whole ledger. The --auto loop rewrites REVIEW.md on every iteration, so it does not retain it
|
|
471
|
+
either: run 1 records CR-01 open, the re-review renumbers it to CR-02, and run 2's ledger
|
|
472
|
+
contains neither. That is #3829's complaint verbatim -- 'no trace of what happened to them' --
|
|
473
|
+
reproduced by the artifact built to prevent it, and 'nothing was decided about it' is exactly
|
|
474
|
+
the state #3829 says must leave a trace.
|
|
475
|
+
The carried marker is what keeps this honest rather than merely additive: the row does not
|
|
476
|
+
claim the finding is live, it records that it was seen and never triaged. Stated cost, since
|
|
477
|
+
it is real: a RENUMBERED finding appears twice until someone triages the old row, and a
|
|
478
|
+
carried untriaged row persists across runs until decided. Both are bounded by the phase's own
|
|
479
|
+
findings, both are legible from the marker, and both are strictly better than a silent delete.
|
|
480
|
+
Prior rows UNION ids a fix report decided that the review no longer reports: a decision the
|
|
481
|
+
ledger cannot render is a decision lost. Precedence matches row() -- applied beats recorded.
|
|
482
|
+
|
|
483
|
+
**Before `const reusedNote = reused.length ? ' (' + reused.length + ' recorded decision(s) DROPPED -- the id now names a different finding, so the decision no longer has a row: ' + reused.join(', ') + ')' : '';`**
|
|
484
|
+
|
|
485
|
+
Surfaced, not thrown: the gate is advisory. But a fix report naming a finding whose title
|
|
486
|
+
no longer matches is the one case where 'open' understates what is known, so it is stated.
|
|
487
|
+
The wording no longer ASSERTS a stale report. Both causes reach here -- a genuinely different
|
|
488
|
+
finding under a reused id, and a fixer that re-titled the same one -- and the step cannot tell
|
|
489
|
+
them apart, so it reports the observation rather than a conclusion it has not earned.
|
|
490
|
+
On the console too, for a reader who never opens the ledger.
|
|
491
|
+
|
|
492
|
+
**Before `if (rows.length === 0 && !unparsed && !fs.existsSync(process.env.DISPOSITION_FILE)) return;`**
|
|
493
|
+
|
|
494
|
+
RECONCILE THE TWO PARSERS. The counts come from REVIEW.md's frontmatter; the rows come from
|
|
495
|
+
heading matches against a CLOSED CR|BL|WR|IN alternation. A finding the heading parser cannot
|
|
496
|
+
match -- a fifth prefix, a missing ': ' separator, a '#### ' heading -- contributed no row, no
|
|
497
|
+
note and no diagnostic, and the ledger then declared 'open: 3 of 3' over a set strictly
|
|
498
|
+
smaller than the console line reported one paragraph earlier. Two findings recorded nowhere,
|
|
499
|
+
and neither artifact said so.
|
|
500
|
+
The earlier argument for the closed alternation -- that an unlisted prefix produces no row
|
|
501
|
+
rather than a MIS-CLASSIFIED one -- is the wrong trade under this repo's own fail-safe rule:
|
|
502
|
+
a dropped finding is demoted below every finding that parsed, and an unparseable finding is
|
|
503
|
+
precisely the one a human most needs to see. Surfaced, not thrown, exactly as the stale
|
|
504
|
+
fix-report case above is: the gate stays advisory and states the shortfall.
|
|
505
|
+
The !unparsed conjunct here is the SECOND of the two exits that discarded the shortfall, and
|
|
506
|
+
it is not redundant with the one above: that guard keys on order and stands down when a fix
|
|
507
|
+
report exists, so a run with a fix report and no parseable finding reaches THIS line with
|
|
508
|
+
rows.length 0. Both exits now decline to fire while a shortfall is outstanding, and the
|
|
509
|
+
result is a zero-row ledger carrying an unparsed key -- an honest record that the review
|
|
510
|
+
declared findings and none of them were understood, which is strictly better than the file
|
|
511
|
+
not existing. A genuinely clean review is untouched either way: a declared total of 0 is not
|
|
512
|
+
greater than order.length, so unparsed is 0 and both returns still fire.
|
|
513
|
+
|
|
514
|
+
**Before `const escapePipes = (t) => t.replace(/\\.|\|/g, (m) => (m === '|' ? '\\|' : m));`**
|
|
515
|
+
|
|
516
|
+
A bare | in a Source cell is escaped on render so the table stays a table. Scanned as PAIRS,
|
|
517
|
+
not by the preceding character: an escaped pair (backslash + anything) is kept verbatim and only
|
|
518
|
+
a pipe outside one is escaped. The previous form, /(^|[^\\])\|/g, CONSUMED the character before
|
|
519
|
+
the pipe, so adjacent bare pipes were escaped one per run (A||B -> A\||B -> A\|\|B, a third run
|
|
520
|
+
to converge) and an escaped backslash before a pipe (A\\|B) hid the pipe behind the wrong
|
|
521
|
+
parity and left it bare. Found by the round-3 adversarial pass, not by the property -- whose
|
|
522
|
+
generator then emitted at most one bare pipe, the one case the old form got right; it now
|
|
523
|
+
reaches adjacent pipes and both backslash parities, against an independent parity oracle.
|
|
524
|
+
|
|
525
|
+
**Before `fs.writeFileSync(process.env.DISPOSITION_FILE, render(new Date().toISOString()));`**
|
|
526
|
+
|
|
527
|
+
READ-MODIFY-WRITE, NO LOCK. The ledger is rendered whole from a read taken above, and nothing
|
|
528
|
+
serializes two writers: this step has two dispatchers (execute-phase's gate and
|
|
529
|
+
code-review-fix's record_disposition) plus a human the legend invites to hand-edit, so a
|
|
530
|
+
lost update is a real window, not a theoretical one. Same shape as #3780 (WINDOWS.md
|
|
531
|
+
append under parallel executors), which #4681 closed with a cross-process lock in
|
|
532
|
+
src/broken-windows.cts. NOT taken here: this is a shell-embedded script with no build
|
|
533
|
+
dependency on the compiled tree, and adopting the lock module is its own change. Residual,
|
|
534
|
+
stated in docs/features/code-review-pipeline.md; not reproduced as a lost update.
|
|
535
|
+
|
|
536
|
+
```bash
|
|
537
|
+
# Each fenced block runs in a FRESH shell, so block 1's PADDED/REVIEW_FILE/DISPOSITION_FILE are NOT
|
|
538
|
+
# live here — re-derive them from the two inputs this step consumes (`PHASE_DIR`, `PHASE_NUMBER`).
|
|
539
|
+
# Inheriting them is not merely stale, it is EMPTY, and the failure is silent rather than loud:
|
|
540
|
+
# the embedded script throws on reading the empty review path, the trailing `|| echo` swallows it
|
|
541
|
+
# as a non-blocking skip, and no ledger is written at all. The shim preamble below is re-emitted
|
|
542
|
+
# for the same reason, and these three belong beside it.
|
|
543
|
+
# PADDED must survive a DOTTED phase number, of ANY segment count. This step is dispatched from
|
|
544
|
+
# exactly TWO places: `execute-phase.md` (`code_review_gate`) and `code-review-fix.md`
|
|
545
|
+
# (`record_disposition`). Only the second validates anything -- `code-review-fix.md`'s PADDED_PHASE validator anchors
|
|
546
|
+
# `^[0-9]+[A-Z]?(\.[0-9]+)*$`, an unbounded `*` widened by #4568 and a letter axis widened by #4744, so it accepts `03.1`, `23.1.2` AND `12A`.
|
|
547
|
+
# `execute-phase.md` applies NO shape gate at all, so this fence is not mirroring an upstream
|
|
548
|
+
# guarantee; it IS the guarantee. (`code-review.md`'s own PADDED_PHASE validator is identical but never
|
|
549
|
+
# dispatches this step. It was cited here as a caller for several rounds and is not one.) And
|
|
550
|
+
# `printf "%02d"` cannot format one: bash prints `invalid number` and exits 1, which under
|
|
551
|
+
# `set -euo pipefail` aborts this step on its FIRST line -- the loudest possible failure from
|
|
552
|
+
# the gate that promises never to block, and it takes the whole phase's review reporting with
|
|
553
|
+
# it. Pad the integer part only and carry the sub-number verbatim, so 3.1 -> 03.1, 23.1.2 ->
|
|
554
|
+
# 23.1.2 and 3 -> 03. The segment count is deliberately NOT bounded here: the canonical
|
|
555
|
+
# grammar in src/phase-id.cts (`PHASE_NUMBER_TOKEN_SOURCE`, #2128) is unbounded in segments,
|
|
556
|
+
# and #4568 widened the one dispatcher that validates to match it on that axis, so a guard
|
|
557
|
+
# narrower than that dispatcher means no ledger for a phase id the dispatcher already accepted.
|
|
558
|
+
# On failure NO path is built and the fence refuses by name: advisory means advisory, and it
|
|
559
|
+
# also means never probing a path assembled out of a value we just rejected.
|
|
560
|
+
# VALIDATE, THEN FORMAT -- never format and fall back on failure. `printf "%02d" abc` writes
|
|
561
|
+
# `00` to stdout BEFORE it fails, so a `$(printf ... || printf %s ...)` fallback CONCATENATES
|
|
562
|
+
# the two and yields `00abc`; `08` fails the same way as invalid octal, giving `0008.1` for a
|
|
563
|
+
# legitimate `08.1`. Both were driven. `${PHASE_NUMBER:-}` because an UNSET input must not trip
|
|
564
|
+
# `set -u` in a step that promises not to abort. Those printf failures are why this block VALIDATES
|
|
565
|
+
# instead of formatting; the pad itself performs NO arithmetic since round 14 (see the
|
|
566
|
+
# `case "${#_dig}"` line below), so it has no octal hazard to guard and needs no `10#`. `10#`
|
|
567
|
+
# survives in this step only where it still belongs -- on the severity COUNTS, which really are
|
|
568
|
+
# numbers being added.
|
|
569
|
+
# VALIDATE THE WHOLE VALUE, then format -- and on failure build NO path at all.
|
|
570
|
+
# Carrying an unusable value verbatim was the first draft and it was worse than the bug it
|
|
571
|
+
# replaced: PHASE_NUMBER is interpolated into a file path, so `../../etc/passwd` produced
|
|
572
|
+
# `${PHASE_DIR}/../../etc/passwd-REVIEW.md`, where the old `printf "%02d"` had at least
|
|
573
|
+
# mangled it to `00`. `code-review-fix.md`'s PADDED_PHASE validator already checks `^[0-9]+[A-Z]?(\.[0-9]+)*$` against its
|
|
574
|
+
# own PADDED_PHASE -- the padded form, not the raw PHASE_NUMBER this step is handed -- while
|
|
575
|
+
# `execute-phase.md` validates nothing at all; this step has two call sites and validates for
|
|
576
|
+
# itself rather than trusting either. Anything else yields an EMPTY PADDED and the blocks
|
|
577
|
+
# below refuse to build a path from it.
|
|
578
|
+
# PHASE_DIR is checked for NON-EMPTINESS ONLY. Both inputs come from the caller's init query, so
|
|
579
|
+
# neither is raw user input; only PHASE_NUMBER has a SHAPE (`^[0-9]+[A-Z]?(\.[0-9]+)*$`) to check
|
|
580
|
+
# against. A filesystem path admits `..` and symlinked parents alike, so a shape
|
|
581
|
+
# check here rejects working setups and proves nothing. Residual: PHASE_DIR may itself be a symlink
|
|
582
|
+
# and the ledger is written through it -- left alone, and not a security boundary.
|
|
583
|
+
_pd="${PHASE_DIR:-}"
|
|
584
|
+
_pn="${PHASE_NUMBER:-}"
|
|
585
|
+
_ok=1
|
|
586
|
+
[ -n "$_pd" ] || _ok=0
|
|
587
|
+
case "$_pn" in
|
|
588
|
+
''|*[!0-9.A-Z]*) _ok=0 ;; # empty, or any character outside [0-9.A-Z] -- this is the traversal fence
|
|
589
|
+
.*|*.) _ok=0 ;; # leading or trailing dot
|
|
590
|
+
*..*) _ok=0 ;; # EMPTY SEGMENT. The three arms plus the letter-axis block below
|
|
591
|
+
# accept exactly digits[LETTER](.digits)* -- byte-congruent with the
|
|
592
|
+
# callers' ^[0-9]+[A-Z]?(\.[0-9]+)*$ -- rather than merely wider than
|
|
593
|
+
# the retired `*.*.*` arity bound, which masked `1..2` by accident.
|
|
594
|
+
esac
|
|
595
|
+
# THE LETTER AXIS. The canonical grammar (src/phase-id.cts) is digits, an OPTIONAL single uppercase
|
|
596
|
+
# letter, then dotted digit segments -- `12A`, `3A`, `23A.1.2`. #4744 (#4660) widened the six
|
|
597
|
+
# shell/markdown mirrors to it after this branch was cut, and its lint ratchet then flagged this
|
|
598
|
+
# step as the one letterless mirror left. The character class above admits the letter; these
|
|
599
|
+
# arms pin WHERE it may sit -- only as the last character of the integer part, at most once --
|
|
600
|
+
# so `23a`, `A23`, `2A3`, `23AB` and `23.1A` are all refused.
|
|
601
|
+
if [ "$_ok" = "1" ]; then
|
|
602
|
+
_int="${_pn%%.*}"
|
|
603
|
+
case "$_pn" in *.*) _sub=".${_pn#*.}" ;; *) _sub="" ;; esac
|
|
604
|
+
_let="${_int##*[0-9]}" # what trails the last digit: '' or the letter
|
|
605
|
+
_dig="${_int%"$_let"}"
|
|
606
|
+
case "$_dig" in ''|*[!0-9]*) _ok=0 ;; esac # the integer part must be digits first
|
|
607
|
+
case "$_let" in ''|[A-Z]) ;; *) _ok=0 ;; esac # at most ONE letter, uppercase
|
|
608
|
+
case "$_sub" in *[!0-9.]*) _ok=0 ;; esac # no letter in any later segment
|
|
609
|
+
fi
|
|
610
|
+
# LENGTH-BOUND EACH COMPONENT SEPARATELY -- and the REASON changed at round 14, so read this rather
|
|
611
|
+
# than inherit it. It used to be integer overflow: the pad ran `$((10#$_int))`, bash integers wrap at
|
|
612
|
+
# 2^64, and a 54-digit value yielded -7908320945662590977 SILENTLY as the padded phase. The pad is a
|
|
613
|
+
# string pad now and converts nothing, so that overflow is unreachable and its rationale is dead.
|
|
614
|
+
# WHAT THE BOUND STILL DOES, stated narrowly because the obvious wider claim is FALSE: it bounds each
|
|
615
|
+
# SEGMENT, and NOTHING here bounds the COMPOSITE. Every segment is joined into ONE filename
|
|
616
|
+
# component and depth is unbounded, so a per-segment bound does not enforce a filename limit --
|
|
617
|
+
# driven at round 14: thirty 8-digit segments yield a 278-character PADDED and a 294-character name
|
|
618
|
+
# against a NAME_MAX of 255. That is a real residual of this validator, it predates the pad change,
|
|
619
|
+
# and it is named here rather than papered over with a filesystem rationale the bound does not
|
|
620
|
+
# deliver. The bound belongs on the INTEGER PART: applied to the whole value it rejected
|
|
621
|
+
# `12345678.1`, whose integer part is a legal 8 digits, while accepting `1.123456` -- an accidental
|
|
622
|
+
# bound on the composite that was both too strict and too loose. Every later segment is bounded too,
|
|
623
|
+
# on the same narrow reading.
|
|
624
|
+
# THE LOOP IS THE POINT, and it is what makes the heading above TRUE. The earlier form bounded
|
|
625
|
+
# `${_pn#*.}` -- the WHOLE tail after the first dot -- which is one component only while the id
|
|
626
|
+
# has at most two. Once N-segment ids are accepted (see the shape arms), that form rejects
|
|
627
|
+
# `1.1234567.1`, whose every component is a legal 7-or-fewer digits, purely because the tail
|
|
628
|
+
# measures 9 characters. That is the composite bound this comment already called "too strict",
|
|
629
|
+
# surviving one level up. Walk the segments instead, so the rule is per-component in fact and
|
|
630
|
+
# not only in the heading. The loop terminates on any string the shape arms admit: each pass
|
|
631
|
+
# strips a leading `<seg>.`, and the no-dot pass clears $_rest.
|
|
632
|
+
if [ "$_ok" = "1" ]; then
|
|
633
|
+
_rest="$_pn"
|
|
634
|
+
while [ -n "$_rest" ]; do
|
|
635
|
+
case "$_rest" in
|
|
636
|
+
*.*) _seg="${_rest%%.*}"; _rest="${_rest#*.}" ;;
|
|
637
|
+
*) _seg="$_rest"; _rest="" ;;
|
|
638
|
+
esac
|
|
639
|
+
# The bound is on the DIGITS: a letter suffix is one character the digit bound has no
|
|
640
|
+
# stake in, so `12345678A` is within it exactly as `12345678` is.
|
|
641
|
+
case "${_seg%[A-Z]}" in ?????????*) _ok=0 ;; esac
|
|
642
|
+
done
|
|
643
|
+
fi
|
|
644
|
+
if [ "$_ok" = "1" ]; then
|
|
645
|
+
# padStart(2,'0'), EXACTLY, and as a STRING -- the canonical normalizer left-pads the digit run to a
|
|
646
|
+
# MINIMUM of two and otherwise preserves it, so arithmetic is the wrong tool. `printf "%02d"
|
|
647
|
+
# "$((10#$_dig))"` agreed on `8`/`08`/`09` and silently DISAGREED on every longer leading-zero run:
|
|
648
|
+
# `008` -> `08`, `0008A` -> `08A`, resolving a REVIEW.md path init never writes. Driven at round 14
|
|
649
|
+
# by the property that asserts agreement with `normalizePhaseName` over generated ids; the 13-shape
|
|
650
|
+
# matrix that preceded it sampled no run longer than two and could not see it. Dropping the
|
|
651
|
+
# arithmetic also RETIRES the octal hazard `10#` existed to work around, rather than guarding it.
|
|
652
|
+
# The letter and any dot segments ride along verbatim, as the canonical padder does: `3A` -> `03A`.
|
|
653
|
+
case "${#_dig}" in 1) PADDED="0${_dig}${_let}${_sub}" ;; *) PADDED="${_dig}${_let}${_sub}" ;; esac
|
|
654
|
+
else
|
|
655
|
+
PADDED=""
|
|
656
|
+
fi
|
|
657
|
+
# REFUSE BEFORE BUILDING ANY PATH. An unusable input yields an empty PADDED above, and the
|
|
658
|
+
# earlier placement -- after the assignments -- meant a rejected value still had
|
|
659
|
+
# `${_pd}/-REVIEW.md` assembled and stat'ed before the refusal fired. Nothing is constructed
|
|
660
|
+
# from a value we have already rejected.
|
|
661
|
+
if [ -z "$PADDED" ]; then
|
|
662
|
+
echo "Code review disposition skipped (unusable phase number or directory: '${PHASE_NUMBER:-}')"
|
|
663
|
+
return 0 2>/dev/null || exit 0
|
|
664
|
+
fi
|
|
665
|
+
REVIEW_FILE="${_pd}/${PADDED}-REVIEW.md"
|
|
666
|
+
DISPOSITION_FILE="${_pd}/${PADDED}-REVIEW-DISPOSITION.md"
|
|
667
|
+
# The condition stated above this block is re-derived HERE rather than left to the reader. Block 1
|
|
668
|
+
# computes REVIEW_STATUS and emits nothing, and its shell is gone, so nothing downstream can act on
|
|
669
|
+
# it: a prose-only gate on a value no later block can see is not a gate. Without this, a clean
|
|
670
|
+
# re-review rewrites an existing ledger it was never meant to touch.
|
|
671
|
+
REVIEW_STATUS=""
|
|
672
|
+
REVIEW_TOTAL=""
|
|
673
|
+
REVIEW_READ=0 # block 1's distinction, re-derived here: read-but-unparseable is not absent
|
|
674
|
+
if [ -f "$REVIEW_FILE" ] && [ -r "$REVIEW_FILE" ]; then
|
|
675
|
+
REVIEW_READ=1
|
|
676
|
+
_FM=$(tr -d '\r' < "$REVIEW_FILE" 2>/dev/null | LC_ALL=C awk 'NR==1{if($0!="---") exit; next} /^---$/{closed=1; exit} {buf = buf $0 "\n"} END{if (closed) printf "%s", buf}' || true)
|
|
677
|
+
REVIEW_STATUS=$(echo "$_FM" | LC_ALL=C grep -m1 "^status:" | cut -d: -f2- | LC_ALL=C sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//' || true)
|
|
678
|
+
# The frontmatter total is carried into the script so the two parsers in this step can be
|
|
679
|
+
# RECONCILED. The counts come from the frontmatter; the rows come from `### <ID>:` heading
|
|
680
|
+
# matches against a closed CR|BL|WR|IN alternation. They are two independent numbers produced
|
|
681
|
+
# one paragraph apart, and nothing compared them: a finding the heading parser cannot match
|
|
682
|
+
# contributed no row, no note and no diagnostic, and the ledger then asserted `open: 3 of 3`
|
|
683
|
+
# over a set strictly smaller than the console line had just reported. Anchored inside the
|
|
684
|
+
# `findings:` mapping — see the anchoring note in block 1 — and digit-only, because a
|
|
685
|
+
# non-numeric total is not a number to reconcile against.
|
|
686
|
+
_FINDINGS_FM=$(echo "$_FM" | LC_ALL=C awk '/^findings:[[:space:]]*$/{f=1; next} f&&/^[^[:space:]]/{exit} f' || true)
|
|
687
|
+
# ONE PARSER FOR EVERY COUNT THIS BLOCK READS, and both halves of it are load-bearing.
|
|
688
|
+
# `-f2-` keeps everything AFTER the first colon: `-f2` alone takes only the SECOND FIELD, so the
|
|
689
|
+
# malformed `critical: 1: junk` arrives as the perfectly numeric `1`. And the ends are trimmed
|
|
690
|
+
# rather than `tr -d ' '`-ed, which would delete INTERNAL spaces and turn `1 0` into `10`.
|
|
691
|
+
# Both quirks are long-standing in the sibling reads and both were INERT here until this block
|
|
692
|
+
# began reconciling; each one repairs a malformed scalar into a number that then decides whether a
|
|
693
|
+
# shortfall is reported. An adversarial pass drove both: `critical: 1: junk` wrongly suppressed a
|
|
694
|
+
# real `unparsed: 2`, and a LENIENT total beside a STRICT severity was worse still -- `critical: 5 0`
|
|
695
|
+
# with `total: 1 0` repaired only the total, rejected the severity, skipped the contradiction check
|
|
696
|
+
# and INVENTED `unparsed: 7`. A field is either trustworthy or it is not; parsing one leniently and
|
|
697
|
+
# its sibling strictly is the shape that fabricates.
|
|
698
|
+
REVIEW_TOTAL=$(echo "$_FINDINGS_FM" | LC_ALL=C grep -E -m1 "^[[:space:]]*total:" | cut -d: -f2- | LC_ALL=C sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//' || true)
|
|
699
|
+
case "$REVIEW_TOTAL" in ''|*[!0-9]*) REVIEW_TOTAL="" ;; ?????????*) REVIEW_TOTAL="" ;; esac
|
|
700
|
+
# ONE FIELD, ONE TRUST MODEL, ACROSS BOTH FENCES. Block 1 withholds the whole breakdown unless the
|
|
701
|
+
# four counts are numeric AND `critical + warning + info == total`; this fence used to bound `total`
|
|
702
|
+
# for digits and length only and then hand it to the `unparsed:` reconciliation, so a REVIEW.md whose
|
|
703
|
+
# `findings:` block is internally inconsistent (`total: 10` beside `critical: 1, warning: 1, info: 1`)
|
|
704
|
+
# made block 1 print the countless form -- breakdown suppressed as untrustworthy -- while this block
|
|
705
|
+
# still computed an `unparsed:` shortfall from that same untrusted number. It fails in the SAFE
|
|
706
|
+
# direction (over-reports a possible gap rather than hiding one), which is why it is not a blocker;
|
|
707
|
+
# it is still two trust models for one field, one fence apart, and the weaker one is downstream.
|
|
708
|
+
# `blocker:` is the documented tier-equivalent of `critical:` (gsd-code-reviewer.md 'Label
|
|
709
|
+
# equivalence') -- the same alternation block 1 reads, because a mirror that drops it would diverge
|
|
710
|
+
# on exactly the reviews that use it.
|
|
711
|
+
# TRIM THE ENDS, NEVER `tr -d ' '`, AND THE DIFFERENCE DECIDES A SUPPRESSION. `tr -d` deletes
|
|
712
|
+
# INTERNAL spaces too, so a malformed `critical: 1 0` would arrive as the perfectly numeric `10`.
|
|
713
|
+
# Every read in this step now takes the end-trim instead -- the sibling reads were moved off `tr -d`
|
|
714
|
+
# in the same round, so this is no longer a divergence between blocks -- and the reason it matters
|
|
715
|
+
# HERE is that a value which LOOKS numeric can satisfy the sum test and SUPPRESS a real
|
|
716
|
+
# `unparsed:` shortfall. Suppression is the new
|
|
717
|
+
# behaviour, so the admission test for it is strict -- an internal space survives the trim, fails the
|
|
718
|
+
# digit `case` below, and the shortfall is reported. Fail-safe in the only direction that matters:
|
|
719
|
+
# when the frontmatter is malformed we decline to suppress, rather than trusting a repaired number.
|
|
720
|
+
_c_crit=$(echo "$_FINDINGS_FM" | LC_ALL=C grep -E -m1 "^[[:space:]]*(critical|blocker):" | cut -d: -f2- | LC_ALL=C sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//' || true)
|
|
721
|
+
_c_warn=$(echo "$_FINDINGS_FM" | LC_ALL=C grep -E -m1 "^[[:space:]]*warning:" | cut -d: -f2- | LC_ALL=C sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//' || true)
|
|
722
|
+
_c_info=$(echo "$_FINDINGS_FM" | LC_ALL=C grep -E -m1 "^[[:space:]]*info:" | cut -d: -f2- | LC_ALL=C sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//' || true)
|
|
723
|
+
# THE CHECK IS NARROWER THAN BLOCK 1'S, DELIBERATELY, AND THE DIFFERENCE IS NOT AN OVERSIGHT.
|
|
724
|
+
# Block 1 withholds on `REVIEW_COUNTS_OK`, which demands ALL FOUR counts be numeric -- because it
|
|
725
|
+
# DISPLAYS all four, and `6 findings -- critical` is the half-filled line that rule exists to
|
|
726
|
+
# prevent. This fence displays none of them: it uses `total` alone, to reconcile against the number
|
|
727
|
+
# of headings the row parser matched. So the all-four rule does not port. Applied verbatim here it
|
|
728
|
+
# would blank `total` on a REVIEW.md carrying `total: 5` and no severity keys -- a review whose total
|
|
729
|
+
# is perfectly usable -- and SILENTLY DROP an `unparsed:` shortfall this step reports correctly today.
|
|
730
|
+
# That trades a safe-direction over-report for a silent under-report, which is the wrong way round
|
|
731
|
+
# and is the exact failure class the `unparsed:` key was added to close.
|
|
732
|
+
# What DOES port is the CONTRADICTION: when the three severities are all present and numeric and do
|
|
733
|
+
# not sum to `total`, the frontmatter disagrees with itself and `total` is not a number to reconcile
|
|
734
|
+
# against. Block 1 already suppresses its breakdown on that input; this fence now declines to compute
|
|
735
|
+
# a shortfall from it. Absent counts are not a contradiction -- there is nothing to disagree.
|
|
736
|
+
# AN ABSENT SEVERITY STILL BOUNDS THE SUM FROM BELOW, and that is enough to prove a contradiction
|
|
737
|
+
# in one direction. Counts are non-negative, so a missing one can only ADD: if the severities that
|
|
738
|
+
# ARE present and numeric already sum to MORE than `total`, the block disagrees with itself whatever
|
|
739
|
+
# the missing value is. Requiring all three before comparing missed that -- driven by an adversarial
|
|
740
|
+
# pass: `critical: 4`, `warning: 4`, no `info:`, `total: 5` reconciled against a total the present
|
|
741
|
+
# counts had already refuted. So the comparison is two-armed: EQUALITY when all three are known,
|
|
742
|
+
# and a LOWER BOUND when they are not. `_p_sum` accumulates only the present-and-numeric ones.
|
|
743
|
+
_sum_ok=1; _p_sum=0
|
|
744
|
+
for _c in "$_c_crit" "$_c_warn" "$_c_info"; do
|
|
745
|
+
# Present AND numeric AND within the same length bound the total carries -- `10#` below needs
|
|
746
|
+
# digits, and bash integers wrap at 2^64.
|
|
747
|
+
case "$_c" in
|
|
748
|
+
''|*[!0-9]*) _sum_ok=0 ;;
|
|
749
|
+
?????????*) _sum_ok=0 ;;
|
|
750
|
+
*) _p_sum=$(( _p_sum + 10#$_c )) ;;
|
|
751
|
+
esac
|
|
752
|
+
done
|
|
753
|
+
# `10#` on every operand, for block 1's reason: bash infers the base from a leading zero, so
|
|
754
|
+
# `critical: 08` makes $(( )) fail with "value too great for base" and, under `set -e`, takes the
|
|
755
|
+
# whole advisory step down -- strictly worse than the stale count this check exists to prevent.
|
|
756
|
+
if [ -n "$REVIEW_TOTAL" ]; then
|
|
757
|
+
_t=$(( 10#$REVIEW_TOTAL ))
|
|
758
|
+
if [ "$_sum_ok" = "1" ]; then
|
|
759
|
+
# All three known: the sum must match exactly.
|
|
760
|
+
if [ "$_p_sum" -ne "$_t" ]; then REVIEW_TOTAL=""; fi
|
|
761
|
+
elif [ "$_p_sum" -gt "$_t" ]; then
|
|
762
|
+
# Not all known: only an OVERSHOOT is provable. An undershoot is the absent count's job.
|
|
763
|
+
REVIEW_TOTAL=""
|
|
764
|
+
fi
|
|
765
|
+
fi
|
|
766
|
+
fi
|
|
767
|
+
# Skip a clean/skipped/absent review ONLY when there is nothing to reconcile AT ALL. An EXISTING
|
|
768
|
+
# ledger is still brought up to date -- freezing it would leave findings showing open that the
|
|
769
|
+
# review no longer reports, and an unconditional skip would make the reconciliation path
|
|
770
|
+
# unreachable on exactly the run that needs it.
|
|
771
|
+
# A FIX REPORT IS THE SECOND REASON TO PROCEED: a direct `/gsd:code-review N --auto` writes no gate
|
|
772
|
+
# ledger and a converged loop leaves `status: clean`, so a fully fixed phase recorded nothing.
|
|
773
|
+
_fix_any=0
|
|
774
|
+
[ -f "${_pd}/${PADDED}-REVIEW-FIX.md" ] && _fix_any=1
|
|
775
|
+
# Backups count too -- a converged loop's earlier iterations live only there. An unmatched glob
|
|
776
|
+
# expands to the literal pattern, which `-f` rejects.
|
|
777
|
+
# $(printf '%s' "$PADDED") per lint-workflow-shellcheck's #4109 remedy: a bare $VAR in a `for x in`
|
|
778
|
+
# splits differently under bash and zsh.
|
|
779
|
+
for _f in "${_pd}/$(printf '%s' "$PADDED")-REVIEW-FIX.iter"*.md; do [ -f "$_f" ] && _fix_any=1; done
|
|
780
|
+
# The word for an empty status names WHICH empty it is, for the same reason block 1 does: a
|
|
781
|
+
# review that was read and could not be parsed is 'unparsed', an absent one is 'none'.
|
|
782
|
+
_st="${REVIEW_STATUS:-none}"; [ -z "$REVIEW_STATUS" ] && [ "$REVIEW_READ" = "1" ] && _st="unparsed"
|
|
783
|
+
case "$REVIEW_STATUS" in
|
|
784
|
+
''|clean|skipped)
|
|
785
|
+
if [ ! -f "$DISPOSITION_FILE" ] && [ "$_fix_any" = "0" ]; then
|
|
786
|
+
echo "Code review disposition skipped (status: ${_st})"
|
|
787
|
+
return 0 2>/dev/null || exit 0
|
|
788
|
+
fi
|
|
789
|
+
echo "Code review status ${_st}; reconciling the fix report and any existing disposition ledger."
|
|
790
|
+
;;
|
|
791
|
+
esac
|
|
792
|
+
_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; _gsd_at() { for _p; do if [ -f "$_p" ]; then GSD_TOOLS="$_p"; return 0; fi; done; return 1; }; _gsd_id_ok() { case "$("$1" runtime-identity --raw 2>/dev/null || true)" in '{"packageName":"@opengsd/gsd-core"'*'}') return 0;; *) return 1;; esac; }; _gsd_homes() { set -- "${CLAUDE_CONFIG_DIR:-$HOME/.claude}" "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}" "$HOME/.gemini/antigravity-ide" "$HOME/.gemini/antigravity-cli" "${AUGMENT_CONFIG_DIR:-$HOME/.augment}" "${CLINE_CONFIG_DIR:-$HOME/.cline}" "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}" "${CODEX_HOME:-$HOME/.codex}" "${COPILOT_CONFIG_DIR:-${COPILOT_HOME:-$HOME/.copilot}}" "${CURSOR_CONFIG_DIR:-$HOME/.cursor}" "${HERMES_HOME:-$HOME/.hermes}" "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}" "${KIMI_CONFIG_DIR:-$HOME/.config/agents}" "$HOME/.agents" "${KIMI_CODE_HOME:-$HOME/.kimi-code}" "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}" "${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}" "${QWEN_CONFIG_DIR:-$HOME/.qwen}" "${TRAE_CONFIG_DIR:-$HOME/.trae}" "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}" "${ZCODE_CONFIG_DIR:-$HOME/.zcode}" "${GROK_AGENTS_HOME:-$HOME/.agents}"; for _h; do _gsd_at "$_h/gsd-core/bin/${_GSD_SHIM_NAME}" && return 0; done; return 1; }; if _gsd_at "${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}" "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif _gsd_homes; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif unset -f gsd_run; _G="$(command -v gsd_run)"; [ -n "$_G" ] && _gsd_id_ok "$_G"; then GSD_TOOLS="$_G"; gsd_run() { "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and no identity-proving gsd_run is on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; GSD_IDENTITY_STATUS=unverified; _gsd_id_ok gsd_run && GSD_IDENTITY_STATUS=ok; export GSD_IDENTITY_STATUS; [ "$GSD_IDENTITY_STATUS" = ok ] || echo "WARNING: \"$GSD_TOOLS\" did not prove it is @opengsd/gsd-core - it is either a different package or an @opengsd/gsd-core older than the runtime-identity verb. See docs/how-to/diagnose-a-foreign-gsd-tools.md" >&2; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi
|
|
793
|
+
# Built before the command for READABILITY, not as a fix. ShellCheck's SC2097/SC2098 here is a FALSE
|
|
794
|
+
# POSITIVE: prefix assignments take effect left to right (driven, bash and dash).
|
|
795
|
+
FIX_REPORT_FILE="${_pd}/${PADDED}-REVIEW-FIX.md"
|
|
796
|
+
REVIEW_FILE="${REVIEW_FILE}" DISPOSITION_FILE="${DISPOSITION_FILE}" PADDED="${PADDED}" \
|
|
797
|
+
REVIEW_TOTAL="${REVIEW_TOTAL}" \
|
|
798
|
+
FIX_REPORT_FILE="${FIX_REPORT_FILE}" node -e "
|
|
799
|
+
(function main() {
|
|
800
|
+
const fs = require('fs'), path = require('path');
|
|
801
|
+
const norm = (s) => s.replace(/\r\n/g, '\n');
|
|
802
|
+
if (fs.existsSync(process.env.DISPOSITION_FILE) && !fs.lstatSync(process.env.DISPOSITION_FILE).isFile()) {
|
|
803
|
+
console.log('Code review disposition skipped: ' + process.env.DISPOSITION_FILE + ' exists and is not a regular file; refusing to read or write through it.');
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
// Captures the id AND the title: the title is what tells a stale fix report apart from a
|
|
807
|
+
// current one, because finding ids are reused across re-reviews.
|
|
808
|
+
const ID_RE = /^###\s+((?:CR|BL|WR|IN)-\d+)\s*:\s*(.*)\$/;
|
|
809
|
+
// BL- is Critical-tier-equivalent to CR- (gsd-code-reviewer.md 'Label equivalence').
|
|
810
|
+
const sectionSev = new Map();
|
|
811
|
+
// PREFIX severity -- the LAST resort, an inference from the id alone. Used only when neither
|
|
812
|
+
// the current review's section nor a severity this ledger already RECORDED is available; the
|
|
813
|
+
// precedence and the reason for it are stated at sev(), defined below once identity is known.
|
|
814
|
+
const prefixSev = (id) => ({ CR: 'critical', BL: 'critical', WR: 'warning' }[id.split('-')[0]] || 'info');
|
|
815
|
+
const headings = (text) => {
|
|
816
|
+
// Fenced blocks are skipped: review and fix bodies quote example findings, and a heading
|
|
817
|
+
// inside a fence is an illustration, not a finding.
|
|
818
|
+
const out = [];
|
|
819
|
+
let fence = null;
|
|
820
|
+
for (const l of norm(text).split('\n')) {
|
|
821
|
+
const f = l.match(/^ {0,3}(\`{3,}|~{3,})/); // >3 spaces is an indented block, not a fence
|
|
822
|
+
if (f) {
|
|
823
|
+
const ch = f[1][0], len = f[1].length;
|
|
824
|
+
if (!fence) { fence = { ch: ch, len: len }; out.push({ fence: true }); continue; }
|
|
825
|
+
// A CLOSER carries nothing but whitespace after the marker; an info string makes it
|
|
826
|
+
// an opener's shape, never a close.
|
|
827
|
+
if (ch === fence.ch && len >= fence.len && /^\s*\$/.test(l.slice(l.indexOf(f[1]) + f[1].length))) {
|
|
828
|
+
fence = null; out.push({ fence: true }); continue;
|
|
829
|
+
}
|
|
830
|
+
out.push({ skip: true, line: l }); continue; // a foreign marker inside a fence is content
|
|
831
|
+
}
|
|
832
|
+
if (fence) { out.push({ skip: true, line: l }); continue; }
|
|
833
|
+
const m = l.match(ID_RE);
|
|
834
|
+
out.push(m ? { id: m[1], title: m[2].trim(), line: l } : { line: l });
|
|
835
|
+
}
|
|
836
|
+
return out;
|
|
837
|
+
};
|
|
838
|
+
const order = [], title = new Map();
|
|
839
|
+
// An ABSENT review still has a ledger to reconcile: the step's own guard proceeds when
|
|
840
|
+
// one exists, and throwing here would send that run to the trailing non-blocking fallback
|
|
841
|
+
// with the
|
|
842
|
+
// ledger untouched -- the freeze the reconciliation path exists to prevent.
|
|
843
|
+
const reviewText = fs.existsSync(process.env.REVIEW_FILE) ? fs.readFileSync(process.env.REVIEW_FILE, 'utf-8') : '';
|
|
844
|
+
const SECTION_SEV = [[/^##\s+Critical Issues\s*\$/, 'critical'], [/^##\s+Warnings\s*\$/, 'warning'], [/^##\s+Info\s*\$/, 'info']];
|
|
845
|
+
let curSection = null;
|
|
846
|
+
for (const h of headings(reviewText)) {
|
|
847
|
+
if (h.fence || h.skip) continue;
|
|
848
|
+
if (h.line !== undefined && /^##\s+/.test(h.line)) {
|
|
849
|
+
const hit = SECTION_SEV.find(([re]) => re.test(h.line));
|
|
850
|
+
curSection = hit ? hit[1] : null; // an unrecognized ## section falls back to the prefix
|
|
851
|
+
}
|
|
852
|
+
if (h.id && order.indexOf(h.id) === -1) {
|
|
853
|
+
order.push(h.id); title.set(h.id, h.title);
|
|
854
|
+
if (curSection) sectionSev.set(h.id, curSection);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
// THE FIX REPORTS THIS RUN MAY RECONCILE AGAINST. --auto overwrites REVIEW-FIX.md each iteration
|
|
858
|
+
// and the re-review drops what was fixed, so an iteration-1 fix is in NEITHER final artifact.
|
|
859
|
+
// Read the backups too, newest first.
|
|
860
|
+
const FIX_FINAL = process.env.FIX_REPORT_FILE;
|
|
861
|
+
const fixStem = path.basename(FIX_FINAL).slice(0, -3); // '<NN>-REVIEW-FIX'
|
|
862
|
+
const iterMarker = fixStem + '.iter';
|
|
863
|
+
// String ops, not a built RegExp: every backslash is one more thing bash rewrites first.
|
|
864
|
+
// ONE expression, no 'return': ShellCheck lints this fence as shell and would mark the rest
|
|
865
|
+
// unreachable (SC2317) against a ratchet baseline.
|
|
866
|
+
const iterDigits = (n) => (n.indexOf(iterMarker) === 0 && n.slice(-3) === '.md') ? n.slice(iterMarker.length, -3) : '';
|
|
867
|
+
const iterOf = (n) => /^[0-9]+\$/.test(iterDigits(n)) ? Number(iterDigits(n)) : null;
|
|
868
|
+
const fixReports = [];
|
|
869
|
+
if (fs.existsSync(FIX_FINAL)) fixReports.push(FIX_FINAL);
|
|
870
|
+
let iterFiles = [];
|
|
871
|
+
// Guarded: the phase directory is not guaranteed readable, and this step never aborts.
|
|
872
|
+
try { iterFiles = fs.readdirSync(path.dirname(FIX_FINAL)).map((n) => [iterOf(n), n]).filter((e) => e[0] !== null); } catch (e) { iterFiles = []; }
|
|
873
|
+
iterFiles.sort((a, b) => b[0] - a[0]);
|
|
874
|
+
for (const e of iterFiles) fixReports.push(path.join(path.dirname(FIX_FINAL), e[1]));
|
|
875
|
+
const declaredTotal = /^[0-9]+\$/.test(process.env.REVIEW_TOTAL || '') ? Number(process.env.REVIEW_TOTAL) : null;
|
|
876
|
+
// Against order.length -- the CURRENT review's findings -- never rows.length, which also counts
|
|
877
|
+
// rows carried from earlier reviews and would understate the shortfall or invent one.
|
|
878
|
+
const unparsed = declaredTotal !== null && declaredTotal > order.length ? declaredTotal - order.length : 0;
|
|
879
|
+
const unparsedNote = unparsed ? ' (' + unparsed + ' finding(s) recorded NOWHERE: the review reports ' + declaredTotal + ', but only ' + order.length + ' matched the expected heading shape \`### <CR|BL|WR|IN>-NN: <title>\`)' : '';
|
|
880
|
+
if (order.length === 0 && !unparsed && !fs.existsSync(process.env.DISPOSITION_FILE) && fixReports.length === 0) return;
|
|
881
|
+
const prior = new Map();
|
|
882
|
+
// TITLES, IN THE FRONTMATTER. Ids are reused across re-reviews (--auto renumbers), so an id alone
|
|
883
|
+
// does not identify a finding: driven, a prior 'CR-01 fixed' rendered a brand-new CR-01 'fixed'.
|
|
884
|
+
// Not a fifth table column -- the Source cell is already the hand-edited, pipe-escaping one.
|
|
885
|
+
const priorTitle = new Map();
|
|
886
|
+
const SEV_VOCAB = ['critical', 'warning', 'info'];
|
|
887
|
+
const priorSev = new Map();
|
|
888
|
+
// Ids whose decision could not be carried: the id now names a DIFFERENT finding. REPORTED, not
|
|
889
|
+
// re-homed -- rows key on the id, and two under one id is an ambiguity. The note does NOT claim the
|
|
890
|
+
// old row is in git: committing is gated on commit_docs. See docs/features/code-review-pipeline.md.
|
|
891
|
+
const reused = [];
|
|
892
|
+
var _fmId = null, _fmSec = null;
|
|
893
|
+
// Set when the ledger declares JSON scalars; without it they are bare. Load-bearing: a legacy title
|
|
894
|
+
// that merely LOOKED like JSON was parsed, lost its quotes, and flipped to open.
|
|
895
|
+
var _fmJson = false;
|
|
896
|
+
if (fs.existsSync(process.env.DISPOSITION_FILE)) {
|
|
897
|
+
for (const l of norm(fs.readFileSync(process.env.DISPOSITION_FILE, 'utf-8')).split('\n')) {
|
|
898
|
+
const m = l.match(/^\|\s*((?:CR|BL|WR|IN)-\d+)\s*\|\s*([^|]*?)\s*\|\s*(open|fixed|skipped|deferred)\s*\|\s*(.*?)\s*\|?\s*\$/);
|
|
899
|
+
if (m) {
|
|
900
|
+
prior.set(m[1], { d: m[3], src: m[4].replace(/\s*\(not in the current review\)\s*\$/, '') });
|
|
901
|
+
// The table wins over the frontmatter (set unconditionally here, only-if-absent below),
|
|
902
|
+
// whichever order the two appear in the file.
|
|
903
|
+
if (SEV_VOCAB.indexOf(m[2]) !== -1) priorSev.set(m[1], m[2]);
|
|
904
|
+
}
|
|
905
|
+
// Frontmatter is walked in the same pass, as a SECTIONED list rather than by one line shape.
|
|
906
|
+
if (/^titles: json\s*\$/.test(l)) { _fmJson = true; continue; }
|
|
907
|
+
var msec = l.match(/^(findings):\s*\$/);
|
|
908
|
+
if (msec) { _fmSec = msec[1]; _fmId = null; continue; }
|
|
909
|
+
var mi = l.match(/^ - id: ((?:CR|BL|WR|IN)-\d+)\s*\$/);
|
|
910
|
+
if (mi && _fmSec) { _fmId = mi[1]; continue; }
|
|
911
|
+
// The frontmatter's own copy of the severity -- the fallback when the table cell is unusable.
|
|
912
|
+
var msv = l.match(/^ severity: (critical|warning|info)\s*\$/);
|
|
913
|
+
if (msv && _fmId && _fmSec === 'findings') { if (!priorSev.has(_fmId)) priorSev.set(_fmId, msv[1]); continue; }
|
|
914
|
+
var mkv = l.match(/^ title: (.*)\$/);
|
|
915
|
+
if (mkv && _fmId && _fmSec === 'findings') {
|
|
916
|
+
var _v = mkv[1];
|
|
917
|
+
if (_fmJson) { try { _v = JSON.parse(_v); } catch (e) { /* keep the raw scalar */ } }
|
|
918
|
+
priorTitle.set(_fmId, _v);
|
|
919
|
+
continue;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
const sameTitle = (a, b) => String(a === undefined ? '' : a).replace(/\s+/g, ' ').trim()
|
|
924
|
+
=== String(b === undefined ? '' : b).replace(/\s+/g, ' ').trim();
|
|
925
|
+
// Section headings are matched WHOLE: a prefix match would let '## Fixed Issues Verification'
|
|
926
|
+
// classify every finding under it as fixed.
|
|
927
|
+
const applied = new Map(), staleFix = [];
|
|
928
|
+
for (const fixPath of fixReports) {
|
|
929
|
+
let sect = null;
|
|
930
|
+
for (const h of headings(fs.readFileSync(fixPath, 'utf-8'))) {
|
|
931
|
+
if (h.fence || h.skip) continue;
|
|
932
|
+
if (/^##\s+Fixed Issues\s*\$/.test(h.line)) { sect = 'fixed'; continue; }
|
|
933
|
+
if (/^##\s+Skipped Issues\s*\$/.test(h.line)) { sect = 'skipped'; continue; }
|
|
934
|
+
if (/^##\s+/.test(h.line)) { sect = null; continue; }
|
|
935
|
+
if (h.id && sect && !applied.has(h.id)) {
|
|
936
|
+
// THREE ARMS. An id the review does not report has no title to disagree with -- not the
|
|
937
|
+
// stale-report case, but what a finding looks like once acted on; the old form dropped it
|
|
938
|
+
// silently. Reuse stays closed below. The record carries the originating report and title.
|
|
939
|
+
var _acted = { d: sect, src: path.basename(fixPath), t: h.title };
|
|
940
|
+
if (!title.has(h.id)) applied.set(h.id, _acted);
|
|
941
|
+
else if (sameTitle(title.get(h.id), h.title)) applied.set(h.id, _acted);
|
|
942
|
+
else if (staleFix.indexOf(h.id) === -1) staleFix.push(h.id);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
// Precedence: an applied outcome is evidence of an action on code and wins; a recorded
|
|
947
|
+
// non-'open' decision wins over the default. 'open' never overwrites a decision.
|
|
948
|
+
// Inherited only while the id names the SAME finding. An ABSENT prior title inherits: a
|
|
949
|
+
// pre-titles ledger has none, and refusing would reset every decision in it.
|
|
950
|
+
const sameFinding = (id) => !priorTitle.has(id) || !title.has(id) || sameTitle(priorTitle.get(id), title.get(id));
|
|
951
|
+
const sev = (id) => sectionSev.get(id) || (priorSev.has(id) && sameFinding(id) ? priorSev.get(id) : prefixSev(id));
|
|
952
|
+
const row = (id) => {
|
|
953
|
+
if (applied.has(id)) { const a = applied.get(id); return { id, sev: sev(id), d: a.d, src: a.src, t: title.has(id) ? title.get(id) : a.t }; }
|
|
954
|
+
const was = prior.get(id);
|
|
955
|
+
if (was && was.d !== 'open' && sameFinding(id)) return { id, sev: sev(id), d: was.d, src: was.src || 'recorded', t: title.get(id) };
|
|
956
|
+
// Reused id: the NEW finding is untriaged and renders 'open'; the prior decision loses its row,
|
|
957
|
+
// and that is REPORTED.
|
|
958
|
+
if (was && was.d !== 'open' && reused.indexOf(id + '=' + was.d) === -1) reused.push(id + '=' + was.d);
|
|
959
|
+
return { id, sev: sev(id), d: 'open', src: '-', t: title.get(id) };
|
|
960
|
+
};
|
|
961
|
+
const rows = order.map(row);
|
|
962
|
+
const carriedIds = [];
|
|
963
|
+
for (const id of prior.keys()) if (order.indexOf(id) === -1 && carriedIds.indexOf(id) === -1) carriedIds.push(id);
|
|
964
|
+
for (const id of applied.keys()) if (order.indexOf(id) === -1 && carriedIds.indexOf(id) === -1) carriedIds.push(id);
|
|
965
|
+
for (const id of carriedIds) {
|
|
966
|
+
const act = applied.get(id), was = prior.get(id);
|
|
967
|
+
const d = act ? act.d : (was ? was.d : 'open');
|
|
968
|
+
const src = act ? act.src : (was && was.src) || (d === 'open' ? '-' : 'recorded');
|
|
969
|
+
// Title precedence: the report that DECIDED it, then the prior ledger. A carried row is absent
|
|
970
|
+
// from the review, so one of those two is the only record of it.
|
|
971
|
+
// typeof, not ||: an empty title is FALSY, and the truthy fallback discarded it -- reading back
|
|
972
|
+
// as a pre-format ledger and reopening the leak.
|
|
973
|
+
const kt = act && typeof act.t === 'string' ? act.t : priorTitle.get(id);
|
|
974
|
+
rows.push({ id, sev: sev(id), d: d, src: src, t: kt, carried: true });
|
|
975
|
+
}
|
|
976
|
+
const open = rows.filter((r) => r.d === 'open').length;
|
|
977
|
+
const reusedNote = reused.length ? ' (' + reused.length + ' recorded decision(s) DROPPED -- the id now names a different finding, so the decision no longer has a row: ' + reused.join(', ') + ')' : '';
|
|
978
|
+
const staleNote = staleFix.length ? ' (' + staleFix.length + ' fix-report entr' + (staleFix.length === 1 ? 'y titles its' : 'ies title their') + ' finding differently from the review, so ' + (staleFix.length === 1 ? 'it was' : 'they were') + ' not reconciled -- a stale report, or a re-titled one: ' + staleFix.join(', ') + ')' : '';
|
|
979
|
+
if (rows.length === 0 && !unparsed && !fs.existsSync(process.env.DISPOSITION_FILE)) return;
|
|
980
|
+
const escapePipes = (t) => t.replace(/\\\\.|\|/g, (m) => (m === '|' ? '\\\\|' : m));
|
|
981
|
+
const body = ['# Phase ' + process.env.PADDED + ': Code Review Disposition', '', '| Finding | Severity | Disposition | Source |', '|---------|----------|-------------|--------|']
|
|
982
|
+
.concat(rows.map((r) => { const src = escapePipes(r.src || '-'); const mark = r.carried && !/\(not in the current review\)\s*\$/.test(src) ? ' (not in the current review)' : ''; return '| ' + r.id + ' | ' + r.sev + ' | ' + r.d + ' | ' + src + mark + ' |'; }))
|
|
983
|
+
.concat(['', 'Dispositions: \`open\` (recorded, not yet triaged), \`fixed\`, \`skipped\`, \`deferred\`.', 'Set \`deferred\` by hand and put the reason in the Source cell; both are preserved. A \`|\` in the reason is kept as prose and escaped on the next run.', 'Re-running the gate keeps every row it can. A row the current review no longer reports is kept and its Source cell flagged, so a finding does not leave this record silently. ONE exception: when a finding id is REUSED by a different finding, the earlier decision cannot keep a row — the id is taken — and it is dropped. A RECORDED decision (anything but \`open\`) is named on the console when that happens; a row still at \`open\` is replaced silently, because \`open\` records no decision to lose.', '']).join('\n');
|
|
984
|
+
// One line: the value feeds a line-oriented record a regex re-reads.
|
|
985
|
+
const oneLine = (t) => String(t === undefined || t === null ? '' : t).replace(/[\r\n]+/g, ' ').trim();
|
|
986
|
+
// JSON.stringify: YAML 1.2 is a JSON superset, so a colon, quote or leading '#' survives. The
|
|
987
|
+
// bare form emitted 'title: Parser: loses data', which a real YAML reader rejects (driven).
|
|
988
|
+
const yv = (t) => JSON.stringify(oneLine(t));
|
|
989
|
+
const head = ['---', 'phase: ' + process.env.PADDED, 'review: ' + path.basename(process.env.REVIEW_FILE), 'titles: json', 'findings:']
|
|
990
|
+
.concat(rows.map((r) => ' - id: ' + r.id + '\n severity: ' + r.sev + '\n disposition: ' + r.d
|
|
991
|
+
// Emitted whenever KNOWN, empty included ('### CR-01:'). Known-empty vs NOT
|
|
992
|
+
// KNOWN is the distinction; conflating them was a leak. Unknown stays absent.
|
|
993
|
+
+ (typeof r.t === 'string' ? '\n title: ' + yv(r.t) : '')))
|
|
994
|
+
.concat(['open: ' + open, 'total: ' + rows.length])
|
|
995
|
+
// Emitted only when there IS a shortfall, so an ordinary ledger gains no noise key and the
|
|
996
|
+
// unchanged-run check below is unaffected on every review that parses cleanly.
|
|
997
|
+
.concat(unparsed ? ['unparsed: ' + unparsed] : []).join('\n');
|
|
998
|
+
// Rewrite only on a real change. The timestamp is the one field that always differs, so
|
|
999
|
+
// stamping unconditionally would dirty the tree and produce a docs commit on every phase
|
|
1000
|
+
// re-run with nothing to report.
|
|
1001
|
+
const render = (stamp) => head + '\nrecorded: ' + stamp + '\n---\n\n' + body;
|
|
1002
|
+
const stripTs = (t) => t.replace(/^recorded:.*\$/m, 'recorded:');
|
|
1003
|
+
const prev = fs.existsSync(process.env.DISPOSITION_FILE) ? norm(fs.readFileSync(process.env.DISPOSITION_FILE, 'utf-8')) : '';
|
|
1004
|
+
if (prev && stripTs(prev) === stripTs(render(''))) {
|
|
1005
|
+
console.log('Code review disposition unchanged: ' + open + ' of ' + rows.length + ' finding(s) open' + staleNote + unparsedNote + reusedNote);
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
fs.writeFileSync(process.env.DISPOSITION_FILE, render(new Date().toISOString()));
|
|
1009
|
+
console.log('Code review disposition recorded: ' + open + ' of ' + rows.length + ' finding(s) open' + staleNote + unparsedNote + reusedNote + ' — ' + process.env.DISPOSITION_FILE);
|
|
1010
|
+
})();
|
|
1011
|
+
" || echo "Code review disposition record skipped (non-blocking)."
|
|
1012
|
+
|
|
1013
|
+
COMMIT_DOCS=$(gsd_run query config-get commit_docs --raw 2>/dev/null || echo "true")
|
|
1014
|
+
# `-f` FOLLOWS a symlink, so this could hand the commit helper a link the script above just
|
|
1015
|
+
# refused to write through -- the guard and its consumer disagreeing about the same path.
|
|
1016
|
+
if [ "$COMMIT_DOCS" = "true" ] && [ -f "${DISPOSITION_FILE}" ] && [ ! -L "${DISPOSITION_FILE}" ]; then
|
|
1017
|
+
gsd_run query commit "docs(${PADDED}): record code review disposition" --files "${DISPOSITION_FILE}" || true
|
|
1018
|
+
fi
|
|
1019
|
+
```
|