@opengsd/gsd-core 1.7.0 → 1.9.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 +45 -1
- package/README.md +2 -0
- package/agents/gsd-code-fixer.md +1 -1
- package/agents/gsd-codebase-mapper.md +1 -1
- package/agents/gsd-debug-session-manager.md +78 -4
- package/agents/gsd-debugger.md +87 -29
- package/agents/gsd-executor.md +49 -9
- package/agents/gsd-intel-updater.md +3 -3
- package/agents/gsd-phase-researcher.md +4 -2
- package/agents/gsd-plan-checker.md +20 -0
- package/agents/gsd-planner.md +44 -59
- package/agents/gsd-project-researcher.md +2 -2
- package/agents/gsd-ui-auditor.md +0 -40
- package/agents/gsd-verifier.md +2 -2
- package/bin/install.js +1338 -135
- package/commands/gsd/ai-integration-phase.md +1 -1
- package/commands/gsd/mempalace-capture.md +9 -5
- package/commands/gsd/new-milestone.md +1 -1
- package/commands/gsd/plan-phase.md +5 -3
- package/commands/gsd/plan-review-convergence.md +7 -2
- package/gsd-core/bin/gsd-tools.cjs +2690 -2472
- package/gsd-core/bin/lib/adapter-imperative.cjs +8 -1
- package/gsd-core/bin/lib/agent-command-router.cjs +20 -5
- package/gsd-core/bin/lib/api-coverage.cjs +360 -53
- package/gsd-core/bin/lib/audit.cjs +8 -8
- package/gsd-core/bin/lib/broken-windows.cjs +716 -0
- package/gsd-core/bin/lib/capability-command-router.cjs +733 -0
- package/gsd-core/bin/lib/capability-consent.cjs +40 -1
- package/gsd-core/bin/lib/capability-lifecycle.cjs +58 -0
- package/gsd-core/bin/lib/capability-loader.cjs +23 -1
- package/gsd-core/bin/lib/capability-registry.cjs +1450 -160
- package/gsd-core/bin/lib/capability-trust.cjs +468 -33
- package/gsd-core/bin/lib/capability-validator.cjs +882 -6
- package/gsd-core/bin/lib/capability-writer.cjs +6 -1
- package/gsd-core/bin/lib/check-command-router.cjs +140 -27
- package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +15 -0
- package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +209 -31
- package/gsd-core/bin/lib/claude-orchestration.cjs +203 -25
- package/gsd-core/bin/lib/command-aliases.cjs +14 -0
- package/gsd-core/bin/lib/commands.cjs +326 -21
- package/gsd-core/bin/lib/config-loader.cjs +214 -30
- package/gsd-core/bin/lib/config.cjs +158 -22
- package/gsd-core/bin/lib/core-utils.cjs +6 -1
- package/gsd-core/bin/lib/decisions.cjs +32 -8
- package/gsd-core/bin/lib/docs.cjs +6 -0
- package/gsd-core/bin/lib/estimate-cli.cjs +336 -0
- package/gsd-core/bin/lib/external-descriptor-trust.cjs +14 -2
- package/gsd-core/bin/lib/frontmatter.cjs +125 -15
- package/gsd-core/bin/lib/gap-checker.cjs +17 -2
- package/gsd-core/bin/lib/host-integration.cjs +215 -8
- package/gsd-core/bin/lib/init.cjs +155 -66
- package/gsd-core/bin/lib/install-engine.cjs +299 -23
- package/gsd-core/bin/lib/install-profiles.cjs +239 -1
- package/gsd-core/bin/lib/installer-migrations/005-opencode-baseline-commands-dir.cjs +146 -0
- package/gsd-core/bin/lib/installer-migrations/006-pi-extension-cjs-to-js.cjs +91 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +44 -5
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +107 -0
- package/gsd-core/bin/lib/milestone.cjs +248 -14
- package/gsd-core/bin/lib/model-catalog.cjs +69 -4
- package/gsd-core/bin/lib/model-resolver.cjs +189 -7
- package/gsd-core/bin/lib/observability/logger.cjs +7 -2
- package/gsd-core/bin/lib/onboard-projection.cjs +11 -8
- package/gsd-core/bin/lib/phase-command-router.cjs +10 -1
- package/gsd-core/bin/lib/phase-estimation.cjs +398 -0
- package/gsd-core/bin/lib/phase-id.cjs +304 -9
- package/gsd-core/bin/lib/phase.cjs +258 -17
- package/gsd-core/bin/lib/plan-drift-guard.cjs +1 -1
- package/gsd-core/bin/lib/plan-scan.cjs +70 -2
- package/gsd-core/bin/lib/planning-workspace.cjs +9 -2
- package/gsd-core/bin/lib/profile-output.cjs +34 -8
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +927 -0
- package/gsd-core/bin/lib/review-lane-invocation.cjs +348 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +594 -0
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +114 -32
- package/gsd-core/bin/lib/roadmap-parser.cjs +61 -10
- package/gsd-core/bin/lib/roadmap.cjs +23 -7
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +38 -5
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +23 -9
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +156 -0
- package/gsd-core/bin/lib/runtime-name-policy.cjs +15 -2
- package/gsd-core/bin/lib/smart-entry.cjs +70 -5
- package/gsd-core/bin/lib/state-document.cjs +171 -24
- package/gsd-core/bin/lib/state-transition.cjs +50 -11
- package/gsd-core/bin/lib/state.cjs +206 -32
- package/gsd-core/bin/lib/surface.cjs +51 -9
- package/gsd-core/bin/lib/uat-predicate.cjs +6 -4
- package/gsd-core/bin/lib/uat.cjs +428 -11
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +2 -2
- package/gsd-core/bin/lib/unusable-input.cjs +216 -0
- package/gsd-core/bin/lib/validate.cjs +44 -8
- package/gsd-core/bin/lib/verification.cjs +163 -31
- package/gsd-core/bin/lib/verify.cjs +348 -42
- package/gsd-core/bin/lib/worktree-safety.cjs +360 -15
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +4 -15
- package/gsd-core/bin/shared/model-catalog.json +5 -0
- package/gsd-core/bin/shared/runtime-aliases.manifest.json +5 -0
- package/gsd-core/references/api-coverage.md +37 -7
- package/gsd-core/references/checkpoints.md +1 -1
- package/gsd-core/references/common-bug-patterns.md +13 -0
- package/gsd-core/references/context-budget.md +40 -0
- package/gsd-core/references/debugger-bug-taxonomy.md +111 -0
- package/gsd-core/references/debugger-fix-acceptance.md +157 -0
- package/gsd-core/references/debugger-philosophy.md +1 -0
- package/gsd-core/references/debugger-prevention.md +98 -0
- package/gsd-core/references/debugger-rca-branching.md +98 -0
- package/gsd-core/references/debugger-repro-hardening.md +130 -0
- package/gsd-core/references/debugger-sbfl.md +110 -0
- package/gsd-core/references/debugger-semantic-recall.md +81 -0
- package/gsd-core/references/execute-phase-quota-recovery.md +55 -0
- package/gsd-core/references/execute-phase-requirement-revert.md +8 -0
- package/gsd-core/references/execute-phase-response-language.md +7 -0
- package/gsd-core/references/gate-prompts.md +6 -3
- package/gsd-core/references/model-profile-resolution.md +64 -13
- package/gsd-core/references/offer-next.md +88 -0
- package/gsd-core/references/planner-antipatterns.md +6 -0
- package/gsd-core/references/planner-mvp-mode.md +12 -13
- package/gsd-core/references/planner-preconditions.md +156 -0
- package/gsd-core/references/planner-reversibility.md +132 -0
- package/gsd-core/references/planning-config.md +2 -1
- package/gsd-core/references/reviewer-instances.md +28 -19
- package/gsd-core/references/runtime-aware-dispatch.md +42 -0
- package/gsd-core/references/skeleton-template.md +1 -1
- package/gsd-core/references/thinking-models-planning.md +3 -1
- package/gsd-core/references/ui-consideration-probe.md +2 -2
- package/gsd-core/references/worktree-branch-check.md +4 -4
- package/gsd-core/templates/DEBUG.md +5 -3
- package/gsd-core/templates/summary-minimal.md +4 -0
- package/gsd-core/templates/summary-standard.md +4 -0
- package/gsd-core/templates/summary.md +7 -0
- package/gsd-core/workflows/add-phase.md +2 -0
- package/gsd-core/workflows/add-tests.md +3 -1
- package/gsd-core/workflows/add-todo.md +32 -1
- package/gsd-core/workflows/ai-integration-phase.md +8 -6
- package/gsd-core/workflows/audit-fix.md +6 -2
- package/gsd-core/workflows/audit-milestone.md +8 -0
- package/gsd-core/workflows/autonomous.md +19 -15
- package/gsd-core/workflows/check-todos.md +5 -3
- package/gsd-core/workflows/cleanup.md +7 -1
- package/gsd-core/workflows/code-review-fix.md +14 -6
- package/gsd-core/workflows/code-review.md +93 -24
- package/gsd-core/workflows/complete-milestone.md +3 -0
- package/gsd-core/workflows/debug.md +35 -7
- package/gsd-core/workflows/diagnose-issues.md +5 -1
- package/gsd-core/workflows/discovery-phase.md +7 -0
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -4
- package/gsd-core/workflows/discuss-phase/modes/auto.md +0 -6
- package/gsd-core/workflows/discuss-phase/templates/context.md +16 -2
- package/gsd-core/workflows/discuss-phase-assumptions.md +18 -9
- package/gsd-core/workflows/discuss-phase.md +2 -2
- package/gsd-core/workflows/do.md +7 -1
- package/gsd-core/workflows/docs-update.md +9 -0
- package/gsd-core/workflows/eval-review.md +4 -1
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +4 -0
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +160 -0
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +4 -4
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +2 -2
- package/gsd-core/workflows/execute-phase.md +110 -149
- package/gsd-core/workflows/execute-plan.md +20 -8
- package/gsd-core/workflows/explore.md +4 -0
- package/gsd-core/workflows/extract-learnings.md +21 -0
- package/gsd-core/workflows/graduation.md +3 -0
- package/gsd-core/workflows/health.md +7 -1
- package/gsd-core/workflows/help/modes/full.md +9 -5
- package/gsd-core/workflows/import.md +11 -2
- package/gsd-core/workflows/inbox.md +7 -0
- package/gsd-core/workflows/ingest-docs.md +19 -10
- package/gsd-core/workflows/manager.md +3 -1
- package/gsd-core/workflows/map-codebase.md +17 -10
- package/gsd-core/workflows/mvp-phase.md +3 -0
- package/gsd-core/workflows/new-milestone.md +79 -23
- package/gsd-core/workflows/new-project.md +28 -19
- package/gsd-core/workflows/new-workspace.md +3 -1
- package/gsd-core/workflows/next.md +5 -2
- package/gsd-core/workflows/onboard.md +3 -0
- package/gsd-core/workflows/plan-phase.md +56 -51
- package/gsd-core/workflows/plan-review-convergence.md +61 -12
- package/gsd-core/workflows/plant-seed.md +3 -0
- package/gsd-core/workflows/profile-user.md +7 -1
- package/gsd-core/workflows/progress.md +31 -3
- package/gsd-core/workflows/quick.md +33 -10
- package/gsd-core/workflows/remove-workspace.md +3 -0
- package/gsd-core/workflows/review.md +172 -585
- package/gsd-core/workflows/scan.md +10 -2
- package/gsd-core/workflows/secure-phase.md +13 -2
- package/gsd-core/workflows/settings-integrations.md +3 -0
- package/gsd-core/workflows/settings.md +3 -0
- package/gsd-core/workflows/ship.md +88 -11
- package/gsd-core/workflows/sketch.md +3 -0
- package/gsd-core/workflows/smart-entry.md +4 -1
- package/gsd-core/workflows/spike.md +7 -1
- package/gsd-core/workflows/ui-phase.md +11 -2
- package/gsd-core/workflows/ui-review.md +11 -1
- package/gsd-core/workflows/undo.md +7 -0
- package/gsd-core/workflows/update.md +106 -5
- package/gsd-core/workflows/validate-phase.md +13 -2
- package/gsd-core/workflows/verify-phase.md +2 -2
- package/gsd-core/workflows/verify-work.md +15 -4
- package/hooks/dist/gsd-context-monitor.js +27 -9
- package/hooks/dist/gsd-cursor-session-start.js +6 -2
- package/hooks/dist/gsd-cursor-stop.js +6 -2
- package/hooks/dist/gsd-cursor-subagent-start.js +6 -2
- package/hooks/dist/gsd-graphify-update.sh +9 -0
- package/hooks/dist/gsd-phase-boundary.sh +14 -2
- package/hooks/dist/gsd-prompt-guard.js +101 -2
- package/hooks/dist/gsd-read-guard.js +100 -2
- package/hooks/dist/gsd-read-injection-scanner.js +109 -2
- package/hooks/dist/gsd-statusline.js +97 -9
- package/hooks/dist/gsd-workflow-guard.js +110 -6
- package/hooks/dist/gsd-worktree-path-guard.js +132 -8
- package/hooks/dist/lib/cursor-workspace.js +74 -0
- package/hooks/gsd-context-monitor.js +27 -9
- package/hooks/gsd-cursor-session-start.js +6 -2
- package/hooks/gsd-cursor-stop.js +6 -2
- package/hooks/gsd-cursor-subagent-start.js +6 -2
- package/hooks/gsd-graphify-update.sh +9 -0
- package/hooks/gsd-phase-boundary.sh +14 -2
- package/hooks/gsd-prompt-guard.js +101 -2
- package/hooks/gsd-read-guard.js +100 -2
- package/hooks/gsd-read-injection-scanner.js +109 -2
- package/hooks/gsd-statusline.js +97 -9
- package/hooks/gsd-workflow-guard.js +110 -6
- package/hooks/gsd-worktree-path-guard.js +132 -8
- package/hooks/lib/cursor-workspace.js +74 -0
- package/package.json +10 -8
- package/pi/gsd.cjs +34 -3
- package/scripts/changeset/lint.cjs +1 -0
- package/scripts/changeset/parse.cjs +26 -0
- package/scripts/check-coverage-gate.cjs +51 -0
- package/scripts/check-glossary-refs.cjs +244 -0
- package/scripts/ci-rebase-check.cjs +48 -4
- package/scripts/ci-test-scope.cjs +67 -17
- package/scripts/gen-adr-index.cjs +528 -0
- package/scripts/gen-capability-matrix.cjs +26 -2
- package/scripts/gen-capability-registry.cjs +132 -34
- package/scripts/gen-emitted-baseline.cjs +145 -0
- package/scripts/gen-test-timings.cjs +201 -0
- package/scripts/lint-compiled-artifact-sync.cjs +146 -0
- package/scripts/lint-emitted-drift-ack.cjs +149 -0
- package/scripts/lint-fix-has-regression-test.cjs +131 -0
- package/scripts/lint-portable-timeout.cjs +140 -0
- package/scripts/lint-resolution-provenance.cjs +9 -0
- package/scripts/lint-test-file-count.allowlist.json +1 -0
- package/scripts/mutation-matrix.cjs +4 -0
- package/scripts/prompt-injection-scan.sh +6 -0
- package/scripts/registry-schema.cjs +57 -8
- package/scripts/release-notes/conventional-title.cjs +19 -1
- package/scripts/release-notes/format-github-release-notes.cjs +7 -3
- package/scripts/release-tarball-smoke.cjs +18 -11
- package/scripts/run-tests.cjs +420 -58
- package/scripts/workflow-size.cjs +16 -8
- package/skills/gsd-ai-integration-phase/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +9 -5
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-plan-phase/SKILL.md +5 -3
- package/skills/gsd-plan-review-convergence/SKILL.md +7 -2
- package/vscode/package.json +1 -1
- package/scripts/gen-golden-install-parity-zcode.cjs +0 -77
- package/scripts/update-size-baseline.cjs +0 -68
|
@@ -48,6 +48,88 @@ const OPTIONAL_PHASE_TAG_SOURCE = '(?:\\s*\\([^)\\n]{0,200}\\))?';
|
|
|
48
48
|
// (scripts/lint-phase-id-drift.cjs) fails CI if a literal re-derivation is
|
|
49
49
|
// introduced outside this module without a `// phase-id-owner:` justification.
|
|
50
50
|
const PHASE_NUMBER_TOKEN_SOURCE = '\\d+[A-Z]?(?:\\.\\d+)*';
|
|
51
|
+
// #2232: the canonical CONTINUATION-segment grammar — a dash-separated segment
|
|
52
|
+
// that extends a phase token (a zero-padded sub-phase or plan number, e.g. the
|
|
53
|
+
// "01" in "02-01-setup"). getPhaseDirFromPhaseId writes these zero-padded to
|
|
54
|
+
// exactly 2 digits, so the digit RUN of a genuine continuation is exactly 2:
|
|
55
|
+
// #2043's `\d{2,}` (2-or-more) over-collected a slug word that merely leads
|
|
56
|
+
// with ≥2 digits (a year: "14-2026-photos-…" yielded token "14-2026", so every
|
|
57
|
+
// phase-locating verb reported the phase as missing). The `(?!\d)` guard caps
|
|
58
|
+
// the run at 2 without anchoring what may follow, so call sites keep their own
|
|
59
|
+
// trailing grammar (letter suffixes, dotted sub-phases, segment boundaries).
|
|
60
|
+
// POLICY (locked by boundary tests): sub-phase/plan numbers ≥100 are out of the
|
|
61
|
+
// dir-token grammar — the LEADING phase number stays unbounded (`\d+`), only
|
|
62
|
+
// continuation segments are width-capped. Shared from here so the five #2043
|
|
63
|
+
// call sites cannot drift independently (see scripts/lint-phase-id-drift.cjs).
|
|
64
|
+
const PHASE_CONTINUATION_SEGMENT_SOURCE = '\\d{2}(?!\\d)';
|
|
65
|
+
const PHASE_CONTINUATION_SEGMENT_PREFIX_RE = new RegExp(`^${PHASE_CONTINUATION_SEGMENT_SOURCE}`);
|
|
66
|
+
function isPhaseContinuationSegment(seg) {
|
|
67
|
+
return PHASE_CONTINUATION_SEGMENT_PREFIX_RE.test(seg);
|
|
68
|
+
}
|
|
69
|
+
// #612 (PR-1): bracket-convention token/heading sources, kept next to the M-NN
|
|
70
|
+
// PHASE_NUMBER_TOKEN_SOURCE so this owner file stays the single origin of every
|
|
71
|
+
// phase-token grammar. `src/phase-id.cts` is exempt from the #2128 drift guard
|
|
72
|
+
// (scripts/lint-phase-id-drift.cjs) by construction, and that guard fails any
|
|
73
|
+
// literal re-derivation of the token grammar elsewhere — so the downstream
|
|
74
|
+
// bracket readers (PR-2: roadmap/validate/verify) must build their regexes by
|
|
75
|
+
// interpolating these exports, never by copying the literal.
|
|
76
|
+
//
|
|
77
|
+
// The canonical numeric WIDTH of a bracket identity field, mirroring pad2()'s
|
|
78
|
+
// output: exactly 2 digits, or 3+ with no leading zero. Owned here as a SOURCE
|
|
79
|
+
// so the read side (BRACKET_PHASE_TOKEN_SOURCE, below) and the emit-side
|
|
80
|
+
// validator (CANONICAL_NUMERIC_RE, which toDir enforces) are one rule rather
|
|
81
|
+
// than two literals that agree today and drift tomorrow.
|
|
82
|
+
const BRACKET_CANONICAL_NUMERIC_SOURCE = '(?:[1-9]\\d{2,}|\\d{2})';
|
|
83
|
+
// BRACKET_PHASE_TOKEN_SOURCE differs from PHASE_NUMBER_TOKEN_SOURCE by a
|
|
84
|
+
// dot-OR-dash sub-separator: a bracket dir/heading numeric run is `MM-PP[.SS]`
|
|
85
|
+
// (a hyphen joins milestone↔phase, a dot joins phase↔sub-phase), whereas M-NN
|
|
86
|
+
// sub-phases are dot-only.
|
|
87
|
+
//
|
|
88
|
+
// The run is POSITIONAL, not a free repetition — `MM-PP[.SS][-LL]` — and each
|
|
89
|
+
// position gets the width its DELIMITER can actually afford:
|
|
90
|
+
//
|
|
91
|
+
// MM leading unbounded — delimited by the `{CODE}.` prefix
|
|
92
|
+
// -PP dash-1 canonical — the grammar REQUIRES this dash, so it is a field
|
|
93
|
+
// separator, not a continuation heuristic
|
|
94
|
+
// .SS dot canonical — a slug carries no dot (toDir sanitizes them
|
|
95
|
+
// away), so this position cannot collide
|
|
96
|
+
// -LL dash-2 #2232 cap — the ONLY slug-adjacent position, and therefore
|
|
97
|
+
// the only one a slug word can collide with
|
|
98
|
+
//
|
|
99
|
+
// #2232 reconciliation: the slug-adjacent position interpolates the single-owner
|
|
100
|
+
// PHASE_CONTINUATION_SEGMENT_SOURCE, so the #2232 bug class cannot reopen on the
|
|
101
|
+
// bracket path — dir `PROJ.01-14-2026-photos-…` (a slug leading with a year)
|
|
102
|
+
// yields `01-14`, never `01-14-2026`.
|
|
103
|
+
//
|
|
104
|
+
// DELIBERATE DIVERGENCE from the M-NN dir-token path (pinned by the parity gate
|
|
105
|
+
// in tests/continuation-grammar-parity.test.cjs, which fails if these two rules
|
|
106
|
+
// drift for a reason nobody intended): the non-slug-adjacent positions stay
|
|
107
|
+
// WIDER than #2232's cap. Bracket admits 3+-digit milestone/phase/sub-phase
|
|
108
|
+
// (CANONICAL_NUMERIC_RE — `[GSD.100] 05` is a pinned regression), and unlike the
|
|
109
|
+
// M-NN continuations those positions are delimiter-disambiguated rather than
|
|
110
|
+
// heuristically recognized, so there is no year collision to defend against.
|
|
111
|
+
// Interpolating the cap verbatim at every position would only under-collect ids
|
|
112
|
+
// that toDir itself emits: `PROJ.02-105-slug` (3-digit phase) would read as
|
|
113
|
+
// `02`, and `[GSD.02] 05.100` (3-digit sub-phase) as `05`. Upstream draws this
|
|
114
|
+
// same line for the same reason — core-utils/phase cap the paired PLAN component
|
|
115
|
+
// while the leading phase component stays unbounded (phase numbers ≥100 are
|
|
116
|
+
// legitimate). The trade-off this accepts is #2232's policy verbatim: a PLAN
|
|
117
|
+
// ≥100 is out of the token grammar.
|
|
118
|
+
//
|
|
119
|
+
// Still deliberately MORE PERMISSIVE than parsePhaseId's strict grammar (it
|
|
120
|
+
// admits a letter-suffixed and unpadded leading token that the parser rejects):
|
|
121
|
+
// this is a READ-TOLERANCE source for the PR-2 readers, which must recognize a
|
|
122
|
+
// bracket-shaped token before deciding what to do with it — it is not the
|
|
123
|
+
// emit/identity grammar. parsePhaseId stays the arbiter of well-formedness.
|
|
124
|
+
const BRACKET_PHASE_TOKEN_SOURCE = `\\d+[A-Z]?` +
|
|
125
|
+
`(?:-${BRACKET_CANONICAL_NUMERIC_SOURCE}(?!\\d))?` +
|
|
126
|
+
`(?:\\.${BRACKET_CANONICAL_NUMERIC_SOURCE}(?!\\d))?` +
|
|
127
|
+
`(?:-${PHASE_CONTINUATION_SEGMENT_SOURCE})?`;
|
|
128
|
+
// A phase HEADING intro under bracket is either a `[...]` bracket (optionally
|
|
129
|
+
// followed by a `Phase ` label) or a bare `Phase ` label; a bare number is NOT
|
|
130
|
+
// a phase-heading intro. The `[^\]]{1,200}` bound mirrors the existing
|
|
131
|
+
// roadmap-parser heading regexes (ReDoS-safe: a header is one short line).
|
|
132
|
+
const PHASE_HEADING_PREFIX_SRC = '(?:\\[[^\\]]{1,200}\\]\\s*(?:Phase\\s+)?|Phase\\s+)';
|
|
51
133
|
function stripProjectCodePrefix(value, caseInsensitive = true) {
|
|
52
134
|
const input = String(value);
|
|
53
135
|
const re = caseInsensitive ? PROJECT_CODE_PREFIX_STRIP_RE_I : PROJECT_CODE_PREFIX_STRIP_RE;
|
|
@@ -80,7 +162,23 @@ function normalizePhaseName(phase) {
|
|
|
80
162
|
// Custom phase IDs (e.g. PROJ-42, AUTH-101): return as-is
|
|
81
163
|
return str;
|
|
82
164
|
}
|
|
83
|
-
function getMilestoneFromPhaseId(phaseId) {
|
|
165
|
+
function getMilestoneFromPhaseId(phaseId, convention) {
|
|
166
|
+
// READING-B (#612): under the bracket convention the milestone comes from the
|
|
167
|
+
// `[PROJECT.MM]` / `{CODE}.{MM}-` prefix, never the phase-token leading
|
|
168
|
+
// integer (ADR-612 Decision 6). Gated on 'bracket' so the `null` and
|
|
169
|
+
// 'milestone-prefixed' (M-NN) paths keep the legacy leading-int rule
|
|
170
|
+
// (READING-A) below, byte-untouched. The optional parameter keeps this helper
|
|
171
|
+
// pure (no config read) and backward-compatible: every existing single-arg
|
|
172
|
+
// caller resolves to the unchanged READING-A body.
|
|
173
|
+
if (convention === 'bracket') {
|
|
174
|
+
const b = String(phaseId).match(/^([A-Z][A-Z0-9_]*)\.(\d+)/);
|
|
175
|
+
if (!b)
|
|
176
|
+
return null;
|
|
177
|
+
const mm = parseInt(b[2], 10);
|
|
178
|
+
if (SENTINEL_RANGES.includes(mm))
|
|
179
|
+
return null; // sentinel milestones have no real milestone
|
|
180
|
+
return `v${mm}.0`;
|
|
181
|
+
}
|
|
84
182
|
const stripped = stripProjectCodePrefix(phaseId);
|
|
85
183
|
const m = stripped.match(/^0*(\d+)-\d/);
|
|
86
184
|
if (!m)
|
|
@@ -105,6 +203,145 @@ function getPhaseDirFromPhaseId(phaseId, phaseName, projectCode) {
|
|
|
105
203
|
const base = parts.join('-');
|
|
106
204
|
return projectCode ? `${projectCode}-${base}` : base;
|
|
107
205
|
}
|
|
206
|
+
const pad2 = (n) => String(parseInt(n, 10)).padStart(2, '0');
|
|
207
|
+
function parsePhaseId(input) {
|
|
208
|
+
// No .trim(): the match anchors (`^`...`$`) then reject leading/trailing
|
|
209
|
+
// whitespace outright, folding that case into the same "not a bracket
|
|
210
|
+
// phase id" rejection below rather than needing its own check.
|
|
211
|
+
const str = String(input);
|
|
212
|
+
// Display form: [PROJECT.MM] PP[.SS][-LL]. The match itself stays
|
|
213
|
+
// permissive on purpose (it will happily match an unpadded number or a
|
|
214
|
+
// multi-space run) — canonicality is enforced UNIFORMLY below via the
|
|
215
|
+
// render round-trip (ADR-612 Decision 4) rather than by hand-tuning every
|
|
216
|
+
// numeric / whitespace sub-pattern, so a field added later inherits the
|
|
217
|
+
// check for free instead of needing its own regex micro-surgery.
|
|
218
|
+
const disp = str.match(/^\[([A-Z][A-Z0-9_]*)\.(\d+)\]\s+(\d+)(?:\.(\d+))?(?:-(\d+))?$/);
|
|
219
|
+
if (disp) {
|
|
220
|
+
const id = { project: disp[1], milestone: pad2(disp[2]), phase: pad2(disp[3]) };
|
|
221
|
+
if (disp[4] !== undefined)
|
|
222
|
+
id.subphase = pad2(disp[4]);
|
|
223
|
+
if (disp[5] !== undefined)
|
|
224
|
+
id.plan = pad2(disp[5]);
|
|
225
|
+
// Canonicality by construction: re-render the parsed id and require
|
|
226
|
+
// byte-equality with the input. This rejects unpadded ('[GSD.5] 5'),
|
|
227
|
+
// over-padded ('[GSD.005] 05'), and multi-space-separated ('[GSD.02] 05')
|
|
228
|
+
// variants uniformly, without special-casing any one of them — the emit
|
|
229
|
+
// path (renderPhaseId) is the single source of truth for "canonical".
|
|
230
|
+
if (renderPhaseId(id) !== str) {
|
|
231
|
+
throw new Error(`parsePhaseId: not canonical: ${JSON.stringify(input)}`);
|
|
232
|
+
}
|
|
233
|
+
return id;
|
|
234
|
+
}
|
|
235
|
+
// Dir / token form: {PROJECT}.{MM}-{PP}[.{SS}][-{plan|slug}]
|
|
236
|
+
const dir = str.match(/^([A-Z][A-Z0-9_]*)\.(\d+)-(\d+)(?:\.(\d+))?(?:-(.+))?$/);
|
|
237
|
+
if (dir) {
|
|
238
|
+
const id = { project: dir[1], milestone: pad2(dir[2]), phase: pad2(dir[3]) };
|
|
239
|
+
if (dir[4] !== undefined)
|
|
240
|
+
id.subphase = pad2(dir[4]);
|
|
241
|
+
// Trailing segment: a pure-integer tail is the plan; anything else is a
|
|
242
|
+
// slug (dropped from the tuple — it is not an identity dimension). The
|
|
243
|
+
// plan tail participates in the canonicality check below; the slug tail
|
|
244
|
+
// is read-tolerant pass-through (a slug is not an identity dimension) and
|
|
245
|
+
// is exempt from it.
|
|
246
|
+
const tail = dir[5];
|
|
247
|
+
const tailIsPlan = tail !== undefined && /^\d+$/.test(tail);
|
|
248
|
+
if (tailIsPlan)
|
|
249
|
+
id.plan = pad2(tail);
|
|
250
|
+
// Canonicality by construction, mirroring the display branch: rebuild the
|
|
251
|
+
// exact dir/token string this id would emit and require it match the
|
|
252
|
+
// input verbatim. Rejects unpadded milestone/phase ('GSD.2-5') and
|
|
253
|
+
// unpadded plan tails ('GSD.02-05-1') without special-casing either.
|
|
254
|
+
const sub = id.subphase ? `.${id.subphase}` : '';
|
|
255
|
+
const tailOut = tail === undefined ? '' : tailIsPlan ? `-${pad2(tail)}` : `-${tail}`;
|
|
256
|
+
const canonical = `${id.project}.${id.milestone}-${id.phase}${sub}${tailOut}`;
|
|
257
|
+
if (canonical !== str) {
|
|
258
|
+
throw new Error(`parsePhaseId: not canonical: ${JSON.stringify(input)}`);
|
|
259
|
+
}
|
|
260
|
+
return id;
|
|
261
|
+
}
|
|
262
|
+
// Ambiguous / bare tokens (e.g. `02-04`, `05`, `2-01`) match neither branch,
|
|
263
|
+
// as does a display/dir form carrying leading/trailing whitespace (the
|
|
264
|
+
// anchors never match it): reject rather than guess a tuple (ADR-612
|
|
265
|
+
// conservative default). The rejection lives ONLY in this new parser —
|
|
266
|
+
// normalizePhaseName and every other legacy reader keep accepting those
|
|
267
|
+
// tokens unchanged.
|
|
268
|
+
throw new Error(`parsePhaseId: not a bracket phase id: ${JSON.stringify(input)}`);
|
|
269
|
+
}
|
|
270
|
+
function renderPhaseId(id) {
|
|
271
|
+
const sub = id.subphase ? `.${id.subphase}` : '';
|
|
272
|
+
const plan = id.plan ? `-${id.plan}` : '';
|
|
273
|
+
return `[${id.project}.${id.milestone}] ${id.phase}${sub}${plan}`;
|
|
274
|
+
}
|
|
275
|
+
// PhaseId is a structural type: nothing forces a caller through parsePhaseId,
|
|
276
|
+
// so toDir cannot trust project/milestone/phase/subphase are already
|
|
277
|
+
// canonical — each is validated below against the exact shape parsePhaseId
|
|
278
|
+
// itself would ever produce, closing off a hand-built id as a path-traversal
|
|
279
|
+
// vector. PROJECT_ID_RE mirrors the parser's `[A-Z][A-Z0-9_]*` grammar;
|
|
280
|
+
// CANONICAL_NUMERIC_RE mirrors pad2()'s output shape — exactly 2 digits, or
|
|
281
|
+
// 3+ digits with no leading zero. It is BUILT from
|
|
282
|
+
// BRACKET_CANONICAL_NUMERIC_SOURCE rather than re-spelled as a literal, so this
|
|
283
|
+
// emit-side gate and the read-side token source cannot disagree about what
|
|
284
|
+
// "canonical width" means (the anchors here make the source's trailing `(?!\d)`
|
|
285
|
+
// guard, which the unanchored read side needs, redundant).
|
|
286
|
+
const PROJECT_ID_RE = /^[A-Z][A-Z0-9_]*$/;
|
|
287
|
+
const CANONICAL_NUMERIC_RE = new RegExp(`^${BRACKET_CANONICAL_NUMERIC_SOURCE}$`);
|
|
288
|
+
function toDir(id, slug) {
|
|
289
|
+
if (!PROJECT_ID_RE.test(id.project)) {
|
|
290
|
+
throw new Error(`toDir: invalid project: ${JSON.stringify(id.project)}`);
|
|
291
|
+
}
|
|
292
|
+
if (!CANONICAL_NUMERIC_RE.test(id.milestone)) {
|
|
293
|
+
throw new Error(`toDir: invalid milestone: ${JSON.stringify(id.milestone)}`);
|
|
294
|
+
}
|
|
295
|
+
if (!CANONICAL_NUMERIC_RE.test(id.phase)) {
|
|
296
|
+
throw new Error(`toDir: invalid phase: ${JSON.stringify(id.phase)}`);
|
|
297
|
+
}
|
|
298
|
+
if (id.subphase !== undefined && !CANONICAL_NUMERIC_RE.test(id.subphase)) {
|
|
299
|
+
throw new Error(`toDir: invalid subphase: ${JSON.stringify(id.subphase)}`);
|
|
300
|
+
}
|
|
301
|
+
// A non-string slug (e.g. an omitted second argument) must not be silently
|
|
302
|
+
// coerced by String(...) into the literal token 'undefined'/'null' on disk.
|
|
303
|
+
if (typeof slug !== 'string') {
|
|
304
|
+
throw new Error(`toDir: slug must be a string: ${JSON.stringify(slug)}`);
|
|
305
|
+
}
|
|
306
|
+
const sub = id.subphase ? `.${id.subphase}` : '';
|
|
307
|
+
// Slug guard: the slug becomes an on-disk path segment, so collapse it to a
|
|
308
|
+
// safe lowercase token — never a path separator or `..` traversal.
|
|
309
|
+
const safeSlug = slug.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
310
|
+
// A slug that sanitizes to nothing (e.g. '!!!') would otherwise emit a
|
|
311
|
+
// dangling trailing hyphen.
|
|
312
|
+
if (!safeSlug) {
|
|
313
|
+
throw new Error(`toDir: slug sanitizes to empty: ${JSON.stringify(slug)}`);
|
|
314
|
+
}
|
|
315
|
+
// An all-digit slug (e.g. '2026') is string-indistinguishable from the
|
|
316
|
+
// parsePhaseId dir branch's plan tail, so it would re-parse as a plan, not
|
|
317
|
+
// a slug — silently breaking the disk↔identity bijection on read-back.
|
|
318
|
+
if (/^\d+$/.test(safeSlug)) {
|
|
319
|
+
throw new Error(`toDir: slug must not be all-digit: ${JSON.stringify(slug)}`);
|
|
320
|
+
}
|
|
321
|
+
return `${id.project}.${id.milestone}-${id.phase}${sub}-${safeSlug}`;
|
|
322
|
+
}
|
|
323
|
+
// Milestone integers reserved as non-milestone sentinels (0.x backlog / 999.x
|
|
324
|
+
// icebox); a phase id in these ranges has no real milestone.
|
|
325
|
+
const SENTINEL_RANGES = Object.freeze([0, 999]);
|
|
326
|
+
function isSentinelPhaseId(phaseId, convention) {
|
|
327
|
+
const s = String(phaseId);
|
|
328
|
+
// Bracket milestone lives in the `{CODE}.{MM}` prefix. GATED on
|
|
329
|
+
// convention === 'bracket' for the same reason as extractPhaseToken below and
|
|
330
|
+
// getMilestoneFromPhaseId above: that prefix is string-indistinguishable from
|
|
331
|
+
// the legacy #1324 letter-prefixed-decimal family (`P0.0-foundation` is a real
|
|
332
|
+
// phase, NOT sentinel milestone 0) whenever the code ends in a digit. A
|
|
333
|
+
// convention-less caller uses the legacy/bare leading-int rule below, so no
|
|
334
|
+
// existing reader gains a false positive; the bracket reading is opt-in.
|
|
335
|
+
if (convention === 'bracket') {
|
|
336
|
+
const bracket = s.match(/^[A-Z][A-Z0-9_]*\.(\d+)/); // bracket: milestone in the prefix
|
|
337
|
+
if (bracket)
|
|
338
|
+
return SENTINEL_RANGES.includes(parseInt(bracket[1], 10));
|
|
339
|
+
}
|
|
340
|
+
const legacy = stripProjectCodePrefix(s).match(/^0*(\d+)/); // legacy/bare: leading int
|
|
341
|
+
if (!legacy)
|
|
342
|
+
return false;
|
|
343
|
+
return SENTINEL_RANGES.includes(parseInt(legacy[1], 10));
|
|
344
|
+
}
|
|
108
345
|
/**
|
|
109
346
|
* Render a regex source fragment matching a phase number against ROADMAP/STATE
|
|
110
347
|
* prose regardless of zero-padding on either side.
|
|
@@ -201,7 +438,25 @@ function comparePhaseNum(a, b) {
|
|
|
201
438
|
/**
|
|
202
439
|
* Extract the phase token from a directory name.
|
|
203
440
|
*/
|
|
204
|
-
function extractPhaseToken(dirName) {
|
|
441
|
+
function extractPhaseToken(dirName, convention) {
|
|
442
|
+
// #612 bracket dir form `{CODE}.{MM}-{PP}[.{SS}]-slug` → phase token `PP[.SS]`.
|
|
443
|
+
// GATED on convention === 'bracket' (mirrors getMilestoneFromPhaseId's READING-B
|
|
444
|
+
// decision above). A bracket dir `{CODE}.{MM}-{PP}` is string-INDISTINGUISHABLE
|
|
445
|
+
// from the legacy #2043/#1324 letter-prefixed-decimal family (`P0.3-2`,
|
|
446
|
+
// `P0.12-34`) whenever the project code ends in a digit, so NO string-only
|
|
447
|
+
// discriminator can separate the two conventions — auto-detecting here silently
|
|
448
|
+
// reinterpreted `P0.3-2` → `2` (was `P0.3-2`), a byte-identical-read regression
|
|
449
|
+
// on this CRITICAL 6-caller helper (ADR-2121). Requiring an explicit convention
|
|
450
|
+
// signal keeps every existing (convention-less) call site byte-identical to
|
|
451
|
+
// prior behaviour — see the #2043 numeric-tail characterization in
|
|
452
|
+
// tests/phase-id.test.cjs — while keeping the helper pure (optional param, no
|
|
453
|
+
// config read). The captured token is dot-only (`PP[.SS]`); the milestone↔phase
|
|
454
|
+
// hyphen and any trailing plan/slug are excluded.
|
|
455
|
+
if (convention === 'bracket') {
|
|
456
|
+
const bracketDir = dirName.match(/^[A-Z][A-Z0-9_]*\.\d+-(\d+(?:\.\d+)?)/);
|
|
457
|
+
if (bracketDir)
|
|
458
|
+
return bracketDir[1];
|
|
459
|
+
}
|
|
205
460
|
const codePrefixMatch = dirName.match(PROJECT_CODE_PREFIX_CAPTURE_RE_I);
|
|
206
461
|
let prefix = '';
|
|
207
462
|
let rest = dirName;
|
|
@@ -211,9 +466,11 @@ function extractPhaseToken(dirName) {
|
|
|
211
466
|
}
|
|
212
467
|
const segments = rest.split('-');
|
|
213
468
|
const tokenSegments = [];
|
|
214
|
-
// #2043: distinguish a real (zero-padded
|
|
215
|
-
//
|
|
216
|
-
// continues with
|
|
469
|
+
// #2043: distinguish a real (zero-padded) phase/sub-phase segment from a
|
|
470
|
+
// single-digit slug word. A pure-numeric leading segment ("46") only
|
|
471
|
+
// continues with exactly-2-digit segments (#2232: a ≥3-digit run is a slug
|
|
472
|
+
// word such as a year — "14-2026-photos-…" yields "14", not "14-2026"), so
|
|
473
|
+
// "46-6-rs-…" yields "46" (the "6" is the
|
|
217
474
|
// slug's first word), not "46-6". Milestone-prefixed ids like "M1-2" reach here
|
|
218
475
|
// with "M1-" already stripped as a project-code prefix (see
|
|
219
476
|
// PROJECT_CODE_PREFIX_CAPTURE_RE_I), so "2" is the leading segment and the same
|
|
@@ -236,7 +493,7 @@ function extractPhaseToken(dirName) {
|
|
|
236
493
|
break;
|
|
237
494
|
}
|
|
238
495
|
}
|
|
239
|
-
else if (
|
|
496
|
+
else if (isPhaseContinuationSegment(seg) || (firstLetterPrefixed && /^\d/.test(seg))) {
|
|
240
497
|
tokenSegments.push(seg);
|
|
241
498
|
}
|
|
242
499
|
else {
|
|
@@ -290,9 +547,38 @@ function parsePhaseFromProse(value) {
|
|
|
290
547
|
// cannot drive O(n^2) regex backtracking (CPU-exhaustion DoS). A real phase
|
|
291
548
|
// name is far shorter than the cap.
|
|
292
549
|
const parenName = str.match(/\(([^)]{1,200})\)/);
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
550
|
+
// #2736 (the #1695 AC #3 residual): status-keyword-aware precedence. The
|
|
551
|
+
// first-party writer shapes are `N — Name (aside)` (completePhaseCore),
|
|
552
|
+
// `N (Name) — EXECUTING` (beginPhaseCore), `N — COMPLETE`, and the
|
|
553
|
+
// gsd2-import `N (slug) — Milestone: Title`. A blind paren-first read
|
|
554
|
+
// harvests the aside as the name on the first shape; a blind dash-first
|
|
555
|
+
// read harvests the status keyword on the others. Prefer the em-dash name
|
|
556
|
+
// when it is a genuine name, else fall back to the parenthetical. Still
|
|
557
|
+
// lossy for names that themselves contain a parenthetical — transitions
|
|
558
|
+
// that hold the exact name bypass this parser entirely via the
|
|
559
|
+
// syncStateFrontmatter authoritative override.
|
|
560
|
+
//
|
|
561
|
+
// The em-dash separator is searched on a paren-stripped copy, so an em-dash
|
|
562
|
+
// INSIDE a parenthetical name (`16 (Native — Global Hotkey) — EXECUTING`)
|
|
563
|
+
// can never be mistaken for the name separator.
|
|
564
|
+
const strNoParens = str.replace(/\([^)\n]{0,200}\)/g, ' ');
|
|
565
|
+
const dashName = strNoParens.match(/—\s*([^(\n]{1,200}?)\s*$/);
|
|
566
|
+
// The precedence-decision vocabulary is deliberately broader than the final
|
|
567
|
+
// name-nulling filter below: a dash tail that merely LOOKS like a status
|
|
568
|
+
// annotation should lose to a parenthetical name, without changing which
|
|
569
|
+
// extracted names are nulled (that set stays the long-standing three).
|
|
570
|
+
const STATUS_WORD_RE = /^(?:complete|executing|not started)$/i;
|
|
571
|
+
const STATUSY_TAIL_RE = /^(?:completed?|executing|not started|planning|planned|ready(?:\s+to\s+\S.{0,50})?|done|in progress|blocked|paused|verifying)$/i;
|
|
572
|
+
const dashRaw = dashName?.[1]?.trim() ?? null;
|
|
573
|
+
const dashIsName = dashRaw !== null && dashRaw.length > 0
|
|
574
|
+
&& !STATUSY_TAIL_RE.test(dashRaw)
|
|
575
|
+
&& !/^milestone\s*:/i.test(dashRaw)
|
|
576
|
+
// A lone ALL-CAPS token after the dash reads as a status marker whenever a
|
|
577
|
+
// parenthetical name exists to prefer (the beginPhase writer's systematic
|
|
578
|
+
// `(Name) — STATUS` shape); with no parenthetical it stays the best guess.
|
|
579
|
+
&& !(parenName && /^[A-Z][A-Z0-9_-]*$/.test(dashRaw));
|
|
580
|
+
const rawName = dashIsName ? dashRaw : (parenName?.[1] ?? dashRaw ?? null);
|
|
581
|
+
const name = rawName && !STATUS_WORD_RE.test(rawName.trim())
|
|
296
582
|
? rawName.trim()
|
|
297
583
|
: null;
|
|
298
584
|
return {
|
|
@@ -358,10 +644,19 @@ module.exports = {
|
|
|
358
644
|
OPTIONAL_PROJECT_CODE_PREFIX_SOURCE,
|
|
359
645
|
OPTIONAL_PHASE_TAG_SOURCE,
|
|
360
646
|
PHASE_NUMBER_TOKEN_SOURCE,
|
|
647
|
+
PHASE_CONTINUATION_SEGMENT_SOURCE,
|
|
648
|
+
isPhaseContinuationSegment,
|
|
649
|
+
BRACKET_PHASE_TOKEN_SOURCE,
|
|
650
|
+
PHASE_HEADING_PREFIX_SRC,
|
|
361
651
|
stripProjectCodePrefix,
|
|
362
652
|
normalizePhaseName,
|
|
363
653
|
getMilestoneFromPhaseId,
|
|
364
654
|
getPhaseDirFromPhaseId,
|
|
655
|
+
parsePhaseId,
|
|
656
|
+
renderPhaseId,
|
|
657
|
+
toDir,
|
|
658
|
+
SENTINEL_RANGES,
|
|
659
|
+
isSentinelPhaseId,
|
|
365
660
|
phaseMarkdownRegexSource,
|
|
366
661
|
phaseMarkdownRegexSourceExact,
|
|
367
662
|
comparePhaseNum,
|