@opengsd/gsd-core 1.11.0 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +12 -0
- package/agents/gsd-code-fixer.md +1 -1
- package/agents/gsd-debug-session-manager.md +1 -1
- package/agents/gsd-debugger.md +1 -1
- package/agents/gsd-dom-verifier.md +169 -0
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +78 -42
- package/agents/gsd-framework-selector.md +1 -3
- package/agents/gsd-intel-updater.md +1 -1
- package/agents/gsd-mempalace-curator.md +0 -1
- package/agents/gsd-pattern-mapper.md +11 -0
- package/agents/gsd-phase-researcher.md +3 -1
- package/agents/gsd-plan-checker.md +91 -112
- package/agents/gsd-planner.md +20 -4
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.md +2 -2
- package/agents/gsd-roadmapper.md +15 -11
- package/agents/gsd-ui-checker.md +82 -7
- package/agents/gsd-ui-researcher.md +70 -3
- package/agents/gsd-verifier.md +24 -2
- package/bin/install.js +847 -200
- package/commands/gsd/discuss-phase.md +1 -1
- package/commands/gsd/execute-phase.md +1 -1
- package/commands/gsd/import.md +1 -1
- package/commands/gsd/ns-workflow.md +2 -1
- package/commands/gsd/phase.md +1 -1
- package/commands/gsd/quick-batch.md +105 -0
- package/commands/gsd/quick.md +8 -4
- package/commands/gsd/surface.md +18 -8
- package/gsd-core/bin/gsd-tools.cjs +761 -100
- package/gsd-core/bin/lib/active-workstream-store.cjs +8 -0
- package/gsd-core/bin/lib/adr-parser.cjs +13 -7
- package/gsd-core/bin/lib/agent-install-check.cjs +162 -0
- package/gsd-core/bin/lib/api-coverage.cjs +30 -9
- package/gsd-core/bin/lib/artifacts.cjs +2 -0
- package/gsd-core/bin/lib/assumption-delta.cjs +30 -11
- package/gsd-core/bin/lib/audit.cjs +163 -41
- package/gsd-core/bin/lib/broken-windows.cjs +306 -28
- package/gsd-core/bin/lib/capability-activation.cjs +27 -0
- package/gsd-core/bin/lib/capability-lock.cjs +10 -4
- package/gsd-core/bin/lib/capability-registry.cjs +785 -144
- package/gsd-core/bin/lib/capability-state.cjs +25 -4
- package/gsd-core/bin/lib/capability-validator.cjs +321 -18
- package/gsd-core/bin/lib/capability-writer.cjs +14 -4
- package/gsd-core/bin/lib/check-command-router.cjs +229 -6
- package/gsd-core/bin/lib/claude-orchestration.cjs +10 -25
- package/gsd-core/bin/lib/cli-exit.cjs +496 -10
- package/gsd-core/bin/lib/clusters.cjs +1 -0
- package/gsd-core/bin/lib/code-review-depth.cjs +288 -0
- package/gsd-core/bin/lib/codex-agent-toml.cjs +410 -4
- package/gsd-core/bin/lib/command-aliases.cjs +16 -0
- package/gsd-core/bin/lib/command-arg-projection.cjs +144 -14
- package/gsd-core/bin/lib/command-routing-hub.cjs +31 -2
- package/gsd-core/bin/lib/commands.cjs +877 -54
- package/gsd-core/bin/lib/complexity-trigger.cjs +26 -6
- package/gsd-core/bin/lib/config-loader.cjs +121 -29
- package/gsd-core/bin/lib/config.cjs +92 -2
- package/gsd-core/bin/lib/configuration.cjs +129 -37
- package/gsd-core/bin/lib/core-utils.cjs +118 -14
- package/gsd-core/bin/lib/decisions.cjs +213 -1
- package/gsd-core/bin/lib/edge-probe.cjs +23 -2
- package/gsd-core/bin/lib/estimate-cli.cjs +55 -11
- package/gsd-core/bin/lib/exit-code-registry.cjs +98 -0
- package/gsd-core/bin/lib/file-overlap-partitioner.cjs +74 -0
- package/gsd-core/bin/lib/frontmatter.cjs +975 -326
- package/gsd-core/bin/lib/gap-checker.cjs +41 -8
- package/gsd-core/bin/lib/git-base-branch.cjs +182 -39
- package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +7 -3
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +8 -2
- package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +60 -14
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +75 -22
- package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +22 -8
- package/gsd-core/bin/lib/health-diagnostic.cjs +23 -3
- package/gsd-core/bin/lib/host-integration.cjs +96 -11
- package/gsd-core/bin/lib/init-command-router.cjs +132 -21
- package/gsd-core/bin/lib/init.cjs +252 -56
- package/gsd-core/bin/lib/install-engine.cjs +252 -15
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +78 -1
- package/gsd-core/bin/lib/install-profiles.cjs +100 -18
- package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
- package/gsd-core/bin/lib/installer-migrations/010-antigravity-retire-confighome-artifacts.cjs +169 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +10 -7
- package/gsd-core/bin/lib/intel.cjs +101 -26
- package/gsd-core/bin/lib/io.cjs +195 -15
- package/gsd-core/bin/lib/learnings.cjs +85 -14
- package/gsd-core/bin/lib/legacy-cleanup.cjs +8 -2
- package/gsd-core/bin/lib/loop-resolver.cjs +14 -8
- package/gsd-core/bin/lib/markdown-table.cjs +175 -4
- package/gsd-core/bin/lib/milestone.cjs +112 -7
- package/gsd-core/bin/lib/model-catalog.cjs +177 -19
- package/gsd-core/bin/lib/model-resolver.cjs +10 -28
- package/gsd-core/bin/lib/onboard-projection.cjs +5 -1
- package/gsd-core/bin/lib/phase-command-router.cjs +13 -6
- package/gsd-core/bin/lib/phase-estimation.cjs +17 -8
- package/gsd-core/bin/lib/phase-id.cjs +321 -13
- package/gsd-core/bin/lib/phase-lifecycle.cjs +24 -16
- package/gsd-core/bin/lib/phase-locator.cjs +138 -17
- package/gsd-core/bin/lib/phase.cjs +1175 -115
- package/gsd-core/bin/lib/plan-document.cjs +273 -0
- package/gsd-core/bin/lib/plan-scan.cjs +13 -2
- package/gsd-core/bin/lib/planning-command-router.cjs +61 -0
- package/gsd-core/bin/lib/planning-inspect.cjs +1168 -0
- package/gsd-core/bin/lib/planning-snapshot.cjs +165 -34
- package/gsd-core/bin/lib/planning-workspace.cjs +159 -28
- package/gsd-core/bin/lib/probe-core.cjs +4 -1
- package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +50 -7
- package/gsd-core/bin/lib/profile-pipeline.cjs +6 -3
- package/gsd-core/bin/lib/quick-batch-command-router.cjs +285 -0
- package/gsd-core/bin/lib/quick-batch-dispatch.cjs +250 -0
- package/gsd-core/bin/lib/quick-batch.cjs +840 -0
- package/gsd-core/bin/lib/real-home-guard.cjs +419 -0
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +71 -45
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +62 -14
- package/gsd-core/bin/lib/review-lane-invocation.cjs +73 -1
- package/gsd-core/bin/lib/review-lane-runner.cjs +136 -10
- package/gsd-core/bin/lib/roadmap-command-router.cjs +45 -31
- package/gsd-core/bin/lib/roadmap-parser.cjs +577 -41
- package/gsd-core/bin/lib/roadmap.cjs +248 -64
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +329 -41
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +16 -17
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +320 -109
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +487 -83
- package/gsd-core/bin/lib/runtime-identity.cjs +234 -0
- package/gsd-core/bin/lib/runtime-slash.cjs +72 -2
- package/gsd-core/bin/lib/shell-command-projection.cjs +75 -8
- package/gsd-core/bin/lib/smart-entry.cjs +19 -31
- package/gsd-core/bin/lib/spec-section.cjs +12 -7
- package/gsd-core/bin/lib/state-command-router.cjs +47 -18
- package/gsd-core/bin/lib/state-contract.cjs +359 -0
- package/gsd-core/bin/lib/state-document.cjs +216 -5
- package/gsd-core/bin/lib/state-md-schema.cjs +231 -0
- package/gsd-core/bin/lib/state-transition.cjs +850 -145
- package/gsd-core/bin/lib/state.cjs +1629 -287
- package/gsd-core/bin/lib/surface.cjs +33 -10
- package/gsd-core/bin/lib/task-command-router.cjs +111 -1
- package/gsd-core/bin/lib/task-content-resolution.cjs +368 -0
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +133 -0
- package/gsd-core/bin/lib/teams-status.cjs +4 -1
- package/gsd-core/bin/lib/uat-predicate.cjs +58 -20
- package/gsd-core/bin/lib/uat.cjs +2542 -387
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +9 -1
- package/gsd-core/bin/lib/ui-safety-gate.cjs +37 -7
- package/gsd-core/bin/lib/unusable-input.cjs +13 -0
- package/gsd-core/bin/lib/update-context.cjs +6 -2
- package/gsd-core/bin/lib/validate-command-router.cjs +2 -2
- package/gsd-core/bin/lib/validate.cjs +230 -12
- package/gsd-core/bin/lib/vendor/README.md +43 -5
- package/gsd-core/bin/lib/vendor/js-yaml.cjs +3014 -0
- package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
- package/gsd-core/bin/lib/verification.cjs +287 -13
- package/gsd-core/bin/lib/verify-command-grounding.cjs +846 -0
- package/gsd-core/bin/lib/verify-command-router.cjs +1 -0
- package/gsd-core/bin/lib/verify.cjs +441 -56
- package/gsd-core/bin/lib/workstream-inventory.cjs +20 -2
- package/gsd-core/bin/lib/workstream-name-policy.cjs +25 -4
- package/gsd-core/bin/lib/worktree-base-ref.cjs +66 -12
- package/gsd-core/bin/lib/worktree-safety.cjs +185 -21
- package/gsd-core/bin/shared/config-defaults.manifest.json +7 -1
- package/gsd-core/bin/shared/config-schema.manifest.json +13 -0
- package/gsd-core/bin/shared/exit-codes.json +8 -0
- package/gsd-core/bin/shared/exit-codes.sh +20 -0
- package/gsd-core/bin/shared/model-catalog.json +8 -1
- package/gsd-core/bin/verify-reapply-patches.cjs +70 -3
- package/gsd-core/references/agent-contracts.md +6 -5
- package/gsd-core/references/api-coverage.md +24 -2
- package/gsd-core/references/autonomous-smart-discuss.md +3 -3
- package/gsd-core/references/checkpoints.md +37 -19
- package/gsd-core/references/decimal-phase-calculation.md +5 -5
- package/gsd-core/references/edge-probe.md +17 -5
- package/gsd-core/references/execute-mvp-tdd.md +18 -18
- package/gsd-core/references/execute-phase-between-wave-reset.md +9 -12
- package/gsd-core/references/execute-phase-response-language.md +6 -0
- package/gsd-core/references/execute-phase-wave-guard.md +11 -9
- package/gsd-core/references/executor-examples.md +42 -0
- package/gsd-core/references/failing-direction.md +78 -0
- package/gsd-core/references/few-shot-examples/plan-checker.md +15 -15
- package/gsd-core/references/gate-prompts.md +1 -1
- package/gsd-core/references/git-integration.md +5 -5
- package/gsd-core/references/git-planning-commit.md +3 -3
- package/gsd-core/references/gsd-run-resolver.md +1 -1
- package/gsd-core/references/loop-hook-dispatch.md +22 -0
- package/gsd-core/references/model-profiles.md +1 -1
- package/gsd-core/references/mvp-concepts.md +2 -2
- package/gsd-core/references/nyquist-compliance.md +74 -0
- package/gsd-core/references/offer-next.md +3 -5
- package/gsd-core/references/phase-argument-parsing.md +3 -3
- package/gsd-core/references/plan-checker-examples.md +41 -0
- package/gsd-core/references/planner-antipatterns.md +25 -0
- package/gsd-core/references/planner-chunked.md +5 -1
- package/gsd-core/references/planner-coupling.md +42 -0
- package/gsd-core/references/planner-failing-direction.md +53 -0
- package/gsd-core/references/planner-human-verify-mode.md +15 -1
- package/gsd-core/references/planner-quick-batch.md +71 -0
- package/gsd-core/references/planner-reviews.md +47 -0
- package/gsd-core/references/planner-revision.md +76 -3
- package/gsd-core/references/planner-verify-command-grounding.md +17 -0
- package/gsd-core/references/planning-config.md +39 -9
- package/gsd-core/references/response-language-directive.md +9 -0
- package/gsd-core/references/reviewer-instances.md +31 -0
- package/gsd-core/references/revision-loop.md +118 -11
- package/gsd-core/references/runtime-aware-dispatch.md +1 -1
- package/gsd-core/references/tdd.md +15 -12
- package/gsd-core/references/ui-brand.md +65 -21
- package/gsd-core/references/ui-consideration-probe.md +1 -1
- package/gsd-core/references/universal-anti-patterns.md +2 -2
- package/gsd-core/references/verifier-evidence-gate.md +160 -0
- package/gsd-core/references/verify-command-path-resolvability.md +42 -0
- package/gsd-core/references/verify-mvp-mode.md +1 -1
- package/gsd-core/references/workstream-flag.md +11 -11
- package/gsd-core/templates/README.md +1 -1
- package/gsd-core/templates/SECURITY.md +3 -3
- package/gsd-core/templates/UI-SPEC.md +25 -3
- package/gsd-core/templates/VALIDATION.md +3 -3
- package/gsd-core/templates/phase-prompt.md +7 -0
- package/gsd-core/templates/state.md +7 -0
- package/gsd-core/templates/verification-report.md +5 -0
- package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/gsd-core/workflows/add-backlog.md +3 -1
- package/gsd-core/workflows/add-phase.md +5 -3
- package/gsd-core/workflows/add-tests.md +4 -9
- package/gsd-core/workflows/add-todo.md +2 -2
- package/gsd-core/workflows/ai-integration-phase.md +5 -10
- package/gsd-core/workflows/analyze-dependencies.md +2 -0
- package/gsd-core/workflows/audit-fix.md +14 -3
- package/gsd-core/workflows/audit-milestone.md +11 -9
- package/gsd-core/workflows/audit-uat.md +19 -2
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +2 -2
- package/gsd-core/workflows/autonomous.md +12 -26
- package/gsd-core/workflows/check-todos.md +2 -2
- package/gsd-core/workflows/cleanup.md +3 -3
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +16 -14
- package/gsd-core/workflows/code-review-fix.md +3 -1
- package/gsd-core/workflows/code-review.md +192 -69
- package/gsd-core/workflows/complete-milestone.md +28 -14
- package/gsd-core/workflows/debug.md +6 -4
- package/gsd-core/workflows/diagnose-issues.md +17 -7
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +3 -1
- package/gsd-core/workflows/discuss-phase/modes/all.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/analyze.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/auto.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/batch.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/chain.md +5 -7
- package/gsd-core/workflows/discuss-phase/modes/default.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/power.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/text.md +3 -1
- package/gsd-core/workflows/discuss-phase/templates/context.md +2 -0
- package/gsd-core/workflows/discuss-phase/templates/discussion-log.md +2 -0
- package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +1 -3
- package/gsd-core/workflows/discuss-phase-assumptions.md +3 -3
- package/gsd-core/workflows/discuss-phase-power.md +2 -0
- package/gsd-core/workflows/discuss-phase.md +2 -2
- package/gsd-core/workflows/do.md +46 -19
- package/gsd-core/workflows/docs-update.md +6 -5
- package/gsd-core/workflows/edit-phase.md +3 -1
- package/gsd-core/workflows/eval-review.md +5 -10
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +3 -1
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +129 -11
- package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
- 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 +29 -5
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +2 -2
- package/gsd-core/workflows/execute-phase/steps/protected-branch.md +21 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +4 -2
- package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +25 -0
- package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +39 -0
- package/gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md +2 -0
- package/gsd-core/workflows/execute-phase.md +68 -66
- package/gsd-core/workflows/execute-plan.md +25 -20
- package/gsd-core/workflows/explore.md +3 -1
- package/gsd-core/workflows/extract-learnings.md +3 -1
- package/gsd-core/workflows/fast.md +8 -2
- package/gsd-core/workflows/forensics.md +3 -1
- package/gsd-core/workflows/graduation.md +6 -6
- package/gsd-core/workflows/health.md +4 -7
- package/gsd-core/workflows/help/modes/brief.md +2 -0
- package/gsd-core/workflows/help/modes/default.md +2 -0
- package/gsd-core/workflows/help/modes/full.md +12 -0
- package/gsd-core/workflows/help/modes/topic.md +2 -0
- package/gsd-core/workflows/help.md +2 -0
- package/gsd-core/workflows/import.md +17 -14
- package/gsd-core/workflows/inbox.md +5 -6
- package/gsd-core/workflows/ingest-docs.md +45 -12
- package/gsd-core/workflows/insert-phase.md +7 -5
- package/gsd-core/workflows/list-phase-assumptions.md +2 -0
- package/gsd-core/workflows/list-seeds.md +7 -3
- package/gsd-core/workflows/list-workspaces.md +3 -1
- package/gsd-core/workflows/manager.md +15 -26
- package/gsd-core/workflows/map-codebase.md +3 -1
- package/gsd-core/workflows/milestone-summary.md +3 -1
- package/gsd-core/workflows/mvp-phase.md +3 -3
- package/gsd-core/workflows/new-milestone.md +10 -22
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
- package/gsd-core/workflows/new-project.md +17 -29
- package/gsd-core/workflows/new-workspace.md +2 -2
- package/gsd-core/workflows/next.md +4 -2
- package/gsd-core/workflows/node-repair.md +2 -0
- package/gsd-core/workflows/note.md +2 -0
- package/gsd-core/workflows/onboard.md +1 -1
- package/gsd-core/workflows/pause-work.md +20 -5
- package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +100 -18
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +4 -4
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +12 -3
- package/gsd-core/workflows/plan-phase.md +251 -54
- package/gsd-core/workflows/plan-review-convergence.md +148 -19
- package/gsd-core/workflows/plant-seed.md +3 -3
- package/gsd-core/workflows/pr-branch.md +195 -51
- package/gsd-core/workflows/profile-user.md +17 -15
- package/gsd-core/workflows/progress/steps/forensic-audit.md +1 -1
- package/gsd-core/workflows/progress.md +52 -15
- package/gsd-core/workflows/quick/steps/discussion-phase.md +1 -3
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +38 -5
- package/gsd-core/workflows/quick/steps/quick-verification.md +2 -4
- package/gsd-core/workflows/quick/steps/research-phase.md +5 -7
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +3 -3
- package/gsd-core/workflows/quick-batch/steps/batch-init.md +55 -0
- package/gsd-core/workflows/quick-batch/steps/completion.md +65 -0
- package/gsd-core/workflows/quick-batch/steps/merge-wave.md +100 -0
- package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +147 -0
- package/gsd-core/workflows/quick-batch/steps/planner-wave.md +158 -0
- package/gsd-core/workflows/quick-batch/steps/research-phase.md +95 -0
- package/gsd-core/workflows/quick-batch/steps/resume-mode.md +49 -0
- package/gsd-core/workflows/quick-batch/steps/verification-wave.md +73 -0
- package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +169 -0
- package/gsd-core/workflows/quick-batch.md +203 -0
- package/gsd-core/workflows/quick.md +33 -32
- package/gsd-core/workflows/reapply-patches.md +2 -0
- package/gsd-core/workflows/remove-phase.md +6 -4
- package/gsd-core/workflows/remove-workspace.md +3 -3
- package/gsd-core/workflows/resume-project.md +14 -14
- package/gsd-core/workflows/review.md +404 -21
- package/gsd-core/workflows/scan.md +3 -1
- package/gsd-core/workflows/section-manifest.json +12 -0
- package/gsd-core/workflows/secure-phase.md +3 -3
- package/gsd-core/workflows/session-report.md +2 -0
- package/gsd-core/workflows/settings-advanced.md +9 -9
- package/gsd-core/workflows/settings-integrations.md +66 -32
- package/gsd-core/workflows/settings.md +4 -6
- package/gsd-core/workflows/ship.md +22 -16
- package/gsd-core/workflows/sketch-wrap-up.md +13 -17
- package/gsd-core/workflows/sketch.md +13 -19
- package/gsd-core/workflows/smart-entry.md +4 -6
- package/gsd-core/workflows/spec-phase.md +31 -4
- package/gsd-core/workflows/spike-wrap-up.md +9 -11
- package/gsd-core/workflows/spike.md +21 -32
- package/gsd-core/workflows/stats.md +4 -2
- package/gsd-core/workflows/sync-skills.md +13 -5
- package/gsd-core/workflows/thread.md +13 -7
- package/gsd-core/workflows/transition.md +7 -5
- package/gsd-core/workflows/ui-phase.md +36 -21
- package/gsd-core/workflows/ui-review.md +7 -11
- package/gsd-core/workflows/ultraplan-phase.md +7 -13
- package/gsd-core/workflows/undo.md +9 -17
- package/gsd-core/workflows/update.md +47 -48
- package/gsd-core/workflows/validate-phase.md +3 -3
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +25 -1
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
- package/gsd-core/workflows/verify-work.md +106 -21
- package/hooks/dist/gsd-agent-isolation-guard.js +77 -38
- package/hooks/dist/gsd-check-update-worker.js +19 -2
- package/hooks/dist/gsd-config-reload.js +18 -12
- package/hooks/dist/gsd-context-monitor.js +302 -22
- package/hooks/dist/gsd-cursor-post-tool.js +3 -1
- package/hooks/dist/gsd-cursor-pre-tool.js +3 -1
- package/hooks/dist/gsd-cursor-session-start.js +2 -1
- package/hooks/dist/gsd-cursor-stop.js +2 -1
- package/hooks/dist/gsd-cursor-subagent-start.js +28 -23
- package/hooks/dist/gsd-cursor-subagent-stop.js +3 -1
- package/hooks/dist/gsd-ensure-canonical-path.js +2 -1
- package/hooks/dist/gsd-graphify-update.sh +22 -18
- package/hooks/dist/gsd-node-runner.sh +77 -0
- package/hooks/dist/gsd-phase-boundary.sh +1 -0
- package/hooks/dist/gsd-prompt-guard.js +46 -12
- package/hooks/dist/gsd-read-guard.js +18 -7
- package/hooks/dist/gsd-read-injection-scanner.js +22 -13
- package/hooks/dist/gsd-secret-read-guard.js +1079 -0
- package/hooks/dist/gsd-session-state.sh +1 -0
- package/hooks/dist/gsd-statusline.js +222 -29
- package/hooks/dist/gsd-validate-commit.sh +523 -12
- package/hooks/dist/gsd-windsurf-pre-command.js +16 -11
- package/hooks/dist/gsd-windsurf-pre-write.js +22 -13
- package/hooks/dist/gsd-workflow-guard.js +36 -17
- package/hooks/dist/gsd-worktree-path-guard.js +36 -21
- package/hooks/dist/gsd-write-guard.js +35 -25
- package/hooks/dist/lib/cli-exit.js +560 -0
- package/hooks/dist/lib/exit-code-registry.js +98 -0
- package/hooks/dist/lib/git-cmd.js +210 -1
- package/hooks/dist/lib/git-probe.js +84 -0
- package/hooks/dist/lib/hook-exit.js +81 -0
- package/hooks/dist/lib/injection-patterns.js +36 -6
- package/hooks/dist/managed-hooks-registry.cjs +4 -0
- package/hooks/gsd-agent-isolation-guard.js +77 -38
- package/hooks/gsd-check-update-worker.js +19 -2
- package/hooks/gsd-config-reload.js +18 -12
- package/hooks/gsd-context-monitor.js +302 -22
- package/hooks/gsd-cursor-post-tool.js +3 -1
- package/hooks/gsd-cursor-pre-tool.js +3 -1
- package/hooks/gsd-cursor-session-start.js +2 -1
- package/hooks/gsd-cursor-stop.js +2 -1
- package/hooks/gsd-cursor-subagent-start.js +28 -23
- package/hooks/gsd-cursor-subagent-stop.js +3 -1
- package/hooks/gsd-ensure-canonical-path.js +2 -1
- package/hooks/gsd-graphify-update.sh +22 -18
- package/hooks/gsd-node-runner.sh +77 -0
- package/hooks/gsd-phase-boundary.sh +1 -0
- package/hooks/gsd-prompt-guard.js +46 -12
- package/hooks/gsd-read-guard.js +18 -7
- package/hooks/gsd-read-injection-scanner.js +22 -13
- package/hooks/gsd-secret-read-guard.js +1079 -0
- package/hooks/gsd-session-state.sh +1 -0
- package/hooks/gsd-statusline.js +222 -29
- package/hooks/gsd-validate-commit.sh +523 -12
- package/hooks/gsd-windsurf-pre-command.js +16 -11
- package/hooks/gsd-windsurf-pre-write.js +22 -13
- package/hooks/gsd-workflow-guard.js +36 -17
- package/hooks/gsd-worktree-path-guard.js +36 -21
- package/hooks/gsd-write-guard.js +35 -25
- package/hooks/hooks.json +6 -0
- package/hooks/lib/cli-exit.js +560 -0
- package/hooks/lib/exit-code-registry.js +98 -0
- package/hooks/lib/git-cmd.js +210 -1
- package/hooks/lib/git-probe.js +84 -0
- package/hooks/lib/hook-exit.js +81 -0
- package/hooks/lib/injection-patterns.js +36 -6
- package/hooks/managed-hooks-registry.cjs +4 -0
- package/package.json +14 -9
- package/scripts/base64-scan.sh +74 -12
- package/scripts/build-hooks.js +12 -0
- package/scripts/check-glossary-refs.cjs +77 -15
- package/scripts/check-mutation-score-ratchet.cjs +156 -0
- package/scripts/ci-check-job-near-cap.cjs +49 -0
- package/scripts/ci-pr-mergeability.cjs +262 -0
- package/scripts/ci-test-scope.cjs +52 -12
- package/scripts/ci-timeout-report.cjs +230 -0
- package/scripts/docs-guard-registry.cjs +406 -0
- package/scripts/gen-capability-registry.cjs +8 -6
- package/scripts/gen-exit-code-docs.cjs +318 -0
- package/scripts/gen-exit-code-registry.cjs +891 -0
- package/scripts/gen-features.cjs +836 -0
- package/scripts/gen-hooks-cli-exit.cjs +239 -0
- package/scripts/gen-install-tree-fixtures.cjs +2 -2
- package/scripts/gen-loop-host-contract.cjs +189 -4
- package/scripts/gen-scripts-cli-exit.cjs +185 -0
- package/scripts/gen-state-md-docs.cjs +727 -0
- package/scripts/{test-failure-reasons.cjs → gsd-test-gate-reasons.cjs} +6 -0
- package/scripts/lib/ci-job-timing.cjs +72 -0
- package/scripts/lib/cli-exit.cjs +546 -44
- package/scripts/lib/drift-scan.cjs +32 -2
- package/scripts/lib/exit-code-registry.cjs +98 -0
- package/scripts/lib/ndjson-reporter.cjs +119 -0
- package/scripts/lib/shellcheck-fetch.cjs +247 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -6
- package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
- package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
- package/scripts/lint-docs-guard-registration.cjs +495 -0
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +198 -0
- package/scripts/lint-eslint-glob-coverage.allowlist.json +4 -0
- package/scripts/{lint-fix-has-regression-test.cjs → lint-fix-has-regression-tests.cjs} +12 -6
- package/scripts/lint-health-diagnostic-rule-table.cjs +65 -8
- package/scripts/lint-mutation-test-derivation-drift.cjs +86 -0
- package/scripts/lint-phase-enumeration-drift.cjs +45 -14
- package/scripts/lint-phase-id-drift.cjs +133 -8
- package/scripts/lint-planning-prompt-drift.cjs +38 -1
- package/scripts/lint-portable-grep.cjs +176 -0
- package/scripts/lint-removed-but-needed.cjs +184 -16
- package/scripts/lint-response-language-coverage.cjs +524 -0
- package/scripts/lint-seam-enforcement.cjs +182 -0
- package/scripts/lint-slug-derivation-drift.cjs +921 -0
- package/scripts/lint-source-test-name-collision.cjs +241 -0
- package/scripts/lint-state-write-path-drift.cjs +337 -432
- package/scripts/lint-test-file-count.allowlist.json +124 -4
- package/scripts/lint-test-file-count.cjs +25 -3
- package/scripts/lint-unreachable-guard-drift.cjs +51 -64
- package/scripts/lint-vendored-deps.cjs +208 -35
- package/scripts/lint-workflow-shellcheck-baseline.json +1027 -0
- package/scripts/lint-workflow-shellcheck.cjs +614 -0
- package/scripts/mutation-matrix.cjs +599 -50
- package/scripts/npm-audit-baseline.cjs +376 -0
- package/scripts/prompt-injection-scan.sh +83 -14
- package/scripts/require-issue-link-policy.cjs +16 -1
- package/scripts/secret-scan.sh +75 -13
- package/scripts/select-docs-guards.cjs +56 -0
- package/scripts/sync-runtime-launcher.cjs +22 -3
- package/skills/gsd-discuss-phase/SKILL.md +1 -1
- package/skills/gsd-execute-phase/SKILL.md +1 -1
- package/skills/gsd-import/SKILL.md +1 -1
- package/skills/gsd-ns-workflow/SKILL.md +1 -0
- package/skills/gsd-phase/SKILL.md +1 -1
- package/skills/gsd-quick/SKILL.md +8 -4
- package/skills/gsd-quick-batch/SKILL.md +105 -0
- package/skills/gsd-surface/SKILL.md +18 -8
- package/vscode/package.json +1 -1
- package/bin/lib/ui-safety-gate.cjs +0 -109
- package/scripts/lint-emitted-drift-ack.cjs +0 -344
- package/scripts/state-write-path-drift-baseline.json +0 -19
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* lint-docs-guard-registration.cjs — every test file that READS a docs/ path
|
|
6
|
+
* (via a real filesystem read call, not merely a string mention) must either
|
|
7
|
+
* be named in the docs-guard lane registry, or carry an explicit
|
|
8
|
+
* `// docs-guard-exempt: <reason>` marker.
|
|
9
|
+
*
|
|
10
|
+
* Exported pure function `checkDocsGuardRegistration({ testsDir, registry })`
|
|
11
|
+
* so tests can drive it against a synthetic fixture directory; also runnable
|
|
12
|
+
* as a CLI against the real tree (`node scripts/lint-docs-guard-registration.cjs`).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const { assertWithinAllowlist } = require('./lib/allowlist-ratchet.cjs');
|
|
18
|
+
const { assertNoSuiteCollision } = require('./docs-guard-registry.cjs');
|
|
19
|
+
const {
|
|
20
|
+
DOCS_GUARD_EXEMPT_BASELINE,
|
|
21
|
+
DOCS_GUARD_EXEMPT_DOCS_PATHS,
|
|
22
|
+
} = require('./lint-docs-guard-registration.exempt-baseline.cjs');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The lint's registry MUST derive from scripts/docs-guard-registry.cjs's
|
|
26
|
+
* `DOCS_GUARD_TESTS` export — not from a second, hand-maintained list.
|
|
27
|
+
* #3753 found exactly that split: this file used to carry its own
|
|
28
|
+
* DOCS_GUARD_REGISTRY literal (20 entries) while the list that actually
|
|
29
|
+
* drives the CI lane only had 10, so the lint reported "registered and
|
|
30
|
+
* fine" for ten guards the lane never ran — the silent gap #3753 exists to
|
|
31
|
+
* close, rebuilt one level down. Two lists holding one shared fact are free
|
|
32
|
+
* to drift; one list read from two places cannot.
|
|
33
|
+
*
|
|
34
|
+
* Returns basenames (test files all live flat under tests/), matching the
|
|
35
|
+
* shape `checkDocsGuardRegistration` expects.
|
|
36
|
+
*/
|
|
37
|
+
function deriveDocsGuardRegistry() {
|
|
38
|
+
const { DOCS_GUARD_TEST_FILES } = require('./docs-guard-registry.cjs');
|
|
39
|
+
if (!Array.isArray(DOCS_GUARD_TEST_FILES) || DOCS_GUARD_TEST_FILES.length === 0) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
'lint-docs-guard-registration: scripts/docs-guard-registry.cjs exported an empty or ' +
|
|
42
|
+
'missing DOCS_GUARD_TEST_FILES — cannot derive a registry. An empty registry would either flag ' +
|
|
43
|
+
'every docs-reading test as unregistered, or (with an empty comparison set) silently pass ' +
|
|
44
|
+
'with zero coverage. Fix the registry rather than defaulting this to [].',
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
// Redundant with docs-guard-registry.cjs's own module-load-time self-check
|
|
48
|
+
// (both consumers of that module — this lint and the docs-required.yml
|
|
49
|
+
// derivation step — must independently reject a suite-token collision per
|
|
50
|
+
// #3753's security follow-up FIX 3), but kept explicit here rather than
|
|
51
|
+
// relying solely on the shared module's require()-time throw: this call
|
|
52
|
+
// makes the guard visible and independently testable from this file's own
|
|
53
|
+
// exports, instead of depending on an implicit side effect of another
|
|
54
|
+
// module's load.
|
|
55
|
+
assertNoSuiteCollision(DOCS_GUARD_TEST_FILES);
|
|
56
|
+
return DOCS_GUARD_TEST_FILES.map(t => path.basename(t));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Real filesystem read calls we treat as "this file reads a path". A docs/
|
|
60
|
+
// path appearing only inside a string literal handed to something else
|
|
61
|
+
// (e.g. assert.equal(msg, 'see docs/foo.md')) must NOT trip this.
|
|
62
|
+
//
|
|
63
|
+
// Detector 1 (segment-shaped): `fs.readFileSync(path.join(ROOT, 'docs', 'x.md'))`.
|
|
64
|
+
// The docs/ path is built from separate path-segment arguments to a known
|
|
65
|
+
// Node fs read function, so it looks for a bare `'docs'` (or `"docs"`/`` `docs` ``)
|
|
66
|
+
// segment, or a `'docs/...'` literal, inside the parenthesized argument list
|
|
67
|
+
// of one of READ_FN_NAMES.
|
|
68
|
+
const READ_FN_NAMES = ['readFileSync', 'readFile', 'readdirSync', 'readdir', 'createReadStream'];
|
|
69
|
+
const READ_CALL_RE = new RegExp(
|
|
70
|
+
`\\b(?:${READ_FN_NAMES.join('|')})\\s*\\(([^()]*(?:\\([^()]*\\)[^()]*)*)\\)`,
|
|
71
|
+
'g',
|
|
72
|
+
);
|
|
73
|
+
const DOCS_QUOTED_PATH_RE = /(['"`])\/?docs\/[^'"`]*\1/;
|
|
74
|
+
const DOCS_QUOTED_SEGMENT_RE = /(['"`])docs\1/;
|
|
75
|
+
|
|
76
|
+
// Detector 2 (single-string-shaped, #3753): `readShipped('docs/how-to/x.md')`.
|
|
77
|
+
// Detector 1 is blind to this spelling — the docs/ path is a single string
|
|
78
|
+
// literal, not a `path.join('docs', ...)` segment, and it is not always
|
|
79
|
+
// handed straight to a bare `fs.read*` call. This is exactly how
|
|
80
|
+
// tests/ui-spec-inventory-provenance.test.cjs reads — the guard whose
|
|
81
|
+
// unregistered drift broke `next` on dacae9273 and motivated #3753 in the
|
|
82
|
+
// first place — so a lint that cannot see its own motivating case's spelling
|
|
83
|
+
// is not a fix.
|
|
84
|
+
//
|
|
85
|
+
// A naive "flag any 'docs/...' string literal" rule produces 59 hits on the
|
|
86
|
+
// real tree (most are mention-only, e.g. assert messages), which is enough
|
|
87
|
+
// false-positive noise that the lint gets disabled rather than obeyed. This
|
|
88
|
+
// heuristic instead requires the docs/ literal to be an argument to a call
|
|
89
|
+
// whose CALLEE NAME looks like a reader (contains read/load/parse/shipped/
|
|
90
|
+
// content/file/doc, case-insensitively) — e.g. `readShipped`, `readRepoFile`,
|
|
91
|
+
// `loadDoc`, `parseContent`. That name-shape restriction is what keeps the
|
|
92
|
+
// count at 6 new hits instead of 59, at the cost of also matching a few
|
|
93
|
+
// call sites (e.g. `groupFilesBySubrepo('docs/x.md', ...)`) whose name
|
|
94
|
+
// happens to contain one of those substrings without actually reading a
|
|
95
|
+
// file — those get `// docs-guard-exempt:` markers instead of registration.
|
|
96
|
+
//
|
|
97
|
+
// Detector 3 (co-occurrence, #3753 correctness follow-up — DEFECT B): the
|
|
98
|
+
// two detectors above only catch a docs/ PATH EXPRESSION passed INLINE, as
|
|
99
|
+
// an argument, to a call in the same statement. The dominant real idiom in
|
|
100
|
+
// this repo builds the path first (`const P = path.join(ROOT, 'docs',
|
|
101
|
+
// 'X.md');`) and reads it later (`fs.readFileSync(P);`) — a two-step form
|
|
102
|
+
// neither detector above can see, along with template-literal
|
|
103
|
+
// (`` `${ROOT}/docs/X.md` ``) and string-concat (`ROOT + '/docs/X.md'`)
|
|
104
|
+
// paths. This detector decouples "does the file build a docs/ path" from
|
|
105
|
+
// "does the file perform a read call" and flags the file when BOTH are true
|
|
106
|
+
// anywhere in it, regardless of whether they share a call site. This trades
|
|
107
|
+
// precision for recall deliberately: a false positive costs one
|
|
108
|
+
// `// docs-guard-exempt:` marker with a reason; a false negative is the
|
|
109
|
+
// #3753 bug shipping again.
|
|
110
|
+
//
|
|
111
|
+
// A docs/ path EXPRESSION is: a quoted 'docs/...' / "docs/..." literal
|
|
112
|
+
// (optionally with a leading slash, for the `ROOT + '/docs/x.md'`
|
|
113
|
+
// string-concat form), a backtick template literal containing `docs/`
|
|
114
|
+
// anywhere inside it (covers both a bare `` `docs/x.md` `` literal and an
|
|
115
|
+
// interpolated `` `${ROOT}/docs/x.md` `` prefix), or a bare `'docs'` segment
|
|
116
|
+
// passed as one of the arguments to a `path.join(...)` call anywhere in the
|
|
117
|
+
// file (not just when that call is itself an argument to a read function).
|
|
118
|
+
const DOCS_TEMPLATE_LITERAL_RE = /`[^`]*\bdocs\/[^`]*`/;
|
|
119
|
+
const PATH_JOIN_CALL_RE = /\bpath\s*\.\s*join\s*\(([^()]*(?:\([^()]*\)[^()]*)*)\)/g;
|
|
120
|
+
|
|
121
|
+
function pathJoinHasDocsSegment(content) {
|
|
122
|
+
let match;
|
|
123
|
+
PATH_JOIN_CALL_RE.lastIndex = 0;
|
|
124
|
+
while ((match = PATH_JOIN_CALL_RE.exec(content)) !== null) {
|
|
125
|
+
if (DOCS_QUOTED_SEGMENT_RE.test(match[1]) || DOCS_QUOTED_PATH_RE.test(match[1])) return true;
|
|
126
|
+
}
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function hasDocsPathExpression(content) {
|
|
131
|
+
return DOCS_QUOTED_PATH_RE.test(content) ||
|
|
132
|
+
DOCS_TEMPLATE_LITERAL_RE.test(content) ||
|
|
133
|
+
pathJoinHasDocsSegment(content);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// A "real read" for the co-occurrence detector: any call to a known Node fs
|
|
137
|
+
// read function, ANYWHERE in the file — deliberately not requiring its
|
|
138
|
+
// argument to look like a docs/ path here (that pairing is what
|
|
139
|
+
// hasDocsPathExpression establishes separately). Does not include
|
|
140
|
+
// `existsSync`: a pure existence check with no content read is exactly the
|
|
141
|
+
// "incidental" case this lint's exemption path exists for, not a guard.
|
|
142
|
+
const READ_CALL_PRESENT_RE = new RegExp(`\\b(?:${READ_FN_NAMES.join('|')})\\s*\\(`);
|
|
143
|
+
|
|
144
|
+
// Detector 2 (single-string-shaped, #3753): `readShipped('docs/how-to/x.md')`.
|
|
145
|
+
// Detector 1 is blind to this spelling — the docs/ path is a single string
|
|
146
|
+
// literal, not a `path.join('docs', ...)` segment, and it is not always
|
|
147
|
+
// handed straight to a bare `fs.read*` call. This is exactly how
|
|
148
|
+
// tests/ui-spec-inventory-provenance.test.cjs reads — the guard whose
|
|
149
|
+
// unregistered drift broke `next` on dacae9273 and motivated #3753 in the
|
|
150
|
+
// first place — so a lint that cannot see its own motivating case's spelling
|
|
151
|
+
// is not a fix.
|
|
152
|
+
//
|
|
153
|
+
// A naive "flag any 'docs/...' string literal" rule produces 59 hits on the
|
|
154
|
+
// real tree (most are mention-only, e.g. assert messages), which is enough
|
|
155
|
+
// false-positive noise that the lint gets disabled rather than obeyed. This
|
|
156
|
+
// heuristic instead requires the docs/ literal to be an argument to a call
|
|
157
|
+
// whose CALLEE NAME looks like a reader (contains read/load/parse/shipped/
|
|
158
|
+
// content/file/doc, case-insensitively) — e.g. `readShipped`, `readRepoFile`,
|
|
159
|
+
// `loadDoc`, `parseContent`. The callee-name test is applied to the WHOLE
|
|
160
|
+
// captured identifier (never requiring a non-keyword prefix before it — see
|
|
161
|
+
// DEFECT A, #3753 correctness follow-up: a prior version of this regex wove
|
|
162
|
+
// the keyword alternation into the SAME character class as a mandatory
|
|
163
|
+
// leading identifier-start character, which made it structurally impossible
|
|
164
|
+
// for the keyword to start at index 0 and silently missed every bare `read(`,
|
|
165
|
+
// `load(`, `parse(`, `content(`, `file(`, and `doc(` callee — this repo's
|
|
166
|
+
// most common reader-helper name among them), at the cost of also matching a
|
|
167
|
+
// few call sites (e.g. `groupFilesBySubrepo('docs/x.md', ...)`) whose name
|
|
168
|
+
// happens to contain one of those substrings without actually reading a
|
|
169
|
+
// file — those get `// docs-guard-exempt:` markers instead of registration.
|
|
170
|
+
const READER_NAME_KEYWORDS_RE = /read|load|parse|shipped|content|file|doc/i;
|
|
171
|
+
const READER_CALL_RE = /\b([A-Za-z_$][\w$]*)\s*\(\s*[^)]{0,60}?["'`]docs\//gi;
|
|
172
|
+
|
|
173
|
+
function readerNameCallHasDocsPath(content) {
|
|
174
|
+
let match;
|
|
175
|
+
READER_CALL_RE.lastIndex = 0;
|
|
176
|
+
while ((match = READER_CALL_RE.exec(content)) !== null) {
|
|
177
|
+
if (READER_NAME_KEYWORDS_RE.test(match[1])) return true;
|
|
178
|
+
}
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Neither detector can see every spelling a docs read could take (e.g. a
|
|
183
|
+
// path built through an indirect helper with a read-agnostic name whose call
|
|
184
|
+
// site never contains a real fs read call in the same file). This registry
|
|
185
|
+
// is a curated, best-effort net with known holes, not an exhaustive static
|
|
186
|
+
// analysis.
|
|
187
|
+
function argsReadDocsPath(args) {
|
|
188
|
+
return DOCS_QUOTED_PATH_RE.test(args) || DOCS_QUOTED_SEGMENT_RE.test(args);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function readsDocsPath(content) {
|
|
192
|
+
let match;
|
|
193
|
+
READ_CALL_RE.lastIndex = 0;
|
|
194
|
+
while ((match = READ_CALL_RE.exec(content)) !== null) {
|
|
195
|
+
if (argsReadDocsPath(match[1])) return true;
|
|
196
|
+
}
|
|
197
|
+
if (readerNameCallHasDocsPath(content)) return true;
|
|
198
|
+
return hasDocsPathExpression(content) && READ_CALL_PRESENT_RE.test(content);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Only scan the file's HEADER — the first EXEMPTION_SCAN_LINES lines. Scanning
|
|
202
|
+
// the whole file lets a `// docs-guard-exempt:`-shaped string embedded in a
|
|
203
|
+
// fixture/template literal (this lint's own test file writes exactly such
|
|
204
|
+
// strings to synthesize fixtures) exempt the entire real file it appears in.
|
|
205
|
+
// A header marker convention closes that hole while still finding every
|
|
206
|
+
// genuine exemption comment, which by convention sits near the top of the
|
|
207
|
+
// file next to its module docstring.
|
|
208
|
+
const EXEMPTION_SCAN_LINES = 20;
|
|
209
|
+
|
|
210
|
+
// DEFECT C (#3753 correctness follow-up): a marker is only honored when the
|
|
211
|
+
// line it appears on is an actual COMMENT, not merely a line whose text
|
|
212
|
+
// contains the marker shape. Probed pre-fix: a string literal like
|
|
213
|
+
// const s = "// docs-guard-exempt: whatever";
|
|
214
|
+
// self-exempted the file with zero signal — the header-window narrowing
|
|
215
|
+
// (EXEMPTION_SCAN_LINES) constrains WHERE the marker may appear but never
|
|
216
|
+
// constrained WHAT KIND of line it must be, so relocating the marker inside
|
|
217
|
+
// a string literal anywhere in the header window still worked. Requiring
|
|
218
|
+
// the line, after trimming leading whitespace, to actually START with a
|
|
219
|
+
// comment token (`//`, `/*`, or a JSDoc-block `*` continuation line) closes
|
|
220
|
+
// this without narrowing the legitimate cases: every real marker in this
|
|
221
|
+
// repo's tests/ sits at column 0 as a full-line `//` comment (verified
|
|
222
|
+
// against every current `docs-guard-exempt:` occurrence in tests/).
|
|
223
|
+
const EXEMPTION_LINE_IS_COMMENT_RE = /^(?:\/\/|\/\*|\*)/;
|
|
224
|
+
|
|
225
|
+
// Security follow-up FIX 4: a marker line inside a multi-line template
|
|
226
|
+
// literal in the header window (e.g. `const F = \`\n// docs-guard-exempt: x\n\`;`)
|
|
227
|
+
// still exempted the file pre-fix — the line, taken on its own, starts with
|
|
228
|
+
// `//` and so passed EXEMPTION_LINE_IS_COMMENT_RE even though it is actually
|
|
229
|
+
// backtick-string CONTENT, not a real comment. Track backtick parity across
|
|
230
|
+
// lines: a line whose entire span is inside an open template literal (i.e.
|
|
231
|
+
// the literal was already open when the line STARTED) is never honored as a
|
|
232
|
+
// comment, no matter what its own text looks like.
|
|
233
|
+
function findExemption(content) {
|
|
234
|
+
const lines = content.split(/\r?\n/).slice(0, EXEMPTION_SCAN_LINES);
|
|
235
|
+
let inTemplateLiteral = false;
|
|
236
|
+
let inBlockComment = false;
|
|
237
|
+
for (const line of lines) {
|
|
238
|
+
const lineStartedInTemplateLiteral = inTemplateLiteral;
|
|
239
|
+
// Count unescaped backticks on this line to toggle template-literal state.
|
|
240
|
+
const backtickCount = (line.match(/\\`|`/g) || []).filter((tok) => tok === '`').length;
|
|
241
|
+
if (backtickCount % 2 === 1) inTemplateLiteral = !inTemplateLiteral;
|
|
242
|
+
if (lineStartedInTemplateLiteral) continue;
|
|
243
|
+
|
|
244
|
+
// Track (real) /* ... */ block-comment state across lines: a line that
|
|
245
|
+
// is genuinely CONTENT inside an open, unterminated block comment is
|
|
246
|
+
// still a real comment (JS syntax), so it stays eligible — this state is
|
|
247
|
+
// tracked only so a future check can distinguish "inside a real block
|
|
248
|
+
// comment" from "inside a template literal" rather than conflating them.
|
|
249
|
+
const opensBlockComment = /\/\*/.test(line);
|
|
250
|
+
const closesBlockComment = /\*\//.test(line);
|
|
251
|
+
if (!inBlockComment && opensBlockComment && !closesBlockComment) {
|
|
252
|
+
inBlockComment = true;
|
|
253
|
+
} else if (inBlockComment && closesBlockComment) {
|
|
254
|
+
inBlockComment = false;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (!EXEMPTION_LINE_IS_COMMENT_RE.test(line.trimStart())) continue;
|
|
258
|
+
const m = /\/\/\s*docs-guard-exempt:(.*)$/.exec(line);
|
|
259
|
+
if (m) return { present: true, reason: m[1].trim() };
|
|
260
|
+
}
|
|
261
|
+
return { present: false, reason: '' };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Security follow-up FIX 3: the exempt ratchet gated on file IDENTITY only —
|
|
266
|
+
* a baselined file that later STARTS genuinely reading shipped docs/ content
|
|
267
|
+
* stayed exempt with zero signal (probed: a baselined file doing
|
|
268
|
+
* `fs.readFileSync('docs/foo.md')` still reported `ok=true, violations=[]`).
|
|
269
|
+
* This extracts every distinct `docs/...` path TOKEN referenced anywhere in
|
|
270
|
+
* an exempted file's content, so the baseline can pin a per-file fingerprint
|
|
271
|
+
* of what it references and fail loudly when that set changes — forcing a
|
|
272
|
+
* human to re-confirm the exemption still holds. Deliberately broader than
|
|
273
|
+
* "only tokens passed to a read call" (this module's readsDocsPath heuristics
|
|
274
|
+
* above): the fingerprint's job is to catch ANY drift in what docs/ paths an
|
|
275
|
+
* exempted file mentions, not just the ones already read-call-shaped, since
|
|
276
|
+
* a mention today can become a read call tomorrow without changing the
|
|
277
|
+
* mention text at all.
|
|
278
|
+
*/
|
|
279
|
+
const DOCS_PATH_TOKEN_RE = /\/?docs\/[A-Za-z0-9_./-]*[A-Za-z0-9_-]/g;
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* @param {string} content
|
|
283
|
+
* @returns {string[]} sorted, deduped list of docs/ path tokens referenced —
|
|
284
|
+
* a readable, diffable fingerprint (never an opaque hash) so a reviewer can
|
|
285
|
+
* see exactly what changed.
|
|
286
|
+
*/
|
|
287
|
+
function extractDocsPathReferences(content) {
|
|
288
|
+
const set = new Set();
|
|
289
|
+
let m;
|
|
290
|
+
DOCS_PATH_TOKEN_RE.lastIndex = 0;
|
|
291
|
+
while ((m = DOCS_PATH_TOKEN_RE.exec(content)) !== null) {
|
|
292
|
+
set.add(m[0].replace(/^\//, ''));
|
|
293
|
+
}
|
|
294
|
+
return [...set].sort();
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Compare each currently-exempted file's live docs-path fingerprint against
|
|
299
|
+
* its pinned baseline fingerprint. A file present in `current` whose sorted
|
|
300
|
+
* path list differs from the baseline's (paths added OR removed) is a
|
|
301
|
+
* violation: the exemption's premise ("this file doesn't really guard
|
|
302
|
+
* shipped docs content") may no longer hold and a human must re-confirm it.
|
|
303
|
+
*
|
|
304
|
+
* @param {Record<string, string[]>} current - file -> live sorted docs/ path list.
|
|
305
|
+
* @param {Record<string, string[]>} baseline - file -> pinned sorted docs/ path list.
|
|
306
|
+
* @returns {Array<{ file: string, reason: string }>}
|
|
307
|
+
*/
|
|
308
|
+
function checkExemptFingerprints(current, baseline) {
|
|
309
|
+
const violations = [];
|
|
310
|
+
for (const [file, paths] of Object.entries(current)) {
|
|
311
|
+
const baselinePaths = baseline[file];
|
|
312
|
+
if (!baselinePaths) continue; // identity ratchet (checkExemptBaseline) already flags a novel exemption
|
|
313
|
+
const currentJoined = paths.join('\n');
|
|
314
|
+
const baselineJoined = [...baselinePaths].sort().join('\n');
|
|
315
|
+
if (currentJoined !== baselineJoined) {
|
|
316
|
+
violations.push({
|
|
317
|
+
file,
|
|
318
|
+
reason:
|
|
319
|
+
`the docs paths referenced by ${file} changed; re-confirm the exemption still holds and ` +
|
|
320
|
+
`update the baseline in ${EXEMPT_BASELINE_FILE}:${EXEMPT_BASELINE_DOCS_PATHS_CONST}. ` +
|
|
321
|
+
`was: [${baselinePaths.join(', ') || '(none)'}], now: [${paths.join(', ') || '(none)'}]`,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return violations;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* File referenced in this module's own remedy messages below (kept as a
|
|
330
|
+
* named constant so the CLI path and the message text cannot drift).
|
|
331
|
+
*/
|
|
332
|
+
const EXEMPT_BASELINE_FILE = 'scripts/lint-docs-guard-registration.exempt-baseline.cjs';
|
|
333
|
+
const EXEMPT_BASELINE_CONST = 'DOCS_GUARD_EXEMPT_BASELINE';
|
|
334
|
+
const EXEMPT_BASELINE_DOCS_PATHS_CONST = 'DOCS_GUARD_EXEMPT_DOCS_PATHS';
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Ratchet the `// docs-guard-exempt:` marker on file identity, mirroring
|
|
338
|
+
* scripts/lint-allow-test-rule-refs.cjs's `allow-test-rule` identity ratchet
|
|
339
|
+
* (scripts/lib/allowlist-ratchet.cjs, ADR-456's pattern): a NEW exemption not
|
|
340
|
+
* already in the pinned baseline fails the lint, and a baseline entry whose
|
|
341
|
+
* file no longer carries a real marker (removed, renamed, or the marker was
|
|
342
|
+
* deleted) is reported STALE and must be pruned — the baseline only ever
|
|
343
|
+
* moves by deliberate edit, never silently grows or goes stale unnoticed.
|
|
344
|
+
*
|
|
345
|
+
* Unlike the sibling gate's generic "do not just add to the allowlist"
|
|
346
|
+
* framing (which fits an offender-tracking allowlist), the correct remedy
|
|
347
|
+
* for a genuinely-warranted new docs-guard exemption really is to add it to
|
|
348
|
+
* the baseline — so the novel-entry case gets its own explicit remedy line
|
|
349
|
+
* here rather than reusing that wording verbatim.
|
|
350
|
+
*
|
|
351
|
+
* @param {string[]} exemptedFiles - basenames with a present, non-empty
|
|
352
|
+
* `docs-guard-exempt:` marker, found in the current tree.
|
|
353
|
+
* @param {string[]} baseline - the pinned baseline (DOCS_GUARD_EXEMPT_BASELINE).
|
|
354
|
+
* @returns {Array<{ file: string, reason: string }>}
|
|
355
|
+
*/
|
|
356
|
+
function checkExemptBaseline(exemptedFiles, baseline) {
|
|
357
|
+
const violations = [];
|
|
358
|
+
const { novel } = assertWithinAllowlist({
|
|
359
|
+
label: 'docs-guard-exempt',
|
|
360
|
+
current: exemptedFiles,
|
|
361
|
+
known: baseline,
|
|
362
|
+
fail: (msg) => violations.push({ file: '(docs-guard-exempt baseline)', reason: msg }),
|
|
363
|
+
pruneHint:
|
|
364
|
+
`its file was removed, renamed, or no longer carries a docs-guard-exempt marker — ` +
|
|
365
|
+
`prune it from the baseline in ${EXEMPT_BASELINE_FILE}:${EXEMPT_BASELINE_CONST}`,
|
|
366
|
+
});
|
|
367
|
+
if (novel.length > 0) {
|
|
368
|
+
violations.push({
|
|
369
|
+
file: '(docs-guard-exempt baseline)',
|
|
370
|
+
reason:
|
|
371
|
+
`${novel.length} NEW docs-guard-exempt marker(s) not in the pinned baseline: ` +
|
|
372
|
+
`${novel.join(', ')} — if this exemption is correct, add it to the baseline in ` +
|
|
373
|
+
`${EXEMPT_BASELINE_FILE}:${EXEMPT_BASELINE_CONST}`,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
return violations;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* @param {{ testsDir: string, registry: string[], exemptBaseline?: string[] }} opts
|
|
381
|
+
* `exemptBaseline`, when provided, ratchets the docs-guard-exempt marker on
|
|
382
|
+
* file identity against that pinned list (see `checkExemptBaseline`).
|
|
383
|
+
* Omitted entirely by callers (e.g. isolated fixture-only tests) that do
|
|
384
|
+
* not want the ratchet applied.
|
|
385
|
+
* @returns {{ ok: boolean, violations: Array<{ file: string, reason: string }>, exemptedFiles: string[] }}
|
|
386
|
+
*/
|
|
387
|
+
function checkDocsGuardRegistration({ testsDir, registry, exemptBaseline, exemptDocsPathsBaseline }) {
|
|
388
|
+
const violations = [];
|
|
389
|
+
const exemptedFiles = [];
|
|
390
|
+
const exemptedDocsPaths = {};
|
|
391
|
+
|
|
392
|
+
for (const entry of registry) {
|
|
393
|
+
const full = path.join(testsDir, entry);
|
|
394
|
+
if (!fs.existsSync(full)) {
|
|
395
|
+
violations.push({ file: entry, reason: `registry entry does not exist on disk: ${full}` });
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const registrySet = new Set(registry);
|
|
400
|
+
let files;
|
|
401
|
+
try {
|
|
402
|
+
files = fs.readdirSync(testsDir).filter(f => f.endsWith('.test.cjs'));
|
|
403
|
+
} catch (err) {
|
|
404
|
+
// A guard that cannot read its own input must never report success.
|
|
405
|
+
// Probed pre-fix: checkDocsGuardRegistration({testsDir:'/nonexistent',
|
|
406
|
+
// registry:[]}) returned {ok:true, violations:[]} — a green check that
|
|
407
|
+
// guarded nothing. Treat an unreadable testsDir as a HARD VIOLATION.
|
|
408
|
+
return {
|
|
409
|
+
ok: false,
|
|
410
|
+
violations: [
|
|
411
|
+
{
|
|
412
|
+
file: '(testsDir)',
|
|
413
|
+
reason: `cannot read testsDir ${testsDir}: ${err.message} — a docs-guard registration ` +
|
|
414
|
+
'lint that cannot read its own input must fail, never silently report zero violations',
|
|
415
|
+
},
|
|
416
|
+
],
|
|
417
|
+
exemptedFiles: [],
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
for (const file of files) {
|
|
422
|
+
const full = path.join(testsDir, file);
|
|
423
|
+
let content;
|
|
424
|
+
try {
|
|
425
|
+
content = fs.readFileSync(full, 'utf8');
|
|
426
|
+
} catch (err) {
|
|
427
|
+
// Same class: a directory or broken symlink named `*.test.cjs` (or any
|
|
428
|
+
// other read failure) must be a hard violation, not a silent skip.
|
|
429
|
+
violations.push({
|
|
430
|
+
file,
|
|
431
|
+
reason: `cannot read candidate test file ${full}: ${err.message} — an unreadable ` +
|
|
432
|
+
'docs-guard candidate must fail the lint, not be silently skipped',
|
|
433
|
+
});
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const exemption = findExemption(content);
|
|
438
|
+
if (exemption.present) {
|
|
439
|
+
if (exemption.reason.length === 0) {
|
|
440
|
+
violations.push({ file, reason: 'docs-guard-exempt marker present with no reason' });
|
|
441
|
+
} else {
|
|
442
|
+
exemptedFiles.push(file);
|
|
443
|
+
exemptedDocsPaths[file] = extractDocsPathReferences(content);
|
|
444
|
+
}
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (readsDocsPath(content) && !registrySet.has(file)) {
|
|
449
|
+
violations.push({
|
|
450
|
+
file,
|
|
451
|
+
reason: 'reads a docs/ path but is not registered in the docs-guard lane and carries no docs-guard-exempt marker',
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (exemptBaseline) {
|
|
457
|
+
violations.push(...checkExemptBaseline(exemptedFiles, exemptBaseline));
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (exemptDocsPathsBaseline) {
|
|
461
|
+
violations.push(...checkExemptFingerprints(exemptedDocsPaths, exemptDocsPathsBaseline));
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return { ok: violations.length === 0, violations, exemptedFiles, exemptedDocsPaths };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
module.exports = {
|
|
468
|
+
checkDocsGuardRegistration,
|
|
469
|
+
checkExemptBaseline,
|
|
470
|
+
checkExemptFingerprints,
|
|
471
|
+
extractDocsPathReferences,
|
|
472
|
+
deriveDocsGuardRegistry,
|
|
473
|
+
EXEMPT_BASELINE_FILE,
|
|
474
|
+
EXEMPT_BASELINE_CONST,
|
|
475
|
+
EXEMPT_BASELINE_DOCS_PATHS_CONST,
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
if (require.main === module) {
|
|
479
|
+
const ROOT = path.join(__dirname, '..');
|
|
480
|
+
const result = checkDocsGuardRegistration({
|
|
481
|
+
testsDir: path.join(ROOT, 'tests'),
|
|
482
|
+
registry: deriveDocsGuardRegistry(),
|
|
483
|
+
exemptBaseline: DOCS_GUARD_EXEMPT_BASELINE,
|
|
484
|
+
exemptDocsPathsBaseline: DOCS_GUARD_EXEMPT_DOCS_PATHS,
|
|
485
|
+
});
|
|
486
|
+
if (!result.ok) {
|
|
487
|
+
process.stderr.write(`lint-docs-guard-registration: ${result.violations.length} violation(s)\n`);
|
|
488
|
+
for (const v of result.violations) {
|
|
489
|
+
process.stderr.write(` ${v.file}: ${v.reason}\n`);
|
|
490
|
+
}
|
|
491
|
+
process.exitCode = 1;
|
|
492
|
+
} else {
|
|
493
|
+
console.log('ok lint-docs-guard-registration: 0 violations');
|
|
494
|
+
}
|
|
495
|
+
}
|