@opengsd/gsd-core 1.18.0 → 1.20.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 -1
- package/.opencode/plugins/gsd-core.js +2 -3
- package/agents/gsd-code-fixer.compact.md +7 -7
- package/agents/gsd-code-fixer.md +13 -7
- package/agents/gsd-code-reviewer.compact.md +49 -8
- package/agents/gsd-code-reviewer.md +53 -14
- package/agents/gsd-executor.md +8 -4
- package/agents/gsd-verifier.md +1 -1
- package/bin/install.js +422 -103
- package/commands/gsd/update.md +5 -2
- package/gsd-core/bin/gsd-tools.cjs +179 -30
- package/gsd-core/bin/lib/agent-install-check.cjs +29 -3
- package/gsd-core/bin/lib/args-terminator.cjs +114 -0
- package/gsd-core/bin/lib/broken-windows.cjs +59 -1
- package/gsd-core/bin/lib/capability-lifecycle.cjs +1 -12
- package/gsd-core/bin/lib/capability-lock.cjs +13 -2
- package/gsd-core/bin/lib/capability-registry.cjs +376 -103
- package/gsd-core/bin/lib/capability-state.cjs +14 -8
- package/gsd-core/bin/lib/capability-validator.cjs +32 -0
- package/gsd-core/bin/lib/commands.cjs +27 -17
- package/gsd-core/bin/lib/config-loader.cjs +13 -3
- package/gsd-core/bin/lib/config.cjs +256 -58
- package/gsd-core/bin/lib/core-utils.cjs +45 -0
- package/gsd-core/bin/lib/decision-coverage-support.cjs +74 -0
- package/gsd-core/bin/lib/frontmatter.cjs +21 -3
- package/gsd-core/bin/lib/gate-api-coverage-verify-pre.cjs +2 -2
- package/gsd-core/bin/lib/gate-context-drift.cjs +9 -4
- package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +6 -1
- package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +6 -1
- package/gsd-core/bin/lib/gate-evaluation-scope.cjs +134 -13
- package/gsd-core/bin/lib/gate-phase-context.cjs +7 -8
- package/gsd-core/bin/lib/gate-schema-drift.cjs +1 -1
- package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +63 -22
- package/gsd-core/bin/lib/gate-ui-safety.cjs +23 -5
- package/gsd-core/bin/lib/git-info-exclude.cjs +52 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +31 -12
- package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +9 -7
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +3 -3
- package/gsd-core/bin/lib/health-diagnostic-types.cjs +4 -0
- package/gsd-core/bin/lib/health-diagnostic.cjs +52 -1
- package/gsd-core/bin/lib/host-integration.cjs +13 -6
- package/gsd-core/bin/lib/init.cjs +245 -68
- package/gsd-core/bin/lib/install-engine.cjs +246 -32
- package/gsd-core/bin/lib/install-profiles.cjs +9 -4
- package/gsd-core/bin/lib/installer-migration-report.cjs +2 -0
- package/gsd-core/bin/lib/io.cjs +6 -0
- package/gsd-core/bin/lib/markdown-line-diff.cjs +147 -0
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +147 -1
- package/gsd-core/bin/lib/markdown-table.cjs +23 -0
- package/gsd-core/bin/lib/mcp-server.cjs +8 -5
- package/gsd-core/bin/lib/milestone-lock.cjs +8 -1
- package/gsd-core/bin/lib/milestone-release-map.cjs +244 -0
- package/gsd-core/bin/lib/milestone-release-tags.cjs +440 -0
- package/gsd-core/bin/lib/milestone.cjs +61 -51
- package/gsd-core/bin/lib/model-catalog.cjs +8 -0
- package/gsd-core/bin/lib/normalize-test-command.cjs +67 -0
- package/gsd-core/bin/lib/phase-entry-grammar.cjs +119 -0
- package/gsd-core/bin/lib/phase-id-display.cjs +2 -0
- package/gsd-core/bin/lib/phase-id.cjs +629 -38
- package/gsd-core/bin/lib/phase-lifecycle.cjs +12 -44
- package/gsd-core/bin/lib/phase-locator.cjs +328 -19
- package/gsd-core/bin/lib/phase.cjs +3066 -653
- package/gsd-core/bin/lib/planning-document.cjs +26 -5
- package/gsd-core/bin/lib/planning-inspect.cjs +27 -35
- package/gsd-core/bin/lib/planning-snapshot.cjs +15 -8
- package/gsd-core/bin/lib/planning-workspace.cjs +21 -3
- package/gsd-core/bin/lib/pristine-baseline.cjs +11 -3
- package/gsd-core/bin/lib/read-only-git-env.cjs +79 -0
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +2 -2
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +57 -2
- package/gsd-core/bin/lib/review-lane-invocation.cjs +125 -18
- package/gsd-core/bin/lib/review-lane-runner.cjs +33 -4
- package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +11 -9
- package/gsd-core/bin/lib/roadmap-parser.cjs +735 -137
- package/gsd-core/bin/lib/roadmap-progress.cjs +136 -0
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +47 -7
- package/gsd-core/bin/lib/roadmap.cjs +383 -123
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +381 -14
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +8 -4
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +60 -16
- package/gsd-core/bin/lib/shell-command-projection.cjs +151 -19
- package/gsd-core/bin/lib/smart-entry.cjs +35 -11
- package/gsd-core/bin/lib/state-contract.cjs +39 -15
- package/gsd-core/bin/lib/state-document.cjs +344 -27
- package/gsd-core/bin/lib/state-transition.cjs +156 -32
- package/gsd-core/bin/lib/state.cjs +527 -186
- package/gsd-core/bin/lib/surface.cjs +9 -0
- package/gsd-core/bin/lib/task-command-router.cjs +5 -25
- package/gsd-core/bin/lib/task-content-resolution.cjs +46 -1
- package/gsd-core/bin/lib/template.cjs +2 -2
- package/gsd-core/bin/lib/validate.cjs +3 -2
- package/gsd-core/bin/lib/vendor/re2js.cjs +863 -577
- package/gsd-core/bin/lib/vendor/re2js.d.cts +284 -219
- package/gsd-core/bin/lib/verification.cjs +96 -9
- package/gsd-core/bin/lib/verify.cjs +4 -2
- package/gsd-core/bin/lib/workstream-inventory.cjs +26 -20
- package/gsd-core/bin/lib/worktree-safety.cjs +22 -1
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
- package/gsd-core/bin/verify-reapply-patches.cjs +411 -10
- package/gsd-core/references/cicd-review-patterns.md +45 -0
- package/gsd-core/references/git-integration.md +11 -0
- package/gsd-core/references/loop-hook-dispatch.md +13 -6
- package/gsd-core/references/necessity-review.md +48 -0
- package/gsd-core/references/planning-config.md +2 -0
- package/gsd-core/references/tdd.md +2 -2
- package/gsd-core/templates/DEBUG.md +16 -0
- package/gsd-core/templates/config.json +2 -1
- package/gsd-core/templates/verification-report.md +1 -1
- package/gsd-core/workflows/audit-fix.md +2 -1
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +10 -1
- package/gsd-core/workflows/code-review-fix.md +46 -5
- package/gsd-core/workflows/code-review.md +64 -7
- package/gsd-core/workflows/complete-milestone/steps/project-md-evolution-review.md +145 -0
- package/gsd-core/workflows/complete-milestone/steps/release-map.md +80 -0
- package/gsd-core/workflows/complete-milestone.md +37 -150
- package/gsd-core/workflows/diagnose-issues.md +29 -5
- package/gsd-core/workflows/discuss-phase/modes/power.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +33 -2
- package/gsd-core/workflows/discuss-phase-power.md +19 -2
- package/gsd-core/workflows/discuss-phase.md +6 -10
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +13 -7
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +7 -2
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -1
- package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +2 -2
- package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +11 -2
- package/gsd-core/workflows/execute-phase.md +10 -6
- package/gsd-core/workflows/execute-plan.md +1 -1
- package/gsd-core/workflows/health.md +2 -1
- package/gsd-core/workflows/import.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +2 -2
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +13 -9
- package/gsd-core/workflows/plan-phase.md +14 -14
- package/gsd-core/workflows/pr-branch.md +2 -0
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +1 -1
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +8 -1
- package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +2 -1
- package/gsd-core/workflows/quick.md +1 -1
- package/gsd-core/workflows/reapply-patches.md +104 -0
- package/gsd-core/workflows/review.md +70 -154
- package/gsd-core/workflows/section-manifest.json +5 -0
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/settings-advanced.md +57 -9
- package/gsd-core/workflows/settings.md +1 -0
- package/gsd-core/workflows/ship.md +15 -4
- package/gsd-core/workflows/spec-phase.md +3 -6
- package/gsd-core/workflows/verify-work.md +4 -3
- package/hooks/dist/gsd-agent-isolation-guard.js +11 -1
- package/hooks/dist/gsd-cursor-subagent-start.js +6 -2
- package/hooks/dist/gsd-graphify-update.sh +6 -0
- package/hooks/dist/gsd-path-export.js +95 -0
- package/hooks/dist/gsd-secret-read-guard.js +172 -39
- package/hooks/dist/gsd-statusline.js +37 -3
- package/hooks/dist/gsd-windsurf-pre-write.js +29 -2
- package/hooks/dist/gsd-workflow-guard.js +3 -2
- package/hooks/dist/gsd-worktree-path-guard.js +29 -2
- package/hooks/dist/lib/git-probe.js +16 -0
- package/hooks/dist/lib/isolation-sentinel.js +44 -0
- package/hooks/dist/managed-hooks-registry.cjs +1 -0
- package/hooks/gsd-agent-isolation-guard.js +11 -1
- package/hooks/gsd-cursor-subagent-start.js +6 -2
- package/hooks/gsd-graphify-update.sh +6 -0
- package/hooks/gsd-path-export.js +95 -0
- package/hooks/gsd-secret-read-guard.js +172 -39
- package/hooks/gsd-statusline.js +37 -3
- package/hooks/gsd-windsurf-pre-write.js +29 -2
- package/hooks/gsd-workflow-guard.js +3 -2
- package/hooks/gsd-worktree-path-guard.js +29 -2
- package/hooks/hooks.json +1 -0
- package/hooks/lib/git-probe.js +16 -0
- package/hooks/lib/isolation-sentinel.js +44 -0
- package/hooks/managed-hooks-registry.cjs +1 -0
- package/package.json +15 -8
- package/pi/gsd.cjs +1047 -10
- package/pi/package-agents/gsd-advisor-researcher.md +113 -0
- package/pi/package-agents/gsd-ai-researcher.md +111 -0
- package/pi/package-agents/gsd-assumptions-analyzer.md +110 -0
- package/pi/package-agents/gsd-code-fixer.md +774 -0
- package/pi/package-agents/gsd-code-reviewer.md +441 -0
- package/pi/package-agents/gsd-codebase-mapper.md +850 -0
- package/pi/package-agents/gsd-debug-session-manager.md +416 -0
- package/pi/package-agents/gsd-debugger.md +1274 -0
- package/pi/package-agents/gsd-doc-classifier.md +269 -0
- package/pi/package-agents/gsd-doc-synthesizer.md +259 -0
- package/pi/package-agents/gsd-doc-verifier.md +212 -0
- package/pi/package-agents/gsd-doc-writer.md +626 -0
- package/pi/package-agents/gsd-dom-verifier.md +164 -0
- package/pi/package-agents/gsd-domain-researcher.md +144 -0
- package/pi/package-agents/gsd-eval-auditor.md +185 -0
- package/pi/package-agents/gsd-eval-planner.md +149 -0
- package/pi/package-agents/gsd-executor.md +898 -0
- package/pi/package-agents/gsd-framework-selector.md +159 -0
- package/pi/package-agents/gsd-integration-checker.md +476 -0
- package/pi/package-agents/gsd-intel-updater.md +338 -0
- package/pi/package-agents/gsd-mempalace-curator.md +49 -0
- package/pi/package-agents/gsd-nyquist-auditor.md +200 -0
- package/pi/package-agents/gsd-pattern-mapper.md +340 -0
- package/pi/package-agents/gsd-phase-researcher.md +897 -0
- package/pi/package-agents/gsd-plan-checker.md +1077 -0
- package/pi/package-agents/gsd-planner.md +1003 -0
- package/pi/package-agents/gsd-project-researcher.md +611 -0
- package/pi/package-agents/gsd-research-synthesizer.md +259 -0
- package/pi/package-agents/gsd-roadmapper.md +779 -0
- package/pi/package-agents/gsd-security-auditor.md +171 -0
- package/pi/package-agents/gsd-ui-auditor.md +581 -0
- package/pi/package-agents/gsd-ui-checker.md +417 -0
- package/pi/package-agents/gsd-ui-researcher.md +469 -0
- package/pi/package-agents/gsd-user-profiler.md +174 -0
- package/pi/package-agents/gsd-verifier.md +963 -0
- package/pi/package-agents/manifest.json +39 -0
- package/pi/package-skills/gsd-add-tests/SKILL.md +39 -0
- package/pi/package-skills/gsd-ai-integration-phase/SKILL.md +36 -0
- package/pi/package-skills/gsd-audit-fix/SKILL.md +34 -0
- package/pi/package-skills/gsd-audit-milestone/SKILL.md +40 -0
- package/pi/package-skills/gsd-audit-uat/SKILL.md +26 -0
- package/pi/package-skills/gsd-autonomous/SKILL.md +52 -0
- package/pi/package-skills/gsd-capture/SKILL.md +68 -0
- package/pi/package-skills/gsd-cleanup/SKILL.md +25 -0
- package/pi/package-skills/gsd-code-review/SKILL.md +71 -0
- package/pi/package-skills/gsd-complete-milestone/SKILL.md +147 -0
- package/pi/package-skills/gsd-config/SKILL.md +60 -0
- package/pi/package-skills/gsd-debug/SKILL.md +64 -0
- package/pi/package-skills/gsd-discuss-phase/SKILL.md +74 -0
- package/pi/package-skills/gsd-docs-update/SKILL.md +50 -0
- package/pi/package-skills/gsd-eval-review/SKILL.md +35 -0
- package/pi/package-skills/gsd-execute-phase/SKILL.md +62 -0
- package/pi/package-skills/gsd-explore/SKILL.md +26 -0
- package/pi/package-skills/gsd-extract-learnings/SKILL.md +25 -0
- package/pi/package-skills/gsd-fast/SKILL.md +33 -0
- package/pi/package-skills/gsd-forensics/SKILL.md +59 -0
- package/pi/package-skills/gsd-graphify/SKILL.md +210 -0
- package/pi/package-skills/gsd-health/SKILL.md +36 -0
- package/pi/package-skills/gsd-help/SKILL.md +37 -0
- package/pi/package-skills/gsd-import/SKILL.md +46 -0
- package/pi/package-skills/gsd-inbox/SKILL.md +42 -0
- package/pi/package-skills/gsd-ingest-docs/SKILL.md +43 -0
- package/pi/package-skills/gsd-manager/SKILL.md +46 -0
- package/pi/package-skills/gsd-map-codebase/SKILL.md +85 -0
- package/pi/package-skills/gsd-mempalace-capture/SKILL.md +111 -0
- package/pi/package-skills/gsd-mempalace-recall/SKILL.md +111 -0
- package/pi/package-skills/gsd-milestone-summary/SKILL.md +54 -0
- package/pi/package-skills/gsd-mvp-phase/SKILL.md +45 -0
- package/pi/package-skills/gsd-new-milestone/SKILL.md +49 -0
- package/pi/package-skills/gsd-new-project/SKILL.md +49 -0
- package/pi/package-skills/gsd-next/SKILL.md +34 -0
- package/pi/package-skills/gsd-ns-context/SKILL.md +31 -0
- package/pi/package-skills/gsd-ns-ideate/SKILL.md +30 -0
- package/pi/package-skills/gsd-ns-manage/SKILL.md +42 -0
- package/pi/package-skills/gsd-ns-project/SKILL.md +34 -0
- package/pi/package-skills/gsd-ns-review/SKILL.md +35 -0
- package/pi/package-skills/gsd-ns-workflow/SKILL.md +44 -0
- package/pi/package-skills/gsd-onboard/SKILL.md +46 -0
- package/pi/package-skills/gsd-pause-work/SKILL.md +48 -0
- package/pi/package-skills/gsd-phase/SKILL.md +61 -0
- package/pi/package-skills/gsd-plan-phase/SKILL.md +62 -0
- package/pi/package-skills/gsd-plan-review-convergence/SKILL.md +63 -0
- package/pi/package-skills/gsd-pr-branch/SKILL.md +31 -0
- package/pi/package-skills/gsd-profile-user/SKILL.md +48 -0
- package/pi/package-skills/gsd-progress/SKILL.md +51 -0
- package/pi/package-skills/gsd-quick/SKILL.md +179 -0
- package/pi/package-skills/gsd-quick-batch/SKILL.md +114 -0
- package/pi/package-skills/gsd-resume-work/SKILL.md +31 -0
- package/pi/package-skills/gsd-review/SKILL.md +50 -0
- package/pi/package-skills/gsd-review-backlog/SKILL.md +65 -0
- package/pi/package-skills/gsd-secure-phase/SKILL.md +37 -0
- package/pi/package-skills/gsd-settings/SKILL.md +30 -0
- package/pi/package-skills/gsd-ship/SKILL.md +27 -0
- package/pi/package-skills/gsd-sketch/SKILL.md +56 -0
- package/pi/package-skills/gsd-spec-phase/SKILL.md +63 -0
- package/pi/package-skills/gsd-spike/SKILL.md +53 -0
- package/pi/package-skills/gsd-stats/SKILL.md +22 -0
- package/pi/package-skills/gsd-surface/SKILL.md +177 -0
- package/pi/package-skills/gsd-thread/SKILL.md +29 -0
- package/pi/package-skills/gsd-ui-phase/SKILL.md +35 -0
- package/pi/package-skills/gsd-ui-review/SKILL.md +35 -0
- package/pi/package-skills/gsd-ultraplan-phase/SKILL.md +38 -0
- package/pi/package-skills/gsd-undo/SKILL.md +38 -0
- package/pi/package-skills/gsd-update/SKILL.md +54 -0
- package/pi/package-skills/gsd-validate-phase/SKILL.md +37 -0
- package/pi/package-skills/gsd-verify-work/SKILL.md +41 -0
- package/pi/package-skills/gsd-workspace/SKILL.md +57 -0
- package/pi/package-skills/gsd-workstreams/SKILL.md +73 -0
- package/scripts/build-hooks.js +4 -0
- package/scripts/ci-next-health.cjs +246 -23
- package/scripts/command-contract-helpers.cjs +1 -1
- package/scripts/docs-guard-registry.cjs +22 -7
- package/scripts/gen-features.cjs +14 -2
- package/scripts/gen-health-docs.cjs +13 -1
- package/scripts/gen-pi-package-skills.cjs +217 -0
- package/scripts/lib/drift-scan.cjs +27 -0
- package/scripts/lib/macos-conformance-tier.generated.cjs +17 -1
- package/scripts/lib/platform-conformance-tier.generated.cjs +37 -1
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +3 -3
- package/scripts/lint-phase-enumeration-drift.cjs +8 -4
- package/scripts/lint-retired-runtime-name.cjs +5 -0
- package/scripts/lint-roadmap-progress-drift.cjs +421 -0
- package/scripts/mutation-matrix.cjs +23 -1
- package/scripts/npm-audit-baseline.cjs +41 -7
- package/scripts/reviewed-or-maintainer-policy.cjs +181 -0
- package/scripts/select-docs-guards.cjs +1 -1
- package/scripts/self-test-report-policy.cjs +350 -0
- package/scripts/sync-next-version.cjs +38 -6
- package/skills/gsd-update/SKILL.md +5 -2
- package/vscode/package.json +1 -1
- package/scripts/diagnose-validate-commit-in-tier.cjs +0 -270
- package/scripts/diagnose-validate-commit-phases.cjs +0 -348
- package/scripts/xtrace-phase-timing.cjs +0 -205
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Anti-divergence drift guard for the ROADMAP Progress-table and phase-entry
|
|
6
|
+
* seams (epic #5692, issue #5765, ADR-2143 section 7).
|
|
7
|
+
*
|
|
8
|
+
* Two modules are the only legitimate home of two questions:
|
|
9
|
+
* - `src/roadmap-progress.cts` "which table is the Progress table, and what
|
|
10
|
+
* is its milestone slice" (Phases 1-2: readers and writers).
|
|
11
|
+
* - `src/phase-entry-grammar.cts` "what is a phase-list entry" (bullet marker,
|
|
12
|
+
* checkbox, bold token, anchoring) (Phases 3-4: readers and writers).
|
|
13
|
+
* The census in #5703 found 12 hand-written Progress locators and 26 hand-written
|
|
14
|
+
* phase-entry patterns that had drifted apart. After Phases 1-4 none remain
|
|
15
|
+
* outside the owners. This guard keeps it that way: the next local `## Progress`
|
|
16
|
+
* regex, Progress-table `.replace()` or `- [ ] Phase` pattern fails `lint:ci`.
|
|
17
|
+
*
|
|
18
|
+
* Four classes are detected, per source line, with comments stripped first (a
|
|
19
|
+
* comment documenting an owner is not a re-derivation of it):
|
|
20
|
+
* progress-heading a regex literal, or a string literal, that spells a
|
|
21
|
+
* level-N heading followed by `Progress` using regex
|
|
22
|
+
* syntax (`/^##[ \t]+Progress\b/im`, `#{1,3}\s*Progress`).
|
|
23
|
+
* progress-title the bare title test (`/^Progress\b/i`, `'Progress\\b'`)
|
|
24
|
+
* that the bracket writers used to share.
|
|
25
|
+
* progress-table-replace `.replace(` / `.replaceAll(` whose FIRST argument is a
|
|
26
|
+
* Progress-table pattern: a regex naming `Progress` or
|
|
27
|
+
* `Plans Complete`, a table-row regex (two or more
|
|
28
|
+
* escaped pipes) on a line that names ROADMAP content, or
|
|
29
|
+
* an identifier such as `progressRowRe` (ADR-2143
|
|
30
|
+
* section 7(b)). It looks at the first argument only, so
|
|
31
|
+
* `roadmap.title.replace(/\|/g, '\\|')` is not flagged.
|
|
32
|
+
* phase-entry-checkbox a literal that carries an escaped checkbox class
|
|
33
|
+
* (`\[[ xX]\]`, `\[( |x)\]`, `\[x\]`, `\[.\]`) AND the word
|
|
34
|
+
* `Phase` (`- [ ] **Phase 1: Name**`).
|
|
35
|
+
*
|
|
36
|
+
* What is NOT flagged, on purpose: a heading BUILT for output (`'## Progress\n'`,
|
|
37
|
+
* `'## **Progress**\n'`: no regex syntax), the STATE.md `Progress:` field, status
|
|
38
|
+
* vocabulary (`IN PROGRESS`, `/^(done|progress\b)/`), the heading bracket tag
|
|
39
|
+
* (`### [tag] Phase N:`) and requirement / plan-row checkboxes (no `Phase`). Those
|
|
40
|
+
* answer different questions owned elsewhere.
|
|
41
|
+
*
|
|
42
|
+
* Owner exemption is per class, never per file: `roadmap-progress.cts` defines the
|
|
43
|
+
* first three classes and `phase-entry-grammar.cts` defines the fourth. Each owner
|
|
44
|
+
* is still scanned for the classes it does not own, so a stray pattern added to an
|
|
45
|
+
* owner is caught (ADR-3180 Decision 4(d)). There is no allowlist file and no
|
|
46
|
+
* function-scoped exemption: Phases 1-4 left nothing to allow.
|
|
47
|
+
*
|
|
48
|
+
* The guard carries its own positive control. Every run first feeds a fixture
|
|
49
|
+
* violation of each class through the same detector and requires it to be flagged,
|
|
50
|
+
* and feeds clean fixtures and requires silence. A guard that cannot go red must
|
|
51
|
+
* not report green, so a detector that stops matching fails `lint:ci` here, in the
|
|
52
|
+
* same run, not only in a test file nobody reads. The tree-walk, root confinement,
|
|
53
|
+
* literal tokenizers and report sanitizer are shared with the sibling drift guards
|
|
54
|
+
* through `scripts/lib/drift-scan.cjs`.
|
|
55
|
+
*
|
|
56
|
+
* KNOWN, ACCEPTED limits of a per-line textual scan (same tradeoff the sibling
|
|
57
|
+
* guards document): a pattern whose tokens sit on different lines, one assembled
|
|
58
|
+
* from several literals, one routed through dynamic dispatch, or a `.replace()` on a
|
|
59
|
+
* variable whose name says neither ROADMAP nor Progress is not caught. The detector
|
|
60
|
+
* is name-sensitive on purpose: it prefers a missed exotic shape to a false
|
|
61
|
+
* positive on ordinary new code, because a guard that cries wolf gets exempted. The
|
|
62
|
+
* cross-site property tests of Phases 1-4 and code review cover the rest.
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
const path = require('node:path');
|
|
66
|
+
const driftScan = require('./lib/drift-scan.cjs');
|
|
67
|
+
const { readRegexLiteralAt, readStringLiteralAt, MAX_REGEX_LITERAL_LEN, sanitizeForReport, scanTree } = driftScan;
|
|
68
|
+
|
|
69
|
+
// Authored source and hooks. Generated mirrors (`gsd-core/bin/lib/*.cjs`) are
|
|
70
|
+
// build output of `src/`; `scripts/` carries these tokens as detector text and
|
|
71
|
+
// `tests/` builds ROADMAP fixtures on purpose.
|
|
72
|
+
const SCAN_DIRS = ['src', 'hooks'];
|
|
73
|
+
const SCAN_EXT = new Set(['.cts', '.ts', '.mts', '.cjs', '.js', '.mjs']);
|
|
74
|
+
|
|
75
|
+
const CLASS = Object.freeze({
|
|
76
|
+
PROGRESS_HEADING: 'progress-heading',
|
|
77
|
+
PROGRESS_TITLE: 'progress-title',
|
|
78
|
+
PROGRESS_TABLE_REPLACE: 'progress-table-replace',
|
|
79
|
+
PHASE_ENTRY_CHECKBOX: 'phase-entry-checkbox',
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const PROGRESS_OWNER = 'src/roadmap-progress.cts';
|
|
83
|
+
const GRAMMAR_OWNER = 'src/phase-entry-grammar.cts';
|
|
84
|
+
|
|
85
|
+
// Which classes each owner DEFINES (and so may spell). Everything else about an
|
|
86
|
+
// owner file is scanned like any other file.
|
|
87
|
+
const OWNED_CLASSES = new Map([
|
|
88
|
+
[PROGRESS_OWNER, new Set([CLASS.PROGRESS_HEADING, CLASS.PROGRESS_TITLE, CLASS.PROGRESS_TABLE_REPLACE])],
|
|
89
|
+
[GRAMMAR_OWNER, new Set([CLASS.PHASE_ENTRY_CHECKBOX])],
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
// A level-N heading marker (`#`, `##`, `#+`, `#*`, `#{1,3}`) followed by whitespace
|
|
93
|
+
// tokens, classes or a quantified gap (`\s*`, `[ \t]+`, ` {1,3}`, a space) and then
|
|
94
|
+
// the word `Progress`. The gap group is a bounded repeat of alternatives that
|
|
95
|
+
// cannot consume the same character two ways (a quantifier belongs to the token
|
|
96
|
+
// before it, never to the group), so matching is linear in the literal length: a
|
|
97
|
+
// repeat of `*` or `+` as its own alternative would make `\s*\s*\s*...` ambiguous
|
|
98
|
+
// and exponential (CodeQL js/redos runs on `lint:ci`).
|
|
99
|
+
const HEADING_THEN_PROGRESS_RE =
|
|
100
|
+
/#(?:\{\d{1,2},\d{1,2}\}|#{0,5}|[+*])(?:\\{1,2}[sSh][*+?]?|\[[ \\t]{1,6}\][*+?]?|\{\d{1,2}(?:,\d{0,2})?\}| ){0,12}[*_]{0,2}(?:\(\?:|\()?Progress/i;
|
|
101
|
+
// The tail the bold marker and the group opener add (`**Progress`, `(?:Progress`):
|
|
102
|
+
// not regex syntax in the gap, so it is cut before the syntax test.
|
|
103
|
+
const PROGRESS_TAIL_RE = /[*_]{0,2}(?:\(\?:|\()?Progress$/i;
|
|
104
|
+
// Regex syntax inside the heading gap: an escape, a class, a quantifier brace, a
|
|
105
|
+
// repeat. A plain `## Progress` (or `## **Progress**`) is a heading someone is WRITING.
|
|
106
|
+
const REGEX_SYNTAX_RE = /[\\[\]{}+*]/;
|
|
107
|
+
const CARET_BEFORE_HASH_RE = /\^[ \t]*#/;
|
|
108
|
+
|
|
109
|
+
// The bare Progress title test: `Progress` followed by a word boundary, right after
|
|
110
|
+
// a quote, a slash or an anchor. Not after `|`: `/^(done|progress\b)/` is status
|
|
111
|
+
// vocabulary, not a heading test.
|
|
112
|
+
const PROGRESS_TITLE_RE = /(?:^['"`/]|\^)(?:\\{1,2}s[*+]?)?Progress\\{1,2}b/i;
|
|
113
|
+
|
|
114
|
+
const REPLACE_CALL_G = /\.replace(?:All)?\s*\(/g;
|
|
115
|
+
const ROADMAP_WORD_RE = /roadmap/i;
|
|
116
|
+
// A first-argument identifier that names a Progress table pattern: `progressRowRe`,
|
|
117
|
+
// `PROGRESS_TABLE_RE`, `progressSectionPattern`.
|
|
118
|
+
const PROGRESS_PATTERN_IDENT_RE = /progress[_\w$]{0,40}?(?:table|row|section|heading)/i;
|
|
119
|
+
const PROGRESS_WORD_RE = /progress/i;
|
|
120
|
+
// The Progress table's own header text (`Plans Complete`), with a literal or escaped space.
|
|
121
|
+
const PLANS_COMPLETE_RE = /Plans(?: |\\{1,2}s[*+]?)Complete/i;
|
|
122
|
+
const ESCAPED_PIPE_G = /\\{1,2}\|/g;
|
|
123
|
+
|
|
124
|
+
// A checkbox written as regex syntax: `\[` + a box interior + `\]`. The interior is
|
|
125
|
+
// 1 to 12 characters drawn from the box alphabet: a space, `x`/`X`, `.`, `\s`, class
|
|
126
|
+
// and group punctuation and the quantifiers (`[ xX]`, `[x ]`, `( |x)`, `([xX ])`,
|
|
127
|
+
// ` ?[xX]? ?`, `.`). Letters other than x/X, digits and `^` are outside it, so a
|
|
128
|
+
// markdown link such as `\[Phase 1\]` and the heading bracket tag
|
|
129
|
+
// `\[[^\]]{1,200}\]` (a different seam: `### [tag] Phase N:`) are not checkboxes. A
|
|
130
|
+
// checkbox literal in a template that EMITS an entry (`- [ ] **Phase 1**`) has no
|
|
131
|
+
// backslashes and is not a pattern.
|
|
132
|
+
const ESCAPED_CHECKBOX_RE = /\\{1,2}\[[ xX.\\s?*+|()[\]-]{1,12}\\{1,2}\]/;
|
|
133
|
+
// The word `Phase`, case-insensitive, not inside a longer word or a hyphenated name
|
|
134
|
+
// (`Phase-gate`), and also right after a regex class escape (`\sPhase`).
|
|
135
|
+
const PHASE_WORD_RE = /(?:^|[^A-Za-z]|\\[sSwWdDbB])Phase(?![A-Za-z-])/i;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Strip comment text before detection: a guard that fires on a comment trains
|
|
139
|
+
* readers to add exemptions for documentation. Whole-line `//` comments and block
|
|
140
|
+
* comment continuations are dropped, a block comment opened and closed on the line
|
|
141
|
+
* is cut out, and a trailing `//` after code is cut. Quote- and regex-aware, so
|
|
142
|
+
* `https://` in a string or `//` inside a regex literal does not cut the line (the
|
|
143
|
+
* older guards use `indexOf('//')` and over-cut). Kept local rather than shared
|
|
144
|
+
* because each guard applies comment stripping at a different point of its
|
|
145
|
+
* pipeline and the variants differ on purpose.
|
|
146
|
+
*/
|
|
147
|
+
function stripComments(line) {
|
|
148
|
+
const trimmed = line.trim();
|
|
149
|
+
if (trimmed.startsWith('/*')) {
|
|
150
|
+
const close = line.indexOf('*/');
|
|
151
|
+
return close === -1 ? '' : stripComments(line.slice(close + 2));
|
|
152
|
+
}
|
|
153
|
+
if (trimmed.startsWith('*') || trimmed.startsWith('//')) return '';
|
|
154
|
+
let quote = null;
|
|
155
|
+
for (let i = 0; i < line.length; i++) {
|
|
156
|
+
const ch = line[i];
|
|
157
|
+
if (quote) {
|
|
158
|
+
if (ch === '\\') i++;
|
|
159
|
+
else if (ch === quote) quote = null;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (ch === "'" || ch === '"' || ch === '`') quote = ch;
|
|
163
|
+
else if (ch === '/' && line[i + 1] === '/') return line.slice(0, i);
|
|
164
|
+
else if (ch === '/' && line[i + 1] !== '*') {
|
|
165
|
+
// A `/` may open a regex literal or be a division. A real literal is skipped so
|
|
166
|
+
// a `//` inside it is not read as a comment; a division reads as a "literal"
|
|
167
|
+
// that closes at the next `/`, which only ever skips code, never a comment
|
|
168
|
+
// opener that follows it on the same line (that case is re-checked below).
|
|
169
|
+
const re = readRegexLiteralAt(line, i);
|
|
170
|
+
if (re && !re.text.slice(1, -1).includes('//')) i = re.end - 1;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return line;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Every regex literal and quoted/backtick string literal on `line`, in order. A `/`
|
|
178
|
+
* is either a regex opener or a division, and the two cannot be told apart without
|
|
179
|
+
* a parser, so each `/` yields a CANDIDATE literal and scanning resumes just after
|
|
180
|
+
* the opening `/` (candidates overlap): `done / total, re = /^##\s+Progress/m`
|
|
181
|
+
* yields the division pseudo-literal and the real regex. Strings are exact, so a
|
|
182
|
+
* string literal is consumed whole.
|
|
183
|
+
*/
|
|
184
|
+
function literalsOn(line) {
|
|
185
|
+
const out = [];
|
|
186
|
+
for (let i = 0; i < line.length; i++) {
|
|
187
|
+
const ch = line[i];
|
|
188
|
+
if (ch === '/') {
|
|
189
|
+
const literal = readRegexLiteralAt(line, i);
|
|
190
|
+
if (literal) out.push(literal.text);
|
|
191
|
+
} else if (ch === "'" || ch === '"' || ch === '`') {
|
|
192
|
+
const literal = readStringLiteralAt(line, i);
|
|
193
|
+
if (literal) {
|
|
194
|
+
out.push(literal.text);
|
|
195
|
+
i = literal.end - 1;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function isProgressHeadingLiteral(text) {
|
|
203
|
+
const match = HEADING_THEN_PROGRESS_RE.exec(text);
|
|
204
|
+
if (!match) return false;
|
|
205
|
+
const gap = match[0].slice(1).replace(PROGRESS_TAIL_RE, '');
|
|
206
|
+
return REGEX_SYNTAX_RE.test(gap) || CARET_BEFORE_HASH_RE.test(text);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* The first argument of the `.replace(` / `.replaceAll(` call that opens at
|
|
211
|
+
* `code[open]` (the index just past the `(`): a regex literal, a string literal or
|
|
212
|
+
* an identifier path. Null when the argument is anything else.
|
|
213
|
+
*/
|
|
214
|
+
function firstReplaceArgument(code, open) {
|
|
215
|
+
let i = open;
|
|
216
|
+
while (code[i] === ' ' || code[i] === '\t') i++;
|
|
217
|
+
const ch = code[i];
|
|
218
|
+
if (ch === '/') {
|
|
219
|
+
const literal = readRegexLiteralAt(code, i);
|
|
220
|
+
return literal ? { kind: 'regex', text: literal.text } : null;
|
|
221
|
+
}
|
|
222
|
+
if (ch === "'" || ch === '"' || ch === '`') {
|
|
223
|
+
const literal = readStringLiteralAt(code, i);
|
|
224
|
+
return literal ? { kind: 'string', text: literal.text } : null;
|
|
225
|
+
}
|
|
226
|
+
const ident = /^[A-Za-z_$][\w$.]*/.exec(code.slice(i, i + 120));
|
|
227
|
+
return ident ? { kind: 'identifier', text: ident[0] } : null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function isProgressTableReplace(code) {
|
|
231
|
+
const namesRoadmap = ROADMAP_WORD_RE.test(code);
|
|
232
|
+
for (const call of code.matchAll(REPLACE_CALL_G)) {
|
|
233
|
+
const arg = firstReplaceArgument(code, call.index + call[0].length);
|
|
234
|
+
if (!arg) continue;
|
|
235
|
+
if (arg.kind === 'identifier') {
|
|
236
|
+
if (PROGRESS_PATTERN_IDENT_RE.test(arg.text) || (namesRoadmap && PROGRESS_WORD_RE.test(arg.text))) return true;
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (PLANS_COMPLETE_RE.test(arg.text)) return true;
|
|
240
|
+
if (namesRoadmap && (PROGRESS_WORD_RE.test(arg.text) || (arg.text.match(ESCAPED_PIPE_G) || []).length >= 2)) return true;
|
|
241
|
+
}
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Pure: every unsanctioned Progress / phase-entry pattern in `text`.
|
|
247
|
+
* `relPath` is the repo-relative POSIX path, used to report file:line and to apply
|
|
248
|
+
* the per-class owner exemption. Returns [{ line, cls, found }], where `found` is
|
|
249
|
+
* the matched literal (or the offending line for the replace class).
|
|
250
|
+
*/
|
|
251
|
+
function findProgressDrift(text, relPath) {
|
|
252
|
+
const owned = OWNED_CLASSES.get(relPath) || new Set();
|
|
253
|
+
const out = [];
|
|
254
|
+
const lines = text.split('\n');
|
|
255
|
+
for (let i = 0; i < lines.length; i++) {
|
|
256
|
+
const code = stripComments(lines[i]);
|
|
257
|
+
if (!code.trim()) continue;
|
|
258
|
+
const literals = literalsOn(code);
|
|
259
|
+
const report = (cls, found) => {
|
|
260
|
+
if (!owned.has(cls)) out.push({ line: i + 1, cls, found: found.slice(0, MAX_REGEX_LITERAL_LEN) });
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const headingLiteral = literals.find(isProgressHeadingLiteral);
|
|
264
|
+
if (headingLiteral !== undefined) report(CLASS.PROGRESS_HEADING, headingLiteral);
|
|
265
|
+
|
|
266
|
+
const titleLiteral = literals.find((t) => PROGRESS_TITLE_RE.test(t));
|
|
267
|
+
if (titleLiteral !== undefined) report(CLASS.PROGRESS_TITLE, titleLiteral);
|
|
268
|
+
|
|
269
|
+
if (isProgressTableReplace(code)) report(CLASS.PROGRESS_TABLE_REPLACE, code.trim());
|
|
270
|
+
|
|
271
|
+
const checkboxLiteral = literals.find((t) => ESCAPED_CHECKBOX_RE.test(t) && PHASE_WORD_RE.test(t));
|
|
272
|
+
if (checkboxLiteral !== undefined) report(CLASS.PHASE_ENTRY_CHECKBOX, checkboxLiteral);
|
|
273
|
+
}
|
|
274
|
+
return out;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const toPosix = (rel) => rel.split(path.sep).join('/');
|
|
278
|
+
|
|
279
|
+
function scanRepo(root) {
|
|
280
|
+
return scanTree({
|
|
281
|
+
root,
|
|
282
|
+
scanDirs: SCAN_DIRS,
|
|
283
|
+
scanExt: SCAN_EXT,
|
|
284
|
+
onFile(rel, text) {
|
|
285
|
+
const posix = toPosix(rel);
|
|
286
|
+
return findProgressDrift(text, posix).map((d) => ({ file: posix, ...d }));
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
// Positive control. One fixture per class that MUST be flagged, one per class
|
|
293
|
+
// that must NOT, and the owner-exemption boundary. The fixtures are source text
|
|
294
|
+
// handed to the same detector; nothing here touches the tree.
|
|
295
|
+
// ---------------------------------------------------------------------------
|
|
296
|
+
const FIXTURE_PATH = 'src/fixture-violation.cts';
|
|
297
|
+
|
|
298
|
+
const MUST_FLAG = [
|
|
299
|
+
{ cls: CLASS.PROGRESS_HEADING, text: 'const m = content.match(/^##[ \\t]+Progress\\b/im);' },
|
|
300
|
+
{ cls: CLASS.PROGRESS_HEADING, text: 'const re = new RegExp(`^#{1,3}\\\\s*Progress`);' },
|
|
301
|
+
{ cls: CLASS.PROGRESS_HEADING, text: 'const m = /^##\\s+(Progress)/m.exec(content);' },
|
|
302
|
+
{ cls: CLASS.PROGRESS_HEADING, text: 'const pct = done / total, re = /^##\\s+Progress/m;' },
|
|
303
|
+
{ cls: CLASS.PROGRESS_HEADING, text: '/* gate */ const re = /^##\\s+Progress/m;' },
|
|
304
|
+
{ cls: CLASS.PROGRESS_TITLE, text: 'const isProgress = (t) => /^Progress\\b/i.test(t);' },
|
|
305
|
+
{ cls: CLASS.PROGRESS_TITLE, text: "const SRC = 'Progress\\\\b';" },
|
|
306
|
+
{ cls: CLASS.PROGRESS_TABLE_REPLACE, text: 'const out = roadmapContent.replace(progressRowRe, row);' },
|
|
307
|
+
{ cls: CLASS.PROGRESS_TABLE_REPLACE, text: 'const out = rm.replace(PROGRESS_TABLE_RE, row);' },
|
|
308
|
+
{ cls: CLASS.PROGRESS_TABLE_REPLACE, text: 'const out = roadmap.replaceAll(/\\|\\s*3\\.\\s*Three\\s*\\|/g, "| 3. Three |");' },
|
|
309
|
+
{ cls: CLASS.PROGRESS_TABLE_REPLACE, text: 'const out = text.replace(/\\|\\s*Plans Complete\\s*\\|/, "| Plans |");' },
|
|
310
|
+
{ cls: CLASS.PHASE_ENTRY_CHECKBOX, text: 'const re = /^- \\[ \\] \\*\\*Phase (\\d+)/m;' },
|
|
311
|
+
{ cls: CLASS.PHASE_ENTRY_CHECKBOX, text: 'const re = /^\\s*-\\s*\\[ ?[xX]? ?\\]\\s*\\*\\*Phase/;' },
|
|
312
|
+
{ cls: CLASS.PHASE_ENTRY_CHECKBOX, text: 'const re = /^- \\[.\\] \\*\\*Phase/;' },
|
|
313
|
+
{ cls: CLASS.PHASE_ENTRY_CHECKBOX, text: 'const re = /^-\\s\\[\\s\\]\\sPhase/;' },
|
|
314
|
+
{ cls: CLASS.PHASE_ENTRY_CHECKBOX, text: 'const re = new RegExp(`-\\\\s*\\\\[[ xX]\\\\]\\\\s*\\\\*\\\\*Phase ${n}`);' },
|
|
315
|
+
];
|
|
316
|
+
|
|
317
|
+
const MUST_STAY_CLEAN = [
|
|
318
|
+
// A heading being WRITTEN: no regex syntax.
|
|
319
|
+
{ name: 'heading builder', text: "const heading = '## Progress\\n\\n';" },
|
|
320
|
+
{ name: 'bold heading builder', text: "const heading = '## **Progress**\\n';" },
|
|
321
|
+
// STATE.md field, status vocabulary.
|
|
322
|
+
{ name: 'STATE progress field', text: 'const m = /^Progress: (.*)$/m.exec(body);' },
|
|
323
|
+
{ name: 'status vocabulary', text: 'const m = content.match(/##\\s*Status:\\s*(OPEN|IN PROGRESS)/i);' },
|
|
324
|
+
{ name: 'status alternation', text: 'const m = /^(done|progress\\b)/i.exec(word);' },
|
|
325
|
+
// Other checkboxes and a different seam: the heading bracket tag, a markdown link.
|
|
326
|
+
{ name: 'requirement checkbox', text: 'const re = /^- \\[[ xX]\\] \\*\\*REQ-\\d+\\*\\*/m;' },
|
|
327
|
+
{ name: 'hyphenated Phase', text: 'const re = /^- \\[[ x]\\] .*Phase-gate/;' },
|
|
328
|
+
{ name: 'emitted entry', text: 'const line = `- [ ] **Phase ${n}: ${name}**`;' },
|
|
329
|
+
{ name: 'heading bracket tag', text: 'const re = /^#{2,4}\\s*(?:\\[[^\\]]{1,200}\\]\\s*)?Phase\\s+(\\d+)/;' },
|
|
330
|
+
{ name: 'markdown link', text: 'const re = /\\[Phase (\\d+)\\]\\(#phase-\\1\\)/;' },
|
|
331
|
+
{ name: 'comment', text: '// the old /^##[ \\t]+Progress\\b/im and /^- \\[ \\] \\*\\*Phase/ shapes' },
|
|
332
|
+
// A replace whose first argument is not a Progress table pattern.
|
|
333
|
+
{ name: 'unrelated replace', text: 'const out = roadmapContent.replace(/\\r\\n/g, "\\n");' },
|
|
334
|
+
{ name: 'pipe escape for output', text: "const cell = roadmap.title.replace(/\\|/g, '\\\\|');" },
|
|
335
|
+
{ name: 'progress variable receiver', text: "const pct = roadmapProgress.replace(/%$/, '');" },
|
|
336
|
+
{ name: 'replace inside a template', text: 'const label = `roadmap progress ${name.replace(/-/g, " ")}`;' },
|
|
337
|
+
];
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Run the positive and negative controls. Returns a list of human-readable
|
|
341
|
+
* failures; empty means the detector can go red where it must and stays quiet where
|
|
342
|
+
* it must.
|
|
343
|
+
*/
|
|
344
|
+
function runPositiveControl() {
|
|
345
|
+
const failures = [];
|
|
346
|
+
for (const fixture of MUST_FLAG) {
|
|
347
|
+
const found = findProgressDrift(fixture.text, FIXTURE_PATH).filter((d) => d.cls === fixture.cls);
|
|
348
|
+
if (found.length === 0) failures.push(`not flagged (${fixture.cls}): ${fixture.text}`);
|
|
349
|
+
}
|
|
350
|
+
for (const fixture of MUST_STAY_CLEAN) {
|
|
351
|
+
const found = findProgressDrift(fixture.text, FIXTURE_PATH);
|
|
352
|
+
if (found.length > 0) failures.push(`flagged but clean (${fixture.name}): ${found.map((d) => d.cls).join(',')}`);
|
|
353
|
+
}
|
|
354
|
+
// Owner boundary: each owner may spell its own classes and nothing else.
|
|
355
|
+
const progressSpelling = MUST_FLAG.find((f) => f.cls === CLASS.PROGRESS_HEADING).text;
|
|
356
|
+
const checkboxSpelling = MUST_FLAG.find((f) => f.cls === CLASS.PHASE_ENTRY_CHECKBOX).text;
|
|
357
|
+
const boundary = [
|
|
358
|
+
[PROGRESS_OWNER, progressSpelling, 0, 'progress owner may spell a Progress heading'],
|
|
359
|
+
[PROGRESS_OWNER, checkboxSpelling, 1, 'progress owner may NOT spell a phase-entry checkbox'],
|
|
360
|
+
[GRAMMAR_OWNER, checkboxSpelling, 0, 'grammar owner may spell a phase-entry checkbox'],
|
|
361
|
+
[GRAMMAR_OWNER, progressSpelling, 1, 'grammar owner may NOT spell a Progress heading'],
|
|
362
|
+
];
|
|
363
|
+
for (const [rel, text, expected, label] of boundary) {
|
|
364
|
+
const count = findProgressDrift(text, rel).length;
|
|
365
|
+
if (count !== expected) failures.push(`owner boundary (${label}): expected ${expected}, got ${count}`);
|
|
366
|
+
}
|
|
367
|
+
return failures;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* The failure report as lines: a header, the pointer to the two owners, then one
|
|
372
|
+
* `file:line [class] fragment` line per violation. `found` is the matched
|
|
373
|
+
* literal (or, for the replace class, the offending line), cut to
|
|
374
|
+
* MAX_REGEX_LITERAL_LEN. The path is as attacker-controlled as the matched text,
|
|
375
|
+
* so both pass through `sanitizeForReport`.
|
|
376
|
+
*/
|
|
377
|
+
function formatViolations(violations) {
|
|
378
|
+
return [
|
|
379
|
+
'roadmap-progress-drift: hand-written Progress-table or phase-entry pattern(s) found.',
|
|
380
|
+
'Locate and edit the Progress table through src/roadmap-progress.cts (`locateRoadmapProgress`, `findProgressSection`,',
|
|
381
|
+
'`editRoadmapProgress`) and compose phase-entry patterns from src/phase-entry-grammar.cts (`phaseEntryRegExp`,',
|
|
382
|
+
'`phaseEntrySplitRegExp`) instead of spelling them again:',
|
|
383
|
+
...violations.map((d) => ` ${sanitizeForReport(d.file)}:${d.line} [${d.cls}] ${sanitizeForReport(d.found)}`),
|
|
384
|
+
];
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function main() {
|
|
388
|
+
const controlFailures = runPositiveControl();
|
|
389
|
+
if (controlFailures.length > 0) {
|
|
390
|
+
process.stderr.write('roadmap-progress-drift: the guard failed its own positive control; a detector that cannot go red is not a guard.\n');
|
|
391
|
+
for (const f of controlFailures) process.stderr.write(` ${f}\n`);
|
|
392
|
+
process.exitCode = 1;
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const root = path.join(__dirname, '..');
|
|
396
|
+
const violations = scanRepo(root);
|
|
397
|
+
if (violations.length === 0) {
|
|
398
|
+
process.stdout.write('ok roadmap-progress-drift: no hand-written Progress-table or phase-entry pattern outside roadmap-progress.cts and phase-entry-grammar.cts (positive control passed)\n');
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
for (const line of formatViolations(violations)) process.stderr.write(`${line}\n`);
|
|
402
|
+
process.exitCode = 1;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (require.main === module) main();
|
|
406
|
+
|
|
407
|
+
module.exports = {
|
|
408
|
+
findProgressDrift,
|
|
409
|
+
scanRepo,
|
|
410
|
+
runPositiveControl,
|
|
411
|
+
formatViolations,
|
|
412
|
+
CLASS,
|
|
413
|
+
PROGRESS_OWNER,
|
|
414
|
+
GRAMMAR_OWNER,
|
|
415
|
+
OWNED_CLASSES,
|
|
416
|
+
SCAN_DIRS,
|
|
417
|
+
MUST_FLAG,
|
|
418
|
+
MUST_STAY_CLEAN,
|
|
419
|
+
stripComments,
|
|
420
|
+
literalsOn,
|
|
421
|
+
};
|
|
@@ -657,7 +657,29 @@ const COVERED = {
|
|
|
657
657
|
'state-contract': {
|
|
658
658
|
cjs: 'gsd-core/bin/lib/state-contract.cjs',
|
|
659
659
|
excludeTests: ['state-contract.test.cjs'],
|
|
660
|
-
|
|
660
|
+
// #5764: CI run 40800623930, job 123207881000, `Stryker (state-contract)`: measured 70.43%
|
|
661
|
+
// once the checklist fallback ran through the phase-entry grammar; floor(70.43)-1.
|
|
662
|
+
minScore: 69,
|
|
663
|
+
},
|
|
664
|
+
// #4998 review (trek-e 5563202317 Nit 6, 5608265301 Minor 4): the STATE.md and
|
|
665
|
+
// planning-document field readers and writers #4998 grew.
|
|
666
|
+
// state-document: CI run 39742269013 (job 119702274448) measured 50.47% in a 94-minute
|
|
667
|
+
// job. floor(50.47) - 1 = 49 is below the range minimum, so minScore stays at 50.
|
|
668
|
+
// 140 is about 1.5x the measured job time.
|
|
669
|
+
'state-document': {
|
|
670
|
+
cjs: 'gsd-core/bin/lib/state-document.cjs',
|
|
671
|
+
minScore: 50,
|
|
672
|
+
timeoutMinutes: 140,
|
|
673
|
+
},
|
|
674
|
+
// planning-document.test.cjs requires the module through a MODULE_PATH constant, which
|
|
675
|
+
// the require derivation cannot follow; frontmatter-fence.test.cjs requires it directly
|
|
676
|
+
// but sits outside the naming rule. Both run in-process, with no subprocess.
|
|
677
|
+
'planning-document': {
|
|
678
|
+
cjs: 'gsd-core/bin/lib/planning-document.cjs',
|
|
679
|
+
extraTests: ['frontmatter-fence.test.cjs', 'planning-document.test.cjs'],
|
|
680
|
+
// CI run 39736529871, job 119683571231 (#5234, head 43f1bfa93): 75.14%, Stryker in
|
|
681
|
+
// about 3 minutes. Floor = floor(75.14) - 1 = 74.
|
|
682
|
+
minScore: 74,
|
|
661
683
|
},
|
|
662
684
|
};
|
|
663
685
|
|
|
@@ -252,6 +252,39 @@ function runInstalledTreeAudit(cwd, opts = {}) {
|
|
|
252
252
|
return runNpmAuditWithRetry(cwd, ['audit', '--omit=dev', '--json'], opts);
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
+
/**
|
|
256
|
+
* `-c safe.directory=<repoRoot>` for the baseline's git calls (#5659). On the gsd-test bench the
|
|
257
|
+
* checkout is owned by a different uid than the one running the tests, so git refuses every command
|
|
258
|
+
* ("detected dubious ownership") and the baseline silently resolved to nothing, which sent the
|
|
259
|
+
* integrity tests to their zero-tolerance fallback. git honors `safe.directory` from the command line
|
|
260
|
+
* only; scoping it to the audited repo root (never `*`, never global config) trusts exactly the
|
|
261
|
+
* checkout being audited. Same precedent as scripts/lint-compiled-artifact-sync.cjs.
|
|
262
|
+
*/
|
|
263
|
+
function safeDirectoryArgs(repoRoot) {
|
|
264
|
+
// git compares safe.directory against its forward-slash path, also on Windows.
|
|
265
|
+
return ['-c', `safe.directory=${String(repoRoot).replace(/\\/g, '/')}`];
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* One line saying why no baseline resolved (#5659), for the integrity gate's zero-tolerance message.
|
|
270
|
+
* Without it a git refusal (ownership, a missing git, a broken checkout) is indistinguishable from a
|
|
271
|
+
* checkout that simply has no `origin/next`, which is how the ownership problem hid. Only the first
|
|
272
|
+
* line git printed is kept.
|
|
273
|
+
*/
|
|
274
|
+
function explainNoBaseline(repoRoot, { execFileSyncImpl = execFileSync } = {}) {
|
|
275
|
+
try {
|
|
276
|
+
execFileSyncImpl('git', [...safeDirectoryArgs(repoRoot), 'rev-parse', '--git-dir'], {
|
|
277
|
+
cwd: repoRoot,
|
|
278
|
+
encoding: 'utf-8',
|
|
279
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
280
|
+
});
|
|
281
|
+
} catch (error) {
|
|
282
|
+
const detail = String((error && (error.stderr || error.message)) || '').trim().split(/\r?\n/)[0];
|
|
283
|
+
return `git could not read ${repoRoot}${detail ? `: ${detail}` : ''}`;
|
|
284
|
+
}
|
|
285
|
+
return 'no baseline ref (origin/next, next) resolved, or it lacks package.json / package-lock.json';
|
|
286
|
+
}
|
|
287
|
+
|
|
255
288
|
/**
|
|
256
289
|
* Extracts package.json + package-lock.json (optionally under `subdir`,
|
|
257
290
|
* e.g. 'sdk') from `ref` at git object level into a fresh temp directory --
|
|
@@ -260,17 +293,17 @@ function runInstalledTreeAudit(cwd, opts = {}) {
|
|
|
260
293
|
* clone that never fetched it) or either file is absent at that ref --
|
|
261
294
|
* callers treat `null` as "no baseline available", not an error.
|
|
262
295
|
*/
|
|
263
|
-
function extractBaselineTree(ref, repoRoot, subdir = '') {
|
|
296
|
+
function extractBaselineTree(ref, repoRoot, subdir = '', { execFileSyncImpl = execFileSync } = {}) {
|
|
264
297
|
const rel = (name) => (subdir ? path.posix.join(subdir, name) : name);
|
|
265
298
|
let pkgJson;
|
|
266
299
|
let lockJson;
|
|
267
300
|
try {
|
|
268
|
-
pkgJson =
|
|
301
|
+
pkgJson = execFileSyncImpl('git', [...safeDirectoryArgs(repoRoot), 'show', `${ref}:${rel('package.json')}`], {
|
|
269
302
|
cwd: repoRoot,
|
|
270
303
|
encoding: 'utf-8',
|
|
271
304
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
272
305
|
});
|
|
273
|
-
lockJson =
|
|
306
|
+
lockJson = execFileSyncImpl('git', [...safeDirectoryArgs(repoRoot), 'show', `${ref}:${rel('package-lock.json')}`], {
|
|
274
307
|
cwd: repoRoot,
|
|
275
308
|
encoding: 'utf-8',
|
|
276
309
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
@@ -322,12 +355,12 @@ const NULL_SHA = '0000000000000000000000000000000000000000';
|
|
|
322
355
|
* Returns '' if none resolve -- callers fall back to strict zero-tolerance
|
|
323
356
|
* rather than silently skipping the gate.
|
|
324
357
|
*/
|
|
325
|
-
function resolveBaselineRef(repoRoot) {
|
|
358
|
+
function resolveBaselineRef(repoRoot, { execFileSyncImpl = execFileSync } = {}) {
|
|
326
359
|
if (process.env.AUDIT_BASELINE_REF) return process.env.AUDIT_BASELINE_REF;
|
|
327
360
|
if (process.env.GITHUB_BASE_REF) return `origin/${process.env.GITHUB_BASE_REF}`;
|
|
328
361
|
if (process.env.GITHUB_EVENT_NAME === 'push') {
|
|
329
362
|
try {
|
|
330
|
-
const parent =
|
|
363
|
+
const parent = execFileSyncImpl('git', [...safeDirectoryArgs(repoRoot), 'rev-parse', '--verify', 'HEAD~1'], {
|
|
331
364
|
cwd: repoRoot,
|
|
332
365
|
encoding: 'utf-8',
|
|
333
366
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
@@ -338,7 +371,7 @@ function resolveBaselineRef(repoRoot) {
|
|
|
338
371
|
}
|
|
339
372
|
}
|
|
340
373
|
try {
|
|
341
|
-
|
|
374
|
+
execFileSyncImpl('git', [...safeDirectoryArgs(repoRoot), 'rev-parse', '--verify', 'origin/next'], {
|
|
342
375
|
cwd: repoRoot,
|
|
343
376
|
stdio: 'ignore',
|
|
344
377
|
});
|
|
@@ -347,7 +380,7 @@ function resolveBaselineRef(repoRoot) {
|
|
|
347
380
|
// fall through to a plain local branch
|
|
348
381
|
}
|
|
349
382
|
try {
|
|
350
|
-
|
|
383
|
+
execFileSyncImpl('git', [...safeDirectoryArgs(repoRoot), 'rev-parse', '--verify', 'next'], {
|
|
351
384
|
cwd: repoRoot,
|
|
352
385
|
stdio: 'ignore',
|
|
353
386
|
});
|
|
@@ -366,6 +399,7 @@ module.exports = {
|
|
|
366
399
|
runNpmAuditWithRetry,
|
|
367
400
|
extractBaselineTree,
|
|
368
401
|
resolveBaselineRef,
|
|
402
|
+
explainNoBaseline,
|
|
369
403
|
isTimeoutKill,
|
|
370
404
|
buildTimeoutKillError,
|
|
371
405
|
sleepSyncMs,
|