@opengsd/gsd-core 1.9.1 → 1.11.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 +2 -3
- package/.opencode/plugins/gsd-core.js +8 -1
- package/agents/gsd-code-fixer.md +27 -3
- package/agents/gsd-debug-session-manager.md +11 -0
- package/agents/gsd-debugger.md +12 -246
- package/agents/gsd-doc-synthesizer.md +2 -4
- package/agents/gsd-executor.md +12 -10
- package/agents/gsd-integration-checker.md +3 -0
- package/agents/gsd-mempalace-curator.md +5 -2
- package/agents/gsd-phase-researcher.md +20 -1
- package/agents/gsd-plan-checker.md +46 -0
- package/agents/gsd-planner.md +49 -54
- package/agents/gsd-roadmapper.md +21 -3
- package/agents/gsd-user-profiler.md +3 -0
- package/agents/gsd-verifier.md +26 -73
- package/bin/install.js +1272 -1238
- package/bin/lib/ui-safety-gate.cjs +2 -0
- package/commands/gsd/code-review.md +1 -1
- package/commands/gsd/execute-phase.md +1 -1
- package/commands/gsd/map-codebase.md +1 -1
- package/commands/gsd/mempalace-capture.md +2 -2
- package/commands/gsd/mempalace-recall.md +1 -1
- package/commands/gsd/new-milestone.md +2 -2
- package/commands/gsd/plan-phase.md +1 -1
- package/commands/gsd/quick.md +1 -1
- package/commands/gsd/review-backlog.md +2 -1
- package/commands/gsd/verify-work.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +1009 -115
- package/gsd-core/bin/lib/active-workstream-store.cjs +153 -12
- package/gsd-core/bin/lib/agent-install-check.cjs +268 -38
- package/gsd-core/bin/lib/api-coverage.cjs +123 -5
- package/gsd-core/bin/lib/artifacts.cjs +3 -0
- package/gsd-core/bin/lib/assumption-delta.cjs +2 -4
- package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
- package/gsd-core/bin/lib/audit.cjs +926 -202
- package/gsd-core/bin/lib/broken-windows.cjs +36 -6
- package/gsd-core/bin/lib/capability-consent.cjs +149 -15
- package/gsd-core/bin/lib/capability-lifecycle.cjs +45 -0
- package/gsd-core/bin/lib/capability-registry.cjs +608 -148
- package/gsd-core/bin/lib/capability-source.cjs +92 -0
- package/gsd-core/bin/lib/capability-trust.cjs +444 -25
- package/gsd-core/bin/lib/capability-validator.cjs +507 -24
- package/gsd-core/bin/lib/capability-writer.cjs +3 -2
- package/gsd-core/bin/lib/check-command-router.cjs +114 -38
- package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
- package/gsd-core/bin/lib/codex-agent-toml.cjs +329 -0
- package/gsd-core/bin/lib/command-aliases.cjs +94 -0
- package/gsd-core/bin/lib/command-roster.cjs +44 -1
- package/gsd-core/bin/lib/commands.cjs +665 -99
- package/gsd-core/bin/lib/commonjs-marker.cjs +142 -0
- package/gsd-core/bin/lib/complexity-trigger.cjs +1172 -0
- package/gsd-core/bin/lib/config-loader.cjs +76 -0
- package/gsd-core/bin/lib/config.cjs +22 -2
- package/gsd-core/bin/lib/context-composer.cjs +278 -0
- package/gsd-core/bin/lib/context-predicates.cjs +506 -0
- package/gsd-core/bin/lib/core-utils.cjs +217 -40
- package/gsd-core/bin/lib/decisions.cjs +23 -0
- package/gsd-core/bin/lib/docs.cjs +3 -2
- package/gsd-core/bin/lib/external-job.cjs +19 -4
- package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
- package/gsd-core/bin/lib/frontmatter.cjs +239 -32
- package/gsd-core/bin/lib/gap-checker.cjs +68 -7
- package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +57 -6
- package/gsd-core/bin/lib/git-base-branch.cjs +160 -15
- package/gsd-core/bin/lib/graphify.cjs +142 -27
- package/gsd-core/bin/lib/gsd2-import.cjs +37 -5
- package/gsd-core/bin/lib/health-diagnostic-rules/agent-install.cjs +101 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs +348 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +145 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/install-surface-shadowing.cjs +98 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs +100 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +222 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +265 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs +161 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +303 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +173 -0
- package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
- package/gsd-core/bin/lib/health-diagnostic.cjs +431 -0
- package/gsd-core/bin/lib/host-integration.cjs +13 -1
- package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
- package/gsd-core/bin/lib/init-command-router.cjs +83 -8
- package/gsd-core/bin/lib/init.cjs +1325 -169
- package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
- package/gsd-core/bin/lib/install-engine.cjs +805 -264
- package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +203 -0
- package/gsd-core/bin/lib/install-profiles.cjs +160 -57
- package/gsd-core/bin/lib/install-scope.cjs +270 -0
- package/gsd-core/bin/lib/install-shadow-report.cjs +385 -0
- package/gsd-core/bin/lib/installed-surface-resolver.cjs +381 -0
- package/gsd-core/bin/lib/installer-migration-authoring.cjs +3 -1
- package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
- package/gsd-core/bin/lib/installer-migrations/007-retire-config-root-commonjs-marker.cjs +149 -0
- package/gsd-core/bin/lib/installer-migrations/008-cursor-retire-commands-surface.cjs +55 -0
- package/gsd-core/bin/lib/installer-migrations/009-pi-retire-reserved-hooks-dir.cjs +199 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +206 -13
- package/gsd-core/bin/lib/io.cjs +38 -3
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +8 -1
- package/gsd-core/bin/lib/markdown-table.cjs +133 -20
- package/gsd-core/bin/lib/mcp-catalog.cjs +518 -0
- package/gsd-core/bin/lib/mcp-server.cjs +135 -3
- package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
- package/gsd-core/bin/lib/milestone.cjs +821 -109
- package/gsd-core/bin/lib/model-catalog.cjs +59 -1
- package/gsd-core/bin/lib/model-resolver.cjs +183 -40
- package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
- package/gsd-core/bin/lib/pattern.cjs +122 -0
- package/gsd-core/bin/lib/phase-estimation.cjs +1 -1
- package/gsd-core/bin/lib/phase-id.cjs +507 -36
- package/gsd-core/bin/lib/phase-lifecycle.cjs +28 -3
- package/gsd-core/bin/lib/phase-locator.cjs +258 -58
- package/gsd-core/bin/lib/phase.cjs +891 -156
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +303 -0
- package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
- package/gsd-core/bin/lib/plan-scan.cjs +86 -2
- package/gsd-core/bin/lib/planning-scope.cjs +31 -0
- package/gsd-core/bin/lib/planning-snapshot.cjs +890 -0
- package/gsd-core/bin/lib/planning-workspace.cjs +60 -6
- package/gsd-core/bin/lib/probe-core.cjs +1 -1
- package/gsd-core/bin/lib/profile-output.cjs +1 -1
- package/gsd-core/bin/lib/prompt-budget.cjs +128 -165
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +740 -0
- package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +85 -0
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +108 -0
- package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +447 -68
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
- package/gsd-core/bin/lib/roadmap-command-router.cjs +76 -9
- package/gsd-core/bin/lib/roadmap-parser.cjs +1035 -194
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
- package/gsd-core/bin/lib/roadmap.cjs +405 -84
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +795 -100
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +440 -57
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
- package/gsd-core/bin/lib/runtime-homes.cjs +220 -41
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +220 -44
- package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
- package/gsd-core/bin/lib/runtime-slash.cjs +27 -9
- package/gsd-core/bin/lib/section-manifest.cjs +209 -0
- package/gsd-core/bin/lib/security.cjs +104 -5
- package/gsd-core/bin/lib/shell-command-projection.cjs +388 -30
- package/gsd-core/bin/lib/smart-entry.cjs +154 -22
- package/gsd-core/bin/lib/state-command-router.cjs +5 -1
- package/gsd-core/bin/lib/state-document.cjs +152 -8
- package/gsd-core/bin/lib/state-transition.cjs +424 -105
- package/gsd-core/bin/lib/state.cjs +1927 -401
- package/gsd-core/bin/lib/surface.cjs +35 -10
- package/gsd-core/bin/lib/text-lines.cjs +80 -0
- package/gsd-core/bin/lib/token-scanner.cjs +76 -0
- package/gsd-core/bin/lib/uat-predicate.cjs +20 -4
- package/gsd-core/bin/lib/uat.cjs +706 -64
- package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
- package/gsd-core/bin/lib/ui-safety-gate.cjs +14 -5
- package/gsd-core/bin/lib/unusable-input.cjs +33 -0
- package/gsd-core/bin/lib/update-context.cjs +8 -2
- package/gsd-core/bin/lib/user-artifact-staging.cjs +705 -0
- package/gsd-core/bin/lib/validate.cjs +20 -6
- package/gsd-core/bin/lib/vendor/README.md +37 -0
- package/gsd-core/bin/lib/vendor/re2js.cjs +6480 -0
- package/gsd-core/bin/lib/vendor/re2js.d.cts +938 -0
- package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
- package/gsd-core/bin/lib/verification.cjs +287 -20
- package/gsd-core/bin/lib/verify.cjs +368 -880
- package/gsd-core/bin/lib/workflow-fragments.cjs +557 -0
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +203 -19
- package/gsd-core/bin/lib/workstream-inventory.cjs +576 -31
- package/gsd-core/bin/lib/workstream.cjs +8 -2
- package/gsd-core/bin/lib/worktree-base-ref.cjs +50 -6
- package/gsd-core/bin/lib/worktree-safety.cjs +450 -125
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +9 -1
- package/gsd-core/references/agent-contracts.md +43 -26
- package/gsd-core/references/artifact-types.md +10 -3
- package/gsd-core/references/autonomous-ui-design-contract.md +42 -0
- package/gsd-core/references/checkpoints.md +2 -2
- package/gsd-core/references/context-budget.md +1 -1
- package/gsd-core/references/debugger-techniques.md +255 -0
- package/gsd-core/references/dispatch-isolation-gate.md +138 -0
- package/gsd-core/references/doc-conflict-engine.md +1 -1
- package/gsd-core/references/execute-mvp-tdd.md +3 -3
- package/gsd-core/references/execute-phase-between-wave-reset.md +6 -2
- package/gsd-core/references/execute-phase-context-guard.md +1 -1
- package/gsd-core/references/execute-phase-response-language.md +1 -1
- package/gsd-core/references/execute-phase-wave-guard.md +6 -2
- package/gsd-core/references/gate-prompts.md +1 -1
- package/gsd-core/references/git-planning-commit.md +2 -1
- package/gsd-core/references/loop-hook-dispatch.md +39 -2
- package/gsd-core/references/model-profiles.md +12 -4
- package/gsd-core/references/mvp-concepts.md +9 -9
- package/gsd-core/references/planner-guidance.md +3 -9
- package/gsd-core/references/planner-preconditions.md +1 -1
- package/gsd-core/references/planner-reviews.md +1 -1
- package/gsd-core/references/planning-config.md +8 -6
- package/gsd-core/references/research-documentation-lookup.md +5 -3
- package/gsd-core/references/revision-loop.md +1 -1
- package/gsd-core/references/specless-probe-fallback.md +8 -7
- package/gsd-core/references/universal-anti-patterns.md +3 -3
- package/gsd-core/references/verifier-phase-gates.md +192 -0
- package/gsd-core/references/verifier-wiring-patterns.md +100 -0
- package/gsd-core/references/verify-mvp-mode.md +1 -1
- package/gsd-core/references/workstream-flag.md +22 -6
- package/gsd-core/references/worktree-branch-check.md +2 -2
- package/gsd-core/templates/discussion-log.md +1 -1
- package/gsd-core/templates/phase-prompt.md +2 -4
- package/gsd-core/templates/state.md +4 -4
- package/gsd-core/templates/summary-complex.md +2 -0
- package/gsd-core/templates/summary-minimal.md +2 -0
- package/gsd-core/templates/summary-standard.md +2 -0
- package/gsd-core/templates/summary.md +2 -0
- package/gsd-core/templates/verification-report.md +9 -1
- package/gsd-core/workflows/ai-integration-phase.md +9 -11
- package/gsd-core/workflows/audit-milestone.md +3 -0
- package/gsd-core/workflows/autonomous/steps/converge-banner.md +1 -0
- package/gsd-core/workflows/autonomous/steps/converge-dispatch-bg.md +11 -0
- package/gsd-core/workflows/autonomous/steps/converge-dispatch-inline.md +7 -0
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +21 -0
- package/gsd-core/workflows/autonomous/steps/converge-loop.md +7 -0
- package/gsd-core/workflows/autonomous.md +33 -70
- package/gsd-core/workflows/cleanup.md +62 -3
- package/gsd-core/workflows/code-review/steps/dispatch-fix.md +39 -0
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +93 -0
- package/gsd-core/workflows/code-review-fix.md +37 -10
- package/gsd-core/workflows/code-review.md +74 -166
- package/gsd-core/workflows/complete-milestone/steps/git-tag.md +29 -0
- package/gsd-core/workflows/complete-milestone.md +160 -95
- package/gsd-core/workflows/debug.md +16 -17
- package/gsd-core/workflows/diagnose-issues.md +56 -8
- package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -1
- package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +15 -0
- package/gsd-core/workflows/discuss-phase-assumptions.md +7 -17
- package/gsd-core/workflows/docs-update/steps/dispatch-monorepo-packages.md +51 -0
- package/gsd-core/workflows/docs-update.md +8 -51
- package/gsd-core/workflows/edit-phase.md +26 -1
- package/gsd-core/workflows/eval-review.md +3 -5
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +64 -7
- package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +50 -0
- package/gsd-core/workflows/execute-phase/steps/partial-wave.md +31 -0
- package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +77 -0
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +21 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +42 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +43 -37
- package/gsd-core/workflows/execute-phase.md +103 -187
- package/gsd-core/workflows/execute-plan.md +36 -4
- package/gsd-core/workflows/explore.md +131 -4
- package/gsd-core/workflows/fast.md +10 -2
- package/gsd-core/workflows/health.md +73 -4
- package/gsd-core/workflows/help/modes/full.md +6 -1
- package/gsd-core/workflows/import.md +4 -4
- package/gsd-core/workflows/ingest-docs.md +7 -6
- package/gsd-core/workflows/mvp-phase.md +6 -3
- package/gsd-core/workflows/new-milestone/steps/project-md-milestone-write.md +16 -0
- package/gsd-core/workflows/new-milestone/steps/reset-phase-safety.md +19 -0
- package/gsd-core/workflows/new-milestone.md +35 -47
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +176 -0
- package/gsd-core/workflows/new-project/steps/auto-mode-detection.md +32 -0
- package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +18 -0
- package/gsd-core/workflows/new-project.md +27 -240
- package/gsd-core/workflows/next.md +12 -0
- package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +15 -0
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +110 -0
- package/gsd-core/workflows/plan-phase/steps/prd-express-gate.md +8 -0
- package/gsd-core/workflows/plan-phase/steps/research-only-early-exit.md +17 -0
- package/gsd-core/workflows/plan-phase/steps/research-only-modifiers.md +16 -0
- package/gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md +17 -0
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +149 -0
- package/gsd-core/workflows/plan-phase.md +89 -209
- package/gsd-core/workflows/plan-review-convergence.md +50 -2
- package/gsd-core/workflows/progress/steps/forensic-audit.md +125 -0
- package/gsd-core/workflows/progress/steps/mvp-display.md +18 -0
- package/gsd-core/workflows/progress.md +45 -159
- package/gsd-core/workflows/quick/steps/discussion-phase.md +124 -0
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +111 -0
- package/gsd-core/workflows/quick/steps/quick-verification.md +67 -0
- package/gsd-core/workflows/quick/steps/research-phase.md +72 -0
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +37 -0
- package/gsd-core/workflows/quick.md +55 -405
- package/gsd-core/workflows/resume-project.md +3 -0
- package/gsd-core/workflows/review/steps/reviewer-instances-note-1.md +4 -0
- package/gsd-core/workflows/review/steps/reviewer-instances-note-2.md +3 -0
- package/gsd-core/workflows/review.md +41 -13
- package/gsd-core/workflows/section-manifest.json +219 -0
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/session-report.md +2 -1
- package/gsd-core/workflows/settings.md +66 -2
- package/gsd-core/workflows/ship.md +104 -44
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/spec-phase.md +41 -20
- package/gsd-core/workflows/spike-wrap-up.md +20 -5
- package/gsd-core/workflows/spike.md +50 -16
- package/gsd-core/workflows/sync-skills.md +106 -13
- package/gsd-core/workflows/transition/steps/workstream-collision-check.md +17 -0
- package/gsd-core/workflows/transition.md +53 -31
- package/gsd-core/workflows/ui-phase.md +13 -12
- package/gsd-core/workflows/ui-review.md +2 -2
- package/gsd-core/workflows/update/steps/channel-banner.md +7 -0
- package/gsd-core/workflows/update.md +19 -8
- package/gsd-core/workflows/validate-phase.md +1 -1
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +36 -0
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +21 -0
- package/gsd-core/workflows/verify-work.md +17 -65
- package/hooks/dist/gsd-agent-isolation-guard.js +517 -0
- package/hooks/dist/gsd-check-update-worker.js +64 -12
- package/hooks/dist/gsd-check-update.js +19 -1
- package/hooks/dist/gsd-cursor-pre-tool.js +0 -3
- package/hooks/dist/gsd-cursor-subagent-start.js +607 -26
- package/hooks/dist/gsd-cursor-subagent-stop.js +3 -2
- package/hooks/dist/gsd-prompt-guard.js +21 -20
- package/hooks/dist/gsd-read-injection-scanner.js +45 -24
- package/hooks/dist/gsd-statusline.js +90 -6
- package/hooks/dist/gsd-update-banner.js +22 -1
- package/hooks/dist/gsd-workflow-guard.js +134 -36
- package/hooks/dist/gsd-worktree-path-guard.js +2 -1
- package/hooks/dist/gsd-write-guard.js +359 -0
- package/hooks/dist/lib/git-cmd.js +92 -59
- package/hooks/dist/lib/injection-patterns.js +45 -0
- package/hooks/dist/lib/isolation-deny-reason.js +39 -0
- package/hooks/dist/lib/isolation-sentinel.js +277 -0
- package/hooks/dist/managed-hooks-registry.cjs +2 -0
- package/hooks/gsd-agent-isolation-guard.js +517 -0
- package/hooks/gsd-check-update-worker.js +64 -12
- package/hooks/gsd-check-update.js +19 -1
- package/hooks/gsd-cursor-pre-tool.js +0 -3
- package/hooks/gsd-cursor-subagent-start.js +607 -26
- package/hooks/gsd-cursor-subagent-stop.js +3 -2
- package/hooks/gsd-prompt-guard.js +21 -20
- package/hooks/gsd-read-injection-scanner.js +45 -24
- package/hooks/gsd-statusline.js +90 -6
- package/hooks/gsd-update-banner.js +22 -1
- package/hooks/gsd-workflow-guard.js +134 -36
- package/hooks/gsd-worktree-path-guard.js +2 -1
- package/hooks/gsd-write-guard.js +359 -0
- package/hooks/hooks.json +12 -0
- package/hooks/lib/git-cmd.js +92 -59
- package/hooks/lib/injection-patterns.js +45 -0
- package/hooks/lib/isolation-deny-reason.js +39 -0
- package/hooks/lib/isolation-sentinel.js +277 -0
- package/hooks/managed-hooks-registry.cjs +2 -0
- package/package.json +31 -10
- package/pi/gsd.cjs +71 -12
- package/scripts/baselines/planning-prompt-drift-baseline.json +4 -0
- package/scripts/baselines/planning-snapshot-bypass-baseline.json +12 -0
- package/scripts/baselines/unreachable-guard-drift-baseline.json +4 -0
- package/scripts/build-hooks.js +9 -0
- package/scripts/changeset/lint.cjs +68 -6
- package/scripts/changeset/serialize.cjs +5 -1
- package/scripts/check-alias-drift.cjs +7 -43
- package/scripts/check-contract-drift.cjs +297 -0
- package/scripts/ci-test-scope.cjs +19 -2
- package/scripts/command-contract-helpers.cjs +903 -1
- package/scripts/gen-adr-index.cjs +728 -38
- package/scripts/gen-capability-matrix.cjs +1 -1
- package/scripts/gen-capability-registry.cjs +3 -15
- package/scripts/gen-context-index.cjs +439 -0
- package/scripts/gen-health-docs.cjs +390 -0
- package/scripts/gen-inventory-manifest.cjs +150 -4
- package/scripts/gen-loop-host-contract.cjs +4 -24
- package/scripts/gen-prompt-budget-parity-corpus.cjs +645 -0
- package/scripts/gen-registry.cjs +3 -14
- package/scripts/gen-section-manifest.cjs +638 -0
- package/scripts/generate-package-identity.cjs +4 -2
- package/scripts/lib/alias-drift-families.cjs +46 -0
- package/scripts/lib/drift-scan.cjs +278 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +15 -54
- package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +4 -0
- package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +3 -0
- package/scripts/lint-canary-version-leak.cjs +73 -0
- package/scripts/lint-command-contract.cjs +96 -13
- package/scripts/lint-compiled-artifact-sync.cjs +6 -1
- package/scripts/lint-completion-predicate-drift.cjs +933 -0
- package/scripts/lint-completion-ratio-drift.cjs +214 -0
- package/scripts/lint-default-flip-documentation.cjs +193 -0
- package/scripts/lint-docs-command-form.cjs +195 -0
- package/scripts/lint-docs-required.cjs +9 -1
- package/scripts/lint-emitted-drift-ack.cjs +215 -20
- package/scripts/lint-eslint-glob-coverage.allowlist.json +34 -0
- package/scripts/lint-eslint-glob-coverage.cjs +340 -0
- package/scripts/lint-example-parser-parity.cjs +395 -0
- package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
- package/scripts/lint-health-diagnostic-rule-table.cjs +404 -0
- package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
- package/scripts/lint-milestone-window-drift.cjs +468 -0
- package/scripts/lint-phase-enumeration-drift.cjs +479 -0
- package/scripts/lint-plan-count-drift.cjs +318 -0
- package/scripts/lint-planning-artifact-writer-drift.cjs +398 -0
- package/scripts/lint-planning-prompt-drift.cjs +434 -0
- package/scripts/lint-planning-snapshot-bypass-drift.cjs +544 -0
- package/scripts/lint-regression-test-names.cjs +15 -13
- package/scripts/lint-removed-but-needed.cjs +320 -0
- package/scripts/lint-state-field-drift.cjs +805 -0
- package/scripts/lint-state-write-path-drift.cjs +1045 -0
- package/scripts/lint-test-file-count.allowlist.json +40 -3
- package/scripts/lint-unreachable-guard-drift.cjs +843 -0
- package/scripts/lint-vendored-deps.cjs +124 -0
- package/scripts/mutation-matrix.cjs +13 -0
- package/scripts/pr-changed-files.cjs +63 -0
- package/scripts/pr-template-policy.cjs +14 -4
- package/scripts/prompt-injection-scan.sh +52 -6
- package/scripts/require-issue-link-policy.cjs +192 -0
- package/scripts/state-write-path-drift-baseline.json +19 -0
- package/scripts/sync-runtime-launcher.cjs +2 -4
- package/skills/gsd-autonomous/SKILL.md +0 -1
- package/skills/gsd-code-review/SKILL.md +1 -1
- package/skills/gsd-execute-phase/SKILL.md +1 -2
- package/skills/gsd-map-codebase/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +2 -2
- package/skills/gsd-mempalace-recall/SKILL.md +1 -1
- package/skills/gsd-new-milestone/SKILL.md +2 -2
- package/skills/gsd-next/SKILL.md +0 -1
- package/skills/gsd-plan-phase/SKILL.md +1 -2
- package/skills/gsd-progress/SKILL.md +0 -1
- package/skills/gsd-quick/SKILL.md +1 -1
- package/skills/gsd-review-backlog/SKILL.md +2 -1
- package/skills/gsd-stats/SKILL.md +0 -1
- package/skills/gsd-verify-work/SKILL.md +1 -1
- package/vscode/package.json +1 -1
- package/gsd-core/workflows/discovery-phase.md +0 -298
- package/gsd-core/workflows/plan-milestone-gaps.md +0 -281
- package/gsd-core/workflows/verify-phase.md +0 -577
- package/scripts/affected-tests-lib.cjs +0 -554
- package/scripts/gen-emitted-baseline.cjs +0 -145
- package/scripts/lint-allow-test-rule-refs.cjs +0 -162
- package/scripts/run-affected-tests.cjs +0 -7
- package/scripts/run-tests.cjs +0 -1050
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* install-scope.cts — Install Scope Module (#2870, ADR-2866, governed by
|
|
4
|
+
* ADR-2866, Phase 0 PR #3265).
|
|
5
|
+
*
|
|
6
|
+
* `resolveScope()` turns a bare `'global' | 'local'` string — previously
|
|
7
|
+
* re-derived at 12 `isGlobal ? 'global' : 'local'` sites in `bin/install.js`
|
|
8
|
+
* plus several downstream consumers — into ONE resolved value produced by
|
|
9
|
+
* ONE module. See `.gsd/phase/feat-2870-install-scope-module/40-design.md`
|
|
10
|
+
* for the full behavior table and rationale; the summary that matters for
|
|
11
|
+
* future readers is captured in the comments below.
|
|
12
|
+
*
|
|
13
|
+
* This module OWNS the `InstallScope` type name. It was previously declared
|
|
14
|
+
* (as a private, non-exported `TypeAlias`) inside
|
|
15
|
+
* `runtime-artifact-install-plan.cts`; that module now imports it from here
|
|
16
|
+
* instead of re-declaring it, so the codebase has one spelling of "install
|
|
17
|
+
* scope" instead of a fifth one appearing alongside the three that already
|
|
18
|
+
* existed (`'local' | 'global'` in the layout module, `'global' | 'project'`
|
|
19
|
+
* in capability-lifecycle, and the single literal `'project'` in
|
|
20
|
+
* capability-consent).
|
|
21
|
+
*
|
|
22
|
+
* ── Compose, never modify, resolveConfigHomeFromDescriptor ─────────────────
|
|
23
|
+
* `resolveConfigHomeFromDescriptor` (`runtime-homes.cts`) is rated CRITICAL
|
|
24
|
+
* blast radius: 60 dependents across 13 files and 2 process flows. Adding a
|
|
25
|
+
* `scope` parameter to it — the "obvious" refactor — would touch all 60 for
|
|
26
|
+
* no reason this module needs: it already resolves the GLOBAL config home
|
|
27
|
+
* correctly today. So this module calls it as-is for the global scope and
|
|
28
|
+
* derives the LOCAL scope's config dir independently (see
|
|
29
|
+
* `resolveScopeConfigHome` below) — genuine composition, not a rename. Every
|
|
30
|
+
* one of those 60 call sites stays byte-identical.
|
|
31
|
+
*
|
|
32
|
+
* ── Why `settingsFile: null` is correct, not a bug ──────────────────────────
|
|
33
|
+
* Only `claude` declares `hostBehaviors.settingsFileByScope` in the
|
|
34
|
+
* capability registry; the other 18 registered runtimes do not have a
|
|
35
|
+
* per-scope settings file at all. Returning `null` for them is honest —
|
|
36
|
+
* substituting `'settings.json'` (or any other Claude-shaped default) would
|
|
37
|
+
* invent a fact for every non-Claude runtime that asked. Callers that
|
|
38
|
+
* legitimately want a Claude-specific fallback (there is exactly one today,
|
|
39
|
+
* `bin/install.js:550`) apply it themselves; this module does not.
|
|
40
|
+
*/
|
|
41
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
42
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
43
|
+
};
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
exports.SCOPE_ORDER = void 0;
|
|
46
|
+
exports.validateScopeId = validateScopeId;
|
|
47
|
+
exports.isInstallScopeId = isInstallScopeId;
|
|
48
|
+
exports.resolveScope = resolveScope;
|
|
49
|
+
exports.isGlobalScope = isGlobalScope;
|
|
50
|
+
exports.scopeRank = scopeRank;
|
|
51
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
52
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
53
|
+
const runtime_homes_cjs_1 = require("./runtime-homes.cjs");
|
|
54
|
+
// In .cts (CommonJS output) files, `require` is available as a global.
|
|
55
|
+
const _require = require;
|
|
56
|
+
// ── The `local` / `project` boundary (see CONTEXT.md glossary entry) ──────
|
|
57
|
+
//
|
|
58
|
+
// `ConsentRecord.scope: 'project'` (capability-consent.cts) and
|
|
59
|
+
// `capability-lifecycle.cts:163`'s `'global' | 'project'` are NOT renamed to
|
|
60
|
+
// match this module's `'local'` spelling. `ConsentRecord.scope` is persisted
|
|
61
|
+
// on disk in user-owned consent records outside this repo; renaming that
|
|
62
|
+
// literal would silently invalidate every existing project-scoped consent
|
|
63
|
+
// record on a user's machine the next time it is read back. The
|
|
64
|
+
// reconciliation is a documented boundary mapping, not a rename sweep:
|
|
65
|
+
// install scope 'local' ⇄ consent scope 'project'
|
|
66
|
+
// install scope 'global' ⇄ no consent record at all
|
|
67
|
+
// `consentRequired` above reports the install-scope side of that mapping;
|
|
68
|
+
// it is deliberately still the CLI's own vocabulary.
|
|
69
|
+
const VALID_SCOPE_IDS = new Set(['global', 'local']);
|
|
70
|
+
/**
|
|
71
|
+
* Single owner of the `'global' | 'local'` membership check. `resolveScope`,
|
|
72
|
+
* `isGlobalScope`, `scopeRank`, and `resolveTriggerSurface`
|
|
73
|
+
* (`runtime-artifact-layout.cts`, #2871 Phase 2) all call this instead of
|
|
74
|
+
* each carrying its own copy of the rule — one validator every scope-typed
|
|
75
|
+
* seam reads, not N validators that could silently diverge. Exported so a
|
|
76
|
+
* sibling module can reuse it directly rather than re-deriving the same
|
|
77
|
+
* membership check a second time.
|
|
78
|
+
*/
|
|
79
|
+
function validateScopeId(id, caller) {
|
|
80
|
+
if (typeof id !== 'string' || !VALID_SCOPE_IDS.has(id)) {
|
|
81
|
+
throw new TypeError(`${caller}: id must be one of 'global' | 'local', got ${JSON.stringify(id)}`);
|
|
82
|
+
}
|
|
83
|
+
return id;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Non-throwing sibling of {@link validateScopeId}, for readers that must
|
|
87
|
+
* report an unrecognized scope as a value rather than fail (#2872). Reads the
|
|
88
|
+
* same `VALID_SCOPE_IDS` set, so the two can never disagree about what a
|
|
89
|
+
* scope is.
|
|
90
|
+
*/
|
|
91
|
+
function isInstallScopeId(value) {
|
|
92
|
+
return typeof value === 'string' && VALID_SCOPE_IDS.has(value);
|
|
93
|
+
}
|
|
94
|
+
// Higher wins. Not exported as a public constant — only the resulting
|
|
95
|
+
// `hostPrecedenceRank` field on `ResolvedScope` is public API, so a future
|
|
96
|
+
// re-basing of the literal values (Phase 2, #2871) never requires touching
|
|
97
|
+
// an exported symbol.
|
|
98
|
+
const HOST_PRECEDENCE_RANK = {
|
|
99
|
+
global: 2,
|
|
100
|
+
local: 1,
|
|
101
|
+
};
|
|
102
|
+
/** Lazy registry accessor — mirrors the pattern in runtime-homes.cts /
|
|
103
|
+
* runtime-artifact-layout.cts (5b/5c/5d). */
|
|
104
|
+
function getRegistry() {
|
|
105
|
+
return _require('./capability-registry.cjs');
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Normalize path separators UNCONDITIONALLY (never gated on `path.sep` /
|
|
109
|
+
* `process.platform`). A Windows-shaped `home` (`C:\Users\x`) can arrive on
|
|
110
|
+
* any host — via an injected test fixture, a cross-platform config sync, or
|
|
111
|
+
* a value copied from a Windows machine — so the normalization must not
|
|
112
|
+
* depend on which OS this process happens to be running on.
|
|
113
|
+
*/
|
|
114
|
+
function normalizeSeparators(p) {
|
|
115
|
+
return p.replace(/\\/g, '/');
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Minimal leading-`~` expansion for `explicitDir`. `runtime-homes.cts`'s own
|
|
119
|
+
* `expandTilde` is NOT exported (it is a private helper), and this module
|
|
120
|
+
* must not add exports to that CRITICAL-blast-radius file just to reuse
|
|
121
|
+
* three lines — so this is an intentionally small, independent
|
|
122
|
+
* reimplementation, not a fork of shared logic.
|
|
123
|
+
*/
|
|
124
|
+
function expandTildeForExplicitDir(p, home) {
|
|
125
|
+
const resolvedHome = home ?? node_os_1.default.homedir();
|
|
126
|
+
if (p === '~')
|
|
127
|
+
return resolvedHome;
|
|
128
|
+
if (p.startsWith('~/'))
|
|
129
|
+
return node_path_1.default.join(resolvedHome, p.slice(2));
|
|
130
|
+
return p;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Resolve the config-home directory for one scope. `explicitDir` short-
|
|
134
|
+
* circuits both scopes identically (matches `getGlobalConfigDir`'s existing
|
|
135
|
+
* override behavior — the module must not regress it). Otherwise:
|
|
136
|
+
* - `global`: delegates entirely to `resolveConfigHomeFromDescriptor`
|
|
137
|
+
* (composition — see the module-level comment).
|
|
138
|
+
* - `local`: joins the registry's `localConfigDir` onto `cwd` (defaulting
|
|
139
|
+
* to the real process cwd) — the project-local dir, independent of
|
|
140
|
+
* `home`/`env`.
|
|
141
|
+
*/
|
|
142
|
+
function resolveScopeConfigHome(id, descriptor, input) {
|
|
143
|
+
const explicitDir = input.explicitDir;
|
|
144
|
+
if (typeof explicitDir === 'string' && explicitDir.trim() !== '') {
|
|
145
|
+
return normalizeSeparators(expandTildeForExplicitDir(explicitDir, input.home));
|
|
146
|
+
}
|
|
147
|
+
if (id === 'local') {
|
|
148
|
+
// localConfigDir is guaranteed non-null here: the only registered
|
|
149
|
+
// runtime with `localConfigDir: null` is vscode, and vscode's
|
|
150
|
+
// `configHome.kind === 'none'` already causes resolveScope to throw
|
|
151
|
+
// before this function is ever called (see the 'none' guard below).
|
|
152
|
+
const localConfigDir = descriptor.localConfigDir;
|
|
153
|
+
const cwd = input.cwd ?? process.cwd();
|
|
154
|
+
return normalizeSeparators(node_path_1.default.join(cwd, localConfigDir));
|
|
155
|
+
}
|
|
156
|
+
return normalizeSeparators((0, runtime_homes_cjs_1.resolveConfigHomeFromDescriptor)(descriptor.configHome, {
|
|
157
|
+
env: input.env,
|
|
158
|
+
home: input.home,
|
|
159
|
+
existsSync: input.existsSync,
|
|
160
|
+
}));
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Resolve a bare `'global' | 'local'` scope id plus a runtime into a single
|
|
164
|
+
* `ResolvedScope` value: the config directory, the per-scope settings
|
|
165
|
+
* filename (or `null`), whether the scope requires a consent record, and a
|
|
166
|
+
* precedence rank (data only this phase — see `hostPrecedenceRank` above).
|
|
167
|
+
*
|
|
168
|
+
* Pure: performs no writes and no I/O of its own beyond what
|
|
169
|
+
* `resolveConfigHomeFromDescriptor` already performs via the injected
|
|
170
|
+
* `existsSync` (for `global`) or the injected `cwd`, defaulting to
|
|
171
|
+
* `process.cwd()` (for `local`). Never mutates `input`. The returned object
|
|
172
|
+
* is frozen so a caller mutating the result cannot corrupt a subsequent
|
|
173
|
+
* call.
|
|
174
|
+
*
|
|
175
|
+
* Throws `TypeError` for:
|
|
176
|
+
* - an `id` outside `'global' | 'local'` — including wrong case, empty,
|
|
177
|
+
* missing, or any non-string value (no coercion, ever);
|
|
178
|
+
* - an unknown `runtime` (no matching capability-registry entry);
|
|
179
|
+
* - a `runtime` whose descriptor has `configHome.kind === 'none'`
|
|
180
|
+
* (vscode) — there is no installable config directory to resolve, so
|
|
181
|
+
* inventing one (or silently returning `configHome: null`) would be
|
|
182
|
+
* dishonest. All three cases share one catch shape (`instanceof
|
|
183
|
+
* TypeError`) with `resolveRuntimeArtifactLayout`'s existing contract
|
|
184
|
+
* for unknown runtimes, so callers of both never need two different
|
|
185
|
+
* catch blocks.
|
|
186
|
+
*/
|
|
187
|
+
function resolveScope(input) {
|
|
188
|
+
const scopeId = validateScopeId(input?.id, 'resolveScope');
|
|
189
|
+
const runtime = input.runtime;
|
|
190
|
+
const registryEntry = typeof runtime === 'string'
|
|
191
|
+
? getRegistry().runtimes[runtime]
|
|
192
|
+
: undefined;
|
|
193
|
+
const descriptor = registryEntry?.runtime;
|
|
194
|
+
if (!descriptor) {
|
|
195
|
+
throw new TypeError(`resolveScope: unknown runtime '${String(runtime)}' — not present in the capability registry`);
|
|
196
|
+
}
|
|
197
|
+
if (descriptor.configHome.kind === 'none') {
|
|
198
|
+
// #2103: vscode-shaped runtimes (Marketplace/VSIX, installSurface:
|
|
199
|
+
// 'none') have no file-projected config directory at all — the same
|
|
200
|
+
// carve-out tests/runtime-flags.test.cjs's NON_INSTALLABLE_RUNTIMES
|
|
201
|
+
// already documents. Throwing here matches
|
|
202
|
+
// resolveConfigHomeFromDescriptor's own deliberate throw on this kind,
|
|
203
|
+
// rather than silently inventing an install scope for a runtime that
|
|
204
|
+
// cannot be installed.
|
|
205
|
+
throw new TypeError(`resolveScope: runtime '${runtime}' has no installable config directory (configHome.kind === 'none')`);
|
|
206
|
+
}
|
|
207
|
+
const configHome = resolveScopeConfigHome(scopeId, descriptor, input);
|
|
208
|
+
const settingsFile = descriptor.hostBehaviors?.settingsFileByScope?.[scopeId] ?? null;
|
|
209
|
+
const consentRequired = scopeId === 'local';
|
|
210
|
+
const hostPrecedenceRank = HOST_PRECEDENCE_RANK[scopeId];
|
|
211
|
+
return Object.freeze({
|
|
212
|
+
id: scopeId,
|
|
213
|
+
configHome,
|
|
214
|
+
settingsFile,
|
|
215
|
+
consentRequired,
|
|
216
|
+
hostPrecedenceRank,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Project an `InstallScope` down to the boolean shape some downstream APIs
|
|
221
|
+
* still require. Four call sites (both kind-builder closures in
|
|
222
|
+
* `runtime-artifact-layout.cts`, plus one each in
|
|
223
|
+
* `runtime-artifact-install-plan.cts` and `surface.cts`) were each
|
|
224
|
+
* independently re-deriving this same `scope === 'global'` comparison — four
|
|
225
|
+
* copies of one rule that could silently drift apart (#2870). They exist
|
|
226
|
+
* because `runtime-artifact-conversion.cts`'s `_computePathPrefix` takes
|
|
227
|
+
* `isGlobal: boolean` at its API boundary, and that boundary is not changing
|
|
228
|
+
* here, so the boolean projection cannot be eliminated — only centralized to
|
|
229
|
+
* the one place below.
|
|
230
|
+
*
|
|
231
|
+
* Throws the same `TypeError`, with the same message shape, as
|
|
232
|
+
* `resolveScope` throws for an `id` outside `'global' | 'local'` — both call
|
|
233
|
+
* `validateScopeId` above, so the two error contracts cannot diverge.
|
|
234
|
+
*
|
|
235
|
+
* Deliberately throws, rather than returning `false`, for an out-of-union
|
|
236
|
+
* value — unlike the inline `scope === 'global'` comparison it replaced,
|
|
237
|
+
* which silently returned `false` for anything unrecognized. The
|
|
238
|
+
* alternative is silently treating an unknown scope as "not global" and
|
|
239
|
+
* writing artifacts to the wrong place, which is worse than failing loud.
|
|
240
|
+
* A caller holding an optional `scope` (e.g. a raw `Layout.scope`) must
|
|
241
|
+
* default it before calling this — see `surface.cts` for the pattern.
|
|
242
|
+
*/
|
|
243
|
+
function isGlobalScope(scope) {
|
|
244
|
+
return validateScopeId(scope, 'isGlobalScope') === 'global';
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Project a bare `InstallScope` down to its `hostPrecedenceRank` — the SAME
|
|
248
|
+
* `HOST_PRECEDENCE_RANK` table `resolveScope`'s `ResolvedScope.hostPrecedenceRank`
|
|
249
|
+
* field reads, exposed standalone so a caller that only needs the ranking (not a
|
|
250
|
+
* full config-home resolution, which touches the filesystem via
|
|
251
|
+
* `resolveConfigHomeFromDescriptor`) never has to re-derive `{global: 2, local:
|
|
252
|
+
* 1}` as a second copy of the same fact. First consumer: `resolveTriggerSurface`
|
|
253
|
+
* (`runtime-artifact-layout.cts`, #2871 Phase 2), which is documented pure — no
|
|
254
|
+
* filesystem — so it cannot call `resolveScope` itself. Same validation/error
|
|
255
|
+
* contract as `resolveScope` / `isGlobalScope`: all three share `validateScopeId`,
|
|
256
|
+
* so an out-of-union `id` throws the same `TypeError` shape everywhere.
|
|
257
|
+
*/
|
|
258
|
+
function scopeRank(id) {
|
|
259
|
+
return HOST_PRECEDENCE_RANK[validateScopeId(id, 'scopeRank')];
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Both scope ids, highest host precedence first. The ONE ordering of the
|
|
263
|
+
* install-scope axis: `runtime-artifact-layout.cts`'s trigger resolution and
|
|
264
|
+
* `installed-surface-resolver.cts`'s scope-record construction both consume
|
|
265
|
+
* this rather than each re-declaring `['global','local']` (#2872 review
|
|
266
|
+
* finding — this repo's recorded "generative fix divergence" class). Frozen so
|
|
267
|
+
* a caller cannot reorder it for everyone else. Ordering is not arbitrary: it
|
|
268
|
+
* is `scopeRank` descending, and a test locks that so the two cannot drift.
|
|
269
|
+
*/
|
|
270
|
+
exports.SCOPE_ORDER = Object.freeze(['global', 'local']);
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* install-shadow-report.cts — Cross-Scope Shadow Report Module (#2873, epic
|
|
4
|
+
* #2866 Phase 4a — governed by
|
|
5
|
+
* `.gsd/phase/feat-2873-cross-scope-shadowing/40-design.md`).
|
|
6
|
+
*
|
|
7
|
+
* A read-only PROJECTION over `resolveInstalledSurfaces`
|
|
8
|
+
* (`installed-surface-resolver.cts`, #2872 Phase 3). That module answers
|
|
9
|
+
* "what is installed"; it is documented there as read-only, and rendering
|
|
10
|
+
* plus sanitization are a different concern with a different consumer set
|
|
11
|
+
* (installer + `/gsd-health`) — the design doc's "Rejected" #5 is why this is
|
|
12
|
+
* a separate leaf module rather than a second export bolted onto the
|
|
13
|
+
* resolver.
|
|
14
|
+
*
|
|
15
|
+
* ── What "shadowed" means here ──────────────────────────────────────────────
|
|
16
|
+
* A trigger is shadowed when `resolveTriggerSurface` (via the resolver)
|
|
17
|
+
* recorded a non-null `shadowedBy` for it: two scopes both installed a
|
|
18
|
+
* trigger-bearing artifact under the SAME trigger name, and only one wins.
|
|
19
|
+
* For claude (`skills`@global vs `commands`@local) the KINDS differ, so the
|
|
20
|
+
* loser's entire spec tree becomes unreachable through the trigger — the bug
|
|
21
|
+
* #2218 diagnosed. For the 12 both-scopes-`skills` runtimes the kinds are the
|
|
22
|
+
* SAME on both sides, so the loser is merely overridden, not vanished
|
|
23
|
+
* (design row #5 / "Not-corruption"). `kindsDiffer` on `ShadowReport` is what
|
|
24
|
+
* lets `renderShadowReport` word the two cases correctly.
|
|
25
|
+
*
|
|
26
|
+
* ── Report, don't correct (mirrors the resolver's own law) ─────────────────
|
|
27
|
+
* `mismatches` surfaces a declared runtime/scope that disagrees with the
|
|
28
|
+
* probed one (Postel's Law, design doc: liberal in what is accepted, but the
|
|
29
|
+
* mismatch is never silently absorbed). This module never substitutes a
|
|
30
|
+
* declared value for a probed one; it only reports the disagreement the
|
|
31
|
+
* resolver already computed.
|
|
32
|
+
*
|
|
33
|
+
* ── Sanitize at the render seam ─────────────────────────────────────────────
|
|
34
|
+
* `declaredRuntime` is attacker-influenceable (it comes from a manifest that
|
|
35
|
+
* may live inside a merely-cloned repository) and length-bounded but
|
|
36
|
+
* deliberately NOT charset-gated by the reader (`declaredRuntimeMatchesProbe`
|
|
37
|
+
* needs the raw value there). THIS module is what renders it to an operator,
|
|
38
|
+
* so this module owns the guard — `sanitizeForRender` strips ANSI escapes,
|
|
39
|
+
* C0/C1 controls, and Unicode bidi overrides/isolates, then collapses
|
|
40
|
+
* whitespace. It never truncates: `readInstallManifest` already caps at 64
|
|
41
|
+
* chars, and a second truncation here would double-truncate.
|
|
42
|
+
*
|
|
43
|
+
* Trigger names, by contrast, are already `SAFE_STEM`-gated upstream
|
|
44
|
+
* (`installed-surface-resolver.cts`'s `deriveStemsForKindEntry`) before they
|
|
45
|
+
* ever reach a `TriggerSurface` — this module does not re-gate them.
|
|
46
|
+
*
|
|
47
|
+
* ── Per-scope truth filter (why this lives HERE, not in the resolver) ──────
|
|
48
|
+
* `resolveOneRuntime` (`installed-surface-resolver.cts`) builds ONE union of
|
|
49
|
+
* every installed scope's `stems` and hands that single list to
|
|
50
|
+
* `resolveTriggerSurface`, which then synthesizes a candidate trigger for
|
|
51
|
+
* EVERY stem at EVERY installed scope's trigger-bearing kind entry —
|
|
52
|
+
* regardless of whether that specific scope's own manifest actually shipped
|
|
53
|
+
* that stem. Concretely: a global `full`-profile install (stems a, b, c)
|
|
54
|
+
* alongside a local `core`-profile install (stem a only) unions to
|
|
55
|
+
* `{a, b, c}`, and `resolveTriggerSurface` then reports `commands@local`
|
|
56
|
+
* candidates for b and c too — trigger names for artifacts that do not exist
|
|
57
|
+
* on disk at that scope. Left unfiltered, this module would tell the user
|
|
58
|
+
* `/gsd-b` and `/gsd-c` are shadowed local commands when there is no local
|
|
59
|
+
* artifact for either at all — over-reporting that is not cosmetic, since
|
|
60
|
+
* the whole point of this report is to make a real failure legible.
|
|
61
|
+
*
|
|
62
|
+
* `resolveTriggerSurface`'s API takes ONE stem list shared by every scope it
|
|
63
|
+
* is asked about, so per-scope truth cannot be expressed through it without
|
|
64
|
+
* either widening a shipped Phase-2 contract other callers may depend on, or
|
|
65
|
+
* calling it once per scope and re-implementing its winner computation
|
|
66
|
+
* (`isHigherPriority`) here as a second, driftable copy. `resolveOneRuntime`
|
|
67
|
+
* / `resolveInstalledSurfaces` (Phase 3, #2872) is likewise a shipped module
|
|
68
|
+
* this task deliberately leaves untouched. This module already receives the
|
|
69
|
+
* full `InstalledRuntimeSurface`, including each scope's own REAL `stems`
|
|
70
|
+
* list (`installed-surface-resolver.cts`'s `deriveStemsFromManifest`) — so
|
|
71
|
+
* the correction belongs here, as a filter over `resolveTriggerSurface`'s
|
|
72
|
+
* already-computed `shadowedBy` groups: a trigger is reported as shadowed
|
|
73
|
+
* only when its underlying stem is present in BOTH the winner's scope's own
|
|
74
|
+
* `stems` AND the shadowed side's scope's own `stems` — i.e. an artifact
|
|
75
|
+
* genuinely exists at both scopes, not merely "some stem exists somewhere in
|
|
76
|
+
* the union".
|
|
77
|
+
*
|
|
78
|
+
* `TriggerSurface` does not carry the originating stem OR the composing
|
|
79
|
+
* prefix on its output — only the already-composed `trigger` string
|
|
80
|
+
* (`${prefix}${stem}`) — so the stem cannot be read off it directly. Rather
|
|
81
|
+
* than hand-roll a fixed-offset `trigger.slice(4)` (which would silently
|
|
82
|
+
* assume every runtime's prefix is exactly `gsd-` — true today, but not a
|
|
83
|
+
* contract this module owns), the prefix is recovered the honest way: by
|
|
84
|
+
* re-resolving that scope's `ArtifactKind` layout (`resolveRuntimeArtifactLayout`
|
|
85
|
+
* / `resolveRuntimeArtifactLayoutFromRegistry`, the SAME layout descriptor
|
|
86
|
+
* `resolveTriggerSurface` itself reads its `entry.prefix` from) for the
|
|
87
|
+
* winner's and shadowed side's own `(scope, kind)`, and reading `.prefix`
|
|
88
|
+
* off the matching kind entry. This is metadata-only (constructing an
|
|
89
|
+
* `ArtifactKind` never touches the filesystem — see
|
|
90
|
+
* `runtime-artifact-layout.cts`'s kind-builder functions), so it costs
|
|
91
|
+
* nothing beyond a small per-`(scope,kind)` memo. If a prefix cannot be
|
|
92
|
+
* resolved at all (a `TypeError` from an unexpected registry shape), the
|
|
93
|
+
* trigger is conservatively DROPPED rather than kept — the same
|
|
94
|
+
* report-nothing-you-cannot-prove posture as the rest of this filter.
|
|
95
|
+
*
|
|
96
|
+
* ── Pure with respect to caller-visible state ───────────────────────────────
|
|
97
|
+
* `buildShadowReport` builds a fresh `ShadowReport` (fresh arrays, fresh
|
|
98
|
+
* objects) on every call, exactly as the resolver documents for itself
|
|
99
|
+
* (`installed-surface-resolver.cts`'s "Pure with respect to caller-visible
|
|
100
|
+
* state" paragraph) — no shared or cached state between calls.
|
|
101
|
+
*/
|
|
102
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
103
|
+
exports.SHADOW_REASON = void 0;
|
|
104
|
+
exports.sanitizeForRender = sanitizeForRender;
|
|
105
|
+
exports.buildShadowReport = buildShadowReport;
|
|
106
|
+
exports.renderShadowReport = renderShadowReport;
|
|
107
|
+
const installed_surface_resolver_cjs_1 = require("./installed-surface-resolver.cjs");
|
|
108
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
109
|
+
const runtimeArtifactLayoutMod = require("./runtime-artifact-layout.cjs");
|
|
110
|
+
const { resolveRuntimeArtifactLayout, resolveRuntimeArtifactLayoutFromRegistry } = runtimeArtifactLayoutMod;
|
|
111
|
+
// ── Reason enum ─────────────────────────────────────────────────────────
|
|
112
|
+
exports.SHADOW_REASON = Object.freeze({
|
|
113
|
+
NOT_SHADOWED: 'not_shadowed',
|
|
114
|
+
SCOPE_SHADOWED: 'scope_shadowed',
|
|
115
|
+
RESOLVER_UNAVAILABLE: 'resolver_unavailable',
|
|
116
|
+
});
|
|
117
|
+
// ── Sanitization ────────────────────────────────────────────────────────
|
|
118
|
+
/** CSI (`\x1b[...final`) and OSC (`\x1b]...BEL-or-ST`) sequences. An
|
|
119
|
+
* unterminated/malformed sequence is left for the C0-control strip below to
|
|
120
|
+
* remove the bare `\x1b` byte — liberal, never a throw. */
|
|
121
|
+
const ANSI_RE = /\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07\x1b]*(?:\x07|\x1b\\))/g;
|
|
122
|
+
/** C0 controls (`\x00`-`\x1f`, including any `\x1b` the ANSI strip above did
|
|
123
|
+
* not consume) and DEL/C1 (`\x7f`-`\x9f`). */
|
|
124
|
+
const CONTROL_RE = /[\x00-\x1f\x7f-\x9f]/g;
|
|
125
|
+
/** Unicode bidi embedding/override controls (U+202A-U+202E) and bidi
|
|
126
|
+
* isolates (U+2066-U+2069) — the RTL-spoofing class the design doc's row
|
|
127
|
+
* #13 names. */
|
|
128
|
+
const BIDI_RE = /[\u{202A}-\u{202E}\u{2066}-\u{2069}]/gu;
|
|
129
|
+
/** Combining marks (U+0300-U+036F) — "zalgo" text. Stacked onto the
|
|
130
|
+
* preceding base character, an unbounded run visually overflows into
|
|
131
|
+
* adjacent terminal cells/rows even though the string stays within the
|
|
132
|
+
* 64-char cap `readInstallManifest` enforces. Written as `\u{...}` escapes
|
|
133
|
+
* (not literal combining characters) so the source itself stays plain
|
|
134
|
+
* ASCII and does not visually combine in editors/diffs. */
|
|
135
|
+
const COMBINING_MARK_RE = /[\u{0300}-\u{036F}]/gu;
|
|
136
|
+
/** Zero-width characters: ZWSP (U+200B), ZWNJ (U+200C), ZWJ (U+200D), and
|
|
137
|
+
* BOM/ZWNBSP (U+FEFF). None of these are JS `\s`, so they survive both the
|
|
138
|
+
* char-count cap and the whitespace-collapse step below undetected. */
|
|
139
|
+
const ZERO_WIDTH_RE = /[\u{200B}-\u{200D}\u{FEFF}]/gu;
|
|
140
|
+
/**
|
|
141
|
+
* Sanitize a `declaredRuntime` (or any similarly attacker-influenceable
|
|
142
|
+
* string) for terminal/console rendering. `null` passes through as `null`;
|
|
143
|
+
* `''` passes through as `''`. Strips ANSI escapes, C0/C1 controls, Unicode
|
|
144
|
+
* bidi overrides/isolates, combining marks (zalgo), and zero-width
|
|
145
|
+
* characters (replacing each stripped run with nothing — never a space),
|
|
146
|
+
* then collapses any remaining whitespace run (including adjacent spaces
|
|
147
|
+
* left behind by a removed newline) to a single space and trims.
|
|
148
|
+
*
|
|
149
|
+
* Idempotent by construction: once ANSI/control/bidi/combining/zero-width
|
|
150
|
+
* bytes are gone and whitespace is collapsed to single internal spaces with
|
|
151
|
+
* no leading/trailing space, a second pass finds nothing left to strip or
|
|
152
|
+
* collapse. Pure character-class filter — never truncates; `readInstallManifest`
|
|
153
|
+
* already caps at 64 chars.
|
|
154
|
+
*/
|
|
155
|
+
function sanitizeForRender(value) {
|
|
156
|
+
if (value === null)
|
|
157
|
+
return null;
|
|
158
|
+
const stripped = value
|
|
159
|
+
.replace(ANSI_RE, '')
|
|
160
|
+
.replace(CONTROL_RE, '')
|
|
161
|
+
.replace(BIDI_RE, '')
|
|
162
|
+
.replace(COMBINING_MARK_RE, '')
|
|
163
|
+
.replace(ZERO_WIDTH_RE, '');
|
|
164
|
+
return stripped.replace(/\s+/g, ' ').trim();
|
|
165
|
+
}
|
|
166
|
+
// ── Per-scope truth filter helpers ─────────────────────────────────────
|
|
167
|
+
/**
|
|
168
|
+
* Build a `(scope, kind) -> prefix | null` lookup for one runtime, memoized
|
|
169
|
+
* per call to `buildShadowReport` (never shared across calls — matches this
|
|
170
|
+
* module's "fresh objects on every call" contract). `null` means "could not
|
|
171
|
+
* be resolved" (unknown scope record, or a `TypeError` from the layout
|
|
172
|
+
* resolver) — the caller treats that as "cannot honestly attribute this
|
|
173
|
+
* trigger to a real stem here", not as "assume it is fine".
|
|
174
|
+
*/
|
|
175
|
+
function buildPrefixLookup(runtime, scopeRecords, opts) {
|
|
176
|
+
const cache = new Map();
|
|
177
|
+
return (scope, kind) => {
|
|
178
|
+
const key = `${scope}:${kind}`;
|
|
179
|
+
if (cache.has(key))
|
|
180
|
+
return cache.get(key);
|
|
181
|
+
const record = scopeRecords.get(scope);
|
|
182
|
+
let prefix = null;
|
|
183
|
+
if (record) {
|
|
184
|
+
try {
|
|
185
|
+
const layout = opts.registry !== undefined
|
|
186
|
+
? resolveRuntimeArtifactLayoutFromRegistry(opts.registry, runtime, record.configHome, record.scope)
|
|
187
|
+
: resolveRuntimeArtifactLayout(runtime, record.configHome, record.scope);
|
|
188
|
+
const kindEntry = layout.kinds.find((k) => k.kind === kind);
|
|
189
|
+
prefix = kindEntry ? kindEntry.prefix : null;
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
// Unknown runtime / malformed registry — degrade to "cannot resolve",
|
|
193
|
+
// never throw out of a report builder (matches this module's own
|
|
194
|
+
// RESOLVER_UNAVAILABLE degrade-not-propagate posture above).
|
|
195
|
+
prefix = null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
cache.set(key, prefix);
|
|
199
|
+
return prefix;
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
/** `trigger` minus `prefix`, or `null` when `prefix` is unknown, does not
|
|
203
|
+
* actually prefix `trigger`, or the remainder would be empty (a `prefix`
|
|
204
|
+
* covering the whole trigger string is not a real stem). */
|
|
205
|
+
function stemFromTrigger(trigger, prefix) {
|
|
206
|
+
if (prefix === null || !trigger.startsWith(prefix))
|
|
207
|
+
return null;
|
|
208
|
+
const stem = trigger.slice(prefix.length);
|
|
209
|
+
return stem === '' ? null : stem;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* True when `t` (a `resolveTriggerSurface`-reported shadowed trigger) is a
|
|
213
|
+
* REAL cross-scope shadow: its stem is present in the winner's OWN scope
|
|
214
|
+
* `stems` and, independently, in the shadowed side's OWN scope `stems`. See
|
|
215
|
+
* the module-level "Per-scope truth filter" comment for why this check
|
|
216
|
+
* exists and why it lives here rather than in the resolver.
|
|
217
|
+
*/
|
|
218
|
+
function isGenuinelyShadowed(t, scopeRecords, prefixFor) {
|
|
219
|
+
if (t.shadowedBy === null)
|
|
220
|
+
return false;
|
|
221
|
+
const winnerRecord = scopeRecords.get(t.shadowedBy.scope);
|
|
222
|
+
const shadowedRecord = scopeRecords.get(t.scope);
|
|
223
|
+
const winnerStem = stemFromTrigger(t.trigger, prefixFor(t.shadowedBy.scope, t.shadowedBy.kind));
|
|
224
|
+
const shadowedStem = stemFromTrigger(t.trigger, prefixFor(t.scope, t.kind));
|
|
225
|
+
if (winnerStem === null || shadowedStem === null)
|
|
226
|
+
return false;
|
|
227
|
+
return (winnerRecord?.stems ?? []).includes(winnerStem) && (shadowedRecord?.stems ?? []).includes(shadowedStem);
|
|
228
|
+
}
|
|
229
|
+
// ── Report builder ──────────────────────────────────────────────────────
|
|
230
|
+
/**
|
|
231
|
+
* Build a shadow report for one runtime. `opts` is forwarded VERBATIM to
|
|
232
|
+
* `resolveInstalledSurfaces` — this function adds no option of its own.
|
|
233
|
+
* Production call shape: `buildShadowReport('claude', { home, cwd })`.
|
|
234
|
+
*
|
|
235
|
+
* A `resolveInstalledSurfaces` `TypeError` (unknown runtime, or
|
|
236
|
+
* `configHome.kind === 'none'`, e.g. vscode — design row #7) degrades to
|
|
237
|
+
* `reason: RESOLVER_UNAVAILABLE` rather than propagating: an install-time or
|
|
238
|
+
* `/gsd-health` caller must never crash because a runtime has no installable
|
|
239
|
+
* config directory. Any other error type is rethrown — mirrors the
|
|
240
|
+
* resolver's own `TypeError` narrowing (`resolveInstalledSurfaces`'s sweep
|
|
241
|
+
* catch, and `buildScopeRecord`'s stem-derivation catch) so the two cannot
|
|
242
|
+
* drift apart.
|
|
243
|
+
*/
|
|
244
|
+
function buildShadowReport(runtime, opts = {}) {
|
|
245
|
+
let surfaces;
|
|
246
|
+
try {
|
|
247
|
+
surfaces = (0, installed_surface_resolver_cjs_1.resolveInstalledSurfaces)(runtime, opts);
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
if (!(error instanceof TypeError))
|
|
251
|
+
throw error;
|
|
252
|
+
return {
|
|
253
|
+
runtime,
|
|
254
|
+
reason: exports.SHADOW_REASON.RESOLVER_UNAVAILABLE,
|
|
255
|
+
shadowed: false,
|
|
256
|
+
winner: null,
|
|
257
|
+
shadowedSide: null,
|
|
258
|
+
kindsDiffer: false,
|
|
259
|
+
triggers: [],
|
|
260
|
+
mismatches: [],
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
// resolveInstalledSurfaces(runtime, opts) with an explicit string `runtime`
|
|
264
|
+
// always returns exactly one element (see its own doc comment).
|
|
265
|
+
const surface = surfaces[0];
|
|
266
|
+
// Per-scope truth filter (see module comment): `surface.triggers` may
|
|
267
|
+
// contain candidates synthesized from the CROSS-SCOPE stem union
|
|
268
|
+
// (`installed-surface-resolver.cts`'s `stemUnion`) that do not correspond
|
|
269
|
+
// to a real artifact at one or both scopes. Only a trigger whose stem is
|
|
270
|
+
// provably present in BOTH the winner's own `stems` and the shadowed
|
|
271
|
+
// side's own `stems` is reported.
|
|
272
|
+
const scopeRecords = new Map(surface.scopes.map((r) => [r.scope, r]));
|
|
273
|
+
const prefixFor = buildPrefixLookup(runtime, scopeRecords, opts);
|
|
274
|
+
const shadowedSurfaces = surface.triggers.filter((t) => isGenuinelyShadowed(t, scopeRecords, prefixFor));
|
|
275
|
+
const triggers = shadowedSurfaces
|
|
276
|
+
.map((t) => ({
|
|
277
|
+
trigger: t.trigger,
|
|
278
|
+
// `shadowedBy` is non-null by construction of the filter above.
|
|
279
|
+
winnerKind: t.shadowedBy.kind,
|
|
280
|
+
winnerScope: t.shadowedBy.scope,
|
|
281
|
+
shadowedKind: t.kind,
|
|
282
|
+
shadowedScope: t.scope,
|
|
283
|
+
}))
|
|
284
|
+
.sort((a, b) => (a.trigger < b.trigger ? -1 : a.trigger > b.trigger ? 1 : 0));
|
|
285
|
+
const mismatches = [];
|
|
286
|
+
for (const record of surface.scopes) {
|
|
287
|
+
if (record.declaredRuntimeMatchesProbe === false || record.declaredScopeMatchesProbe === false) {
|
|
288
|
+
mismatches.push({
|
|
289
|
+
scope: record.scope,
|
|
290
|
+
// Postel's Law (design doc): sanitized here because this is the
|
|
291
|
+
// render seam — never silently absorbed, always surfaced.
|
|
292
|
+
declaredRuntime: sanitizeForRender(record.declaredRuntime),
|
|
293
|
+
declaredRuntimeMatchesProbe: record.declaredRuntimeMatchesProbe,
|
|
294
|
+
declaredScope: record.declaredScope,
|
|
295
|
+
declaredScopeMatchesProbe: record.declaredScopeMatchesProbe,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (triggers.length === 0) {
|
|
300
|
+
return {
|
|
301
|
+
runtime,
|
|
302
|
+
reason: exports.SHADOW_REASON.NOT_SHADOWED,
|
|
303
|
+
shadowed: false,
|
|
304
|
+
winner: null,
|
|
305
|
+
shadowedSide: null,
|
|
306
|
+
kindsDiffer: false,
|
|
307
|
+
triggers: [],
|
|
308
|
+
mismatches,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
// Winner/shadowedSide are the (kind,scope) pair of the FIRST shadowed
|
|
312
|
+
// trigger (post-sort, for the same determinism reason the array itself is
|
|
313
|
+
// sorted). They are asserted-by-construction uniform across the whole set
|
|
314
|
+
// for every runtime this module has seen (every trigger shadowed by the
|
|
315
|
+
// SAME scope, with the SAME two kinds, on one machine) — but if a future
|
|
316
|
+
// registry shape ever produced a non-uniform set, this still returns the
|
|
317
|
+
// first pair rather than throwing; every distinct (kind,scope) pair is
|
|
318
|
+
// already visible per-entry in `triggers` itself, so nothing is lost.
|
|
319
|
+
const first = triggers[0];
|
|
320
|
+
const winner = { kind: first.winnerKind, scope: first.winnerScope };
|
|
321
|
+
const shadowedSide = { kind: first.shadowedKind, scope: first.shadowedScope };
|
|
322
|
+
return {
|
|
323
|
+
runtime,
|
|
324
|
+
reason: exports.SHADOW_REASON.SCOPE_SHADOWED,
|
|
325
|
+
shadowed: true,
|
|
326
|
+
winner,
|
|
327
|
+
shadowedSide,
|
|
328
|
+
kindsDiffer: winner.kind !== shadowedSide.kind,
|
|
329
|
+
triggers,
|
|
330
|
+
mismatches,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
// ── Renderer ────────────────────────────────────────────────────────────
|
|
334
|
+
/**
|
|
335
|
+
* Render a `ShadowReport` to plain lines — no ANSI, no color, no leading
|
|
336
|
+
* indent. The caller (installer console output, `/gsd-health` text mode)
|
|
337
|
+
* owns terminal formatting; this keeps the module free of terminal concerns
|
|
338
|
+
* and testable without a spawned process. Structured (`--json`) health
|
|
339
|
+
* output (design row #17) consumes the typed `ShadowReport` directly and
|
|
340
|
+
* never calls this function.
|
|
341
|
+
*
|
|
342
|
+
* `reason !== SCOPE_SHADOWED` renders nothing — there is nothing to report
|
|
343
|
+
* (design rows #1, #2, #6, #7, #8, #11).
|
|
344
|
+
*/
|
|
345
|
+
function renderShadowReport(report, opts = {}) {
|
|
346
|
+
if (report.reason !== exports.SHADOW_REASON.SCOPE_SHADOWED || report.winner === null || report.shadowedSide === null) {
|
|
347
|
+
return [];
|
|
348
|
+
}
|
|
349
|
+
const sampleLimit = opts.sampleLimit ?? 5;
|
|
350
|
+
const count = report.triggers.length;
|
|
351
|
+
const plural = count === 1 ? '' : 's';
|
|
352
|
+
const { winner, shadowedSide, kindsDiffer } = report;
|
|
353
|
+
const lines = [];
|
|
354
|
+
lines.push(kindsDiffer
|
|
355
|
+
? `${count} trigger${plural} shadowed: the ${shadowedSide.scope} ${shadowedSide.kind} surface is unreachable through ${count === 1 ? 'that trigger' : 'those triggers'} — ${winner.scope} ${winner.kind} wins instead.`
|
|
356
|
+
: `${count} trigger${plural} shadowed: the ${shadowedSide.scope} ${shadowedSide.kind} ${count === 1 ? 'entry is' : 'entries are'} overridden by ${winner.scope} ${winner.kind}.`);
|
|
357
|
+
// Trigger names in `report.triggers` are already SAFE_STEM-gated upstream
|
|
358
|
+
// (installed-surface-resolver.cts's deriveStemsForKindEntry) — no re-gating
|
|
359
|
+
// needed here.
|
|
360
|
+
const sample = report.triggers.slice(0, sampleLimit);
|
|
361
|
+
for (const t of sample) {
|
|
362
|
+
lines.push(` - ${t.trigger}: ${t.shadowedScope}/${t.shadowedKind} shadowed by ${t.winnerScope}/${t.winnerKind}`);
|
|
363
|
+
}
|
|
364
|
+
const remaining = count - sample.length;
|
|
365
|
+
if (remaining > 0) {
|
|
366
|
+
lines.push(` ...and ${remaining} more`);
|
|
367
|
+
}
|
|
368
|
+
for (const m of report.mismatches) {
|
|
369
|
+
// Re-sanitized defensively: `buildShadowReport` already sanitizes
|
|
370
|
+
// `declaredRuntime` before it reaches a `ShadowReport`, and
|
|
371
|
+
// `sanitizeForRender` is idempotent, so this is a no-op in the normal
|
|
372
|
+
// path and a real guard against a hand-built `ShadowReport` (e.g. a
|
|
373
|
+
// renderer-only test) that skipped it.
|
|
374
|
+
const declaredRuntime = sanitizeForRender(m.declaredRuntime);
|
|
375
|
+
const parts = [];
|
|
376
|
+
if (m.declaredRuntimeMatchesProbe === false) {
|
|
377
|
+
parts.push(`declared runtime "${declaredRuntime}" does not match this runtime`);
|
|
378
|
+
}
|
|
379
|
+
if (m.declaredScopeMatchesProbe === false) {
|
|
380
|
+
parts.push(`declared scope "${m.declaredScope}" does not match the probed ${m.scope} scope`);
|
|
381
|
+
}
|
|
382
|
+
lines.push(`Note: ${m.scope} scope manifest mismatch — ${parts.join('; ')}.`);
|
|
383
|
+
}
|
|
384
|
+
return lines;
|
|
385
|
+
}
|