@opengsd/gsd-core 1.4.4 → 1.5.0-rc.2
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/plugin.json +1 -1
- package/README.md +3 -3
- package/agents/gsd-code-fixer.md +3 -2
- package/agents/gsd-debug-session-manager.md +2 -1
- package/agents/gsd-debugger.md +4 -3
- package/agents/gsd-executor.md +17 -16
- package/agents/gsd-intel-updater.md +38 -41
- package/agents/gsd-phase-researcher.md +8 -8
- package/agents/gsd-plan-checker.md +23 -13
- package/agents/gsd-planner.md +32 -188
- package/agents/gsd-project-researcher.md +5 -4
- package/agents/gsd-research-synthesizer.md +2 -1
- package/agents/gsd-ui-researcher.md +2 -1
- package/agents/gsd-verifier.md +12 -11
- package/bin/install.js +965 -1486
- package/commands/gsd/autonomous.md +5 -1
- package/commands/gsd/ns-manage.md +8 -1
- package/commands/gsd/ns-project.md +5 -0
- package/commands/gsd/ns-review.md +4 -1
- package/commands/gsd/ns-workflow.md +7 -1
- package/commands/gsd/plan-review-convergence.md +5 -4
- package/commands/gsd/surface.md +12 -5
- package/gemini-extension.json +1 -1
- package/gsd-core/bin/gsd-tools.cjs +198 -101
- package/gsd-core/bin/gsd_run +20 -0
- package/gsd-core/bin/lib/audit-command-router.cjs +61 -0
- package/gsd-core/bin/lib/capability-registry.cjs +2234 -0
- package/gsd-core/bin/lib/capability-state.cjs +336 -0
- package/gsd-core/bin/lib/check-command-router.cjs +133 -2
- package/gsd-core/bin/lib/cli-exit.cjs +22 -3
- package/gsd-core/bin/lib/config-loader.cjs +716 -0
- package/gsd-core/bin/lib/configuration.cjs +4 -34
- package/gsd-core/bin/lib/core-utils.cjs +198 -0
- package/gsd-core/bin/lib/core.cjs +107 -1817
- package/gsd-core/bin/lib/edge-probe.cjs +173 -0
- package/gsd-core/bin/lib/fallow-runner.cjs +63 -25
- package/gsd-core/bin/lib/federated-config.cjs +182 -0
- package/gsd-core/bin/lib/graphify-command-router.cjs +74 -0
- package/gsd-core/bin/lib/init.cjs +58 -12
- package/gsd-core/bin/lib/install-profiles.cjs +157 -3
- package/gsd-core/bin/lib/intel-command-router.cjs +116 -0
- package/gsd-core/bin/lib/intel.cjs +3 -3
- package/gsd-core/bin/lib/io.cjs +222 -0
- package/gsd-core/bin/lib/loop-host-contract.cjs +105 -0
- package/gsd-core/bin/lib/loop-resolver.cjs +460 -0
- package/gsd-core/bin/lib/model-resolver.cjs +426 -0
- package/gsd-core/bin/lib/phase-command-router.cjs +20 -0
- package/gsd-core/bin/lib/phase-id.cjs +215 -0
- package/gsd-core/bin/lib/phase-locator.cjs +148 -0
- package/gsd-core/bin/lib/phase.cjs +17 -0
- package/gsd-core/bin/lib/probe-core.cjs +257 -0
- package/gsd-core/bin/lib/profile-pipeline.cjs +2 -2
- package/gsd-core/bin/lib/roadmap-parser.cjs +443 -0
- package/gsd-core/bin/lib/roadmap.cjs +44 -1
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +92 -95
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +68 -29
- package/gsd-core/bin/lib/runtime-homes.cjs +163 -87
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +1439 -0
- package/gsd-core/bin/lib/runtime-name-policy.cjs +2 -1
- package/gsd-core/bin/lib/runtime-slash.cjs +7 -2
- package/gsd-core/bin/lib/shell-command-projection.cjs +13 -0
- package/gsd-core/bin/lib/state-document.cjs +8 -0
- package/gsd-core/bin/lib/state.cjs +114 -2
- package/gsd-core/bin/lib/surface.cjs +66 -14
- package/gsd-core/bin/lib/uat-predicate.cjs +329 -0
- package/gsd-core/bin/lib/update-context.cjs +4 -1
- package/gsd-core/bin/lib/verify.cjs +104 -1
- package/gsd-core/bin/lib/worktree-base-ref.cjs +33 -8
- package/gsd-core/bin/shared/model-catalog.json +11 -6
- package/gsd-core/bin/shared/runtime-aliases.manifest.json +5 -1
- package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +7 -0
- package/gsd-core/references/edge-probe-fixtures/01-round-half-even/requirements.json +1 -0
- package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +8 -0
- package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/requirements.json +1 -0
- package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +7 -0
- package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/requirements.json +1 -0
- package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +7 -0
- package/gsd-core/references/edge-probe-fixtures/04-money-rounding/requirements.json +1 -0
- package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +8 -0
- package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/requirements.json +1 -0
- package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +8 -0
- package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/requirements.json +1 -0
- package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/resolutions.json +4 -0
- package/gsd-core/references/edge-probe.md +261 -0
- package/gsd-core/references/planner-antipatterns.md +41 -0
- package/gsd-core/references/planner-guidance.md +186 -0
- package/gsd-core/references/planner-reviews.md +5 -2
- package/gsd-core/templates/phase-prompt.md +7 -7
- package/gsd-core/templates/project.md +19 -2
- package/gsd-core/templates/spec.md +12 -0
- package/gsd-core/templates/summary-complex.md +1 -0
- package/gsd-core/templates/summary-minimal.md +1 -0
- package/gsd-core/templates/summary-standard.md +1 -0
- package/gsd-core/templates/summary.md +1 -0
- package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/gsd-core/workflows/add-backlog.md +1 -1
- package/gsd-core/workflows/add-phase.md +1 -1
- package/gsd-core/workflows/add-tests.md +1 -1
- package/gsd-core/workflows/add-todo.md +1 -1
- package/gsd-core/workflows/ai-integration-phase.md +1 -1
- package/gsd-core/workflows/audit-fix.md +1 -1
- package/gsd-core/workflows/audit-milestone.md +1 -1
- package/gsd-core/workflows/audit-uat.md +1 -1
- package/gsd-core/workflows/autonomous.md +111 -51
- package/gsd-core/workflows/check-todos.md +1 -1
- package/gsd-core/workflows/cleanup.md +1 -1
- package/gsd-core/workflows/code-review-fix.md +6 -4
- package/gsd-core/workflows/code-review.md +53 -17
- package/gsd-core/workflows/complete-milestone.md +11 -5
- package/gsd-core/workflows/debug.md +1 -1
- package/gsd-core/workflows/diagnose-issues.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/auto.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
- package/gsd-core/workflows/discuss-phase.md +8 -1
- package/gsd-core/workflows/do.md +1 -1
- package/gsd-core/workflows/docs-update.md +1 -1
- package/gsd-core/workflows/edit-phase.md +1 -1
- package/gsd-core/workflows/eval-review.md +4 -1
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +1 -1
- package/gsd-core/workflows/execute-phase.md +8 -1
- package/gsd-core/workflows/execute-plan.md +1 -1
- package/gsd-core/workflows/explore.md +1 -1
- package/gsd-core/workflows/extract-learnings.md +1 -1
- package/gsd-core/workflows/forensics.md +1 -1
- package/gsd-core/workflows/graduation.md +1 -1
- package/gsd-core/workflows/health.md +1 -1
- package/gsd-core/workflows/help/modes/full.md +1 -1
- package/gsd-core/workflows/import.md +1 -1
- package/gsd-core/workflows/ingest-docs.md +1 -1
- package/gsd-core/workflows/insert-phase.md +1 -1
- package/gsd-core/workflows/list-workspaces.md +1 -1
- package/gsd-core/workflows/manager.md +1 -1
- package/gsd-core/workflows/map-codebase.md +1 -1
- package/gsd-core/workflows/milestone-summary.md +1 -1
- package/gsd-core/workflows/mvp-phase.md +1 -1
- package/gsd-core/workflows/new-milestone.md +9 -1
- package/gsd-core/workflows/new-project.md +9 -1
- package/gsd-core/workflows/new-workspace.md +1 -1
- package/gsd-core/workflows/next.md +1 -1
- package/gsd-core/workflows/pause-work.md +1 -1
- package/gsd-core/workflows/plan-milestone-gaps.md +1 -1
- package/gsd-core/workflows/plan-phase.md +65 -28
- package/gsd-core/workflows/plan-review-convergence.md +60 -33
- package/gsd-core/workflows/plant-seed.md +1 -1
- package/gsd-core/workflows/profile-user.md +1 -1
- package/gsd-core/workflows/progress.md +1 -1
- package/gsd-core/workflows/quick.md +2 -2
- package/gsd-core/workflows/remove-phase.md +1 -1
- package/gsd-core/workflows/remove-workspace.md +1 -1
- package/gsd-core/workflows/resume-project.md +1 -1
- package/gsd-core/workflows/review.md +1 -1
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/settings-advanced.md +7 -7
- package/gsd-core/workflows/settings-integrations.md +1 -1
- package/gsd-core/workflows/settings.md +2 -2
- package/gsd-core/workflows/ship.md +8 -1
- package/gsd-core/workflows/sketch-wrap-up.md +1 -1
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/spec-phase.md +130 -1
- package/gsd-core/workflows/spike-wrap-up.md +1 -1
- package/gsd-core/workflows/spike.md +1 -1
- package/gsd-core/workflows/stats.md +1 -1
- package/gsd-core/workflows/thread.md +1 -1
- package/gsd-core/workflows/transition.md +1 -1
- package/gsd-core/workflows/ui-phase.md +1 -1
- package/gsd-core/workflows/ui-review.md +1 -1
- package/gsd-core/workflows/ultraplan-phase.md +1 -1
- package/gsd-core/workflows/update.md +2 -2
- package/gsd-core/workflows/validate-phase.md +1 -1
- package/gsd-core/workflows/verify-phase.md +1 -1
- package/gsd-core/workflows/verify-work.md +8 -1
- package/package.json +11 -3
- package/scripts/base64-scan.sh +1 -1
- package/scripts/changeset/cli.cjs +8 -1
- package/scripts/changeset/lint.cjs +38 -2
- package/scripts/ci-test-scope.cjs +21 -10
- package/scripts/gen-capability-registry.cjs +2293 -0
- package/scripts/gen-loop-host-contract.cjs +471 -0
- package/scripts/lib/allowlist-ratchet.cjs +101 -1
- package/scripts/lint-regression-test-names.allowlist.json +269 -0
- package/scripts/lint-regression-test-names.cjs +117 -0
- package/scripts/lint-test-file-count.allowlist.json +25 -4
- package/scripts/lint-windows-test-portability.cjs +178 -0
- package/scripts/prompt-injection-scan.sh +4 -4
- package/scripts/research-profiles.cjs +10 -10
- package/scripts/run-tests.cjs +133 -29
- package/scripts/secret-scan.sh +3 -3
- package/scripts/sync-next-version.cjs +133 -0
- package/scripts/sync-runtime-launcher.cjs +21 -5
- package/scripts/update-size-baseline.cjs +68 -0
- package/scripts/workflow-policy.cjs +42 -9
- package/scripts/workflow-size.cjs +90 -0
- package/scripts/run-cross-platform-tests.cjs +0 -67
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Model Resolver — Model and effort resolution policy
|
|
4
|
+
*
|
|
5
|
+
* ADR-857 rollout phase 2f: extracted from core.cts (issue #888).
|
|
6
|
+
* Owns model and effort resolution policy: resolves the model, runtime tier,
|
|
7
|
+
* planning granularity, reasoning effort, and fast-mode for a given agent by
|
|
8
|
+
* reading project config and resolving against the model profiles and catalog.
|
|
9
|
+
* Behaviour is preserved byte-for-behaviour from the prior location; only
|
|
10
|
+
* the module boundary moved. core.cjs re-exports the resolvers for back-compat.
|
|
11
|
+
*
|
|
12
|
+
* New imports should pull resolvers from model-resolver.cjs directly.
|
|
13
|
+
*
|
|
14
|
+
* Dependencies (leaf modules only — no core.cjs):
|
|
15
|
+
* - node:fs / node:path (stdlib, not currently needed — included for future use)
|
|
16
|
+
* - ./config-loader.cjs (loadConfig)
|
|
17
|
+
* - ./configuration.cjs (CONFIG_DEFAULTS as CANONICAL_CONFIG_DEFAULTS)
|
|
18
|
+
* - ./model-profiles.cjs (MODEL_PROFILES, AGENT_TO_PHASE_TYPE, AGENT_DEFAULT_TIERS, VALID_AGENT_TIERS, nextTier)
|
|
19
|
+
* - ./model-catalog.cjs (MODEL_ALIAS_MAP, RUNTIME_PROFILE_MAP, PROVIDER_PRESETS)
|
|
20
|
+
*/
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
22
|
+
const configLoaderModule = require("./config-loader.cjs");
|
|
23
|
+
const { loadConfig } = configLoaderModule;
|
|
24
|
+
// ─── Configuration Module (for CANONICAL_CONFIG_DEFAULTS used by effort/fast_mode resolvers) ─
|
|
25
|
+
const configuration_cjs_1 = require("./configuration.cjs");
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
27
|
+
const modelProfiles = require("./model-profiles.cjs");
|
|
28
|
+
const { MODEL_PROFILES, AGENT_TO_PHASE_TYPE, AGENT_DEFAULT_TIERS, VALID_AGENT_TIERS, nextTier } = modelProfiles;
|
|
29
|
+
const model_catalog_cjs_1 = require("./model-catalog.cjs");
|
|
30
|
+
/**
|
|
31
|
+
* #2517 — Resolve the runtime-aware tier entry for (runtime, tier).
|
|
32
|
+
*/
|
|
33
|
+
function resolveTierEntry({ runtime, tier, overrides }) {
|
|
34
|
+
if (!runtime || !tier)
|
|
35
|
+
return null;
|
|
36
|
+
const runtimeMap = model_catalog_cjs_1.RUNTIME_PROFILE_MAP;
|
|
37
|
+
const builtin = runtimeMap[runtime]?.[tier] || null;
|
|
38
|
+
const overridesMap = overrides;
|
|
39
|
+
const userRaw = overridesMap?.[runtime]?.[tier];
|
|
40
|
+
let userEntry = null;
|
|
41
|
+
if (userRaw) {
|
|
42
|
+
userEntry = typeof userRaw === 'string' ? { model: userRaw } : userRaw;
|
|
43
|
+
}
|
|
44
|
+
if (!builtin && !userEntry)
|
|
45
|
+
return null;
|
|
46
|
+
return { ...(builtin || {}), ...(userEntry || {}) };
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Convenience wrapper used by resolveModelInternal.
|
|
50
|
+
*/
|
|
51
|
+
function _resolveRuntimeTier(config, tier) {
|
|
52
|
+
return resolveTierEntry({
|
|
53
|
+
runtime: config['runtime'],
|
|
54
|
+
tier,
|
|
55
|
+
overrides: config['model_profile_overrides'],
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* #49 — Provider-neutral model policy preset resolution.
|
|
60
|
+
*/
|
|
61
|
+
function resolveModelPolicy(policy, tier) {
|
|
62
|
+
if (!policy || typeof policy !== 'object')
|
|
63
|
+
return null;
|
|
64
|
+
if (!tier)
|
|
65
|
+
return null;
|
|
66
|
+
const runtime = policy['runtime'];
|
|
67
|
+
const rtOverrides = policy['runtime_tiers'];
|
|
68
|
+
if (runtime && typeof runtime === 'string' && rtOverrides && typeof rtOverrides === 'object') {
|
|
69
|
+
const rtOverridesMap = rtOverrides;
|
|
70
|
+
if (Object.hasOwn(rtOverridesMap, runtime)) {
|
|
71
|
+
const runtimeEntry = rtOverridesMap[runtime];
|
|
72
|
+
if (runtimeEntry && typeof runtimeEntry === 'object' && Object.hasOwn(runtimeEntry, tier)) {
|
|
73
|
+
const raw = runtimeEntry[tier];
|
|
74
|
+
if (raw != null) {
|
|
75
|
+
const entry = typeof raw === 'string' ? { model: raw } : raw;
|
|
76
|
+
if (entry && entry['model'])
|
|
77
|
+
return entry['model'];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const provider = policy['provider'];
|
|
83
|
+
if (!provider || typeof provider !== 'string')
|
|
84
|
+
return null;
|
|
85
|
+
if (provider === 'generic' || provider === 'custom') {
|
|
86
|
+
const TIER_TO_POLICY_KEY = { opus: 'high', sonnet: 'medium', haiku: 'low' };
|
|
87
|
+
const policyKey = TIER_TO_POLICY_KEY[tier];
|
|
88
|
+
if (!policyKey)
|
|
89
|
+
return null;
|
|
90
|
+
const v = policy[policyKey];
|
|
91
|
+
return (v && typeof v === 'string') ? v : null;
|
|
92
|
+
}
|
|
93
|
+
const presetsMap = model_catalog_cjs_1.PROVIDER_PRESETS;
|
|
94
|
+
if (!Object.hasOwn(presetsMap, provider))
|
|
95
|
+
return null;
|
|
96
|
+
const presetForProvider = presetsMap[provider];
|
|
97
|
+
if (!presetForProvider || typeof presetForProvider !== 'object')
|
|
98
|
+
return null;
|
|
99
|
+
if (!Object.hasOwn(presetForProvider, tier))
|
|
100
|
+
return null;
|
|
101
|
+
const tierPresets = presetForProvider[tier];
|
|
102
|
+
if (!tierPresets || typeof tierPresets !== 'object')
|
|
103
|
+
return null;
|
|
104
|
+
const budget = (policy['budget'] && typeof policy['budget'] === 'string') ? policy['budget'] : 'medium';
|
|
105
|
+
if (!Object.hasOwn(tierPresets, budget))
|
|
106
|
+
return null;
|
|
107
|
+
const budgetEntry = tierPresets[budget];
|
|
108
|
+
if (!budgetEntry || !budgetEntry.model)
|
|
109
|
+
return null;
|
|
110
|
+
return budgetEntry.model;
|
|
111
|
+
}
|
|
112
|
+
function resolveModelInternal(cwd, agentType) {
|
|
113
|
+
const config = loadConfig(cwd);
|
|
114
|
+
// 1. Per-agent override
|
|
115
|
+
const modelOverrides = config['model_overrides'];
|
|
116
|
+
const override = modelOverrides?.[agentType];
|
|
117
|
+
if (override) {
|
|
118
|
+
return override;
|
|
119
|
+
}
|
|
120
|
+
// 2. Compute the tier
|
|
121
|
+
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
|
122
|
+
const profile = String(config['model_profile'] || 'balanced').toLowerCase();
|
|
123
|
+
const agentModels = MODEL_PROFILES[agentType];
|
|
124
|
+
const phaseType = (AGENT_TO_PHASE_TYPE)[agentType];
|
|
125
|
+
const configModels = config['models'];
|
|
126
|
+
const phaseTypeTier = (phaseType && configModels && typeof configModels === 'object')
|
|
127
|
+
? configModels[phaseType]
|
|
128
|
+
: undefined;
|
|
129
|
+
const VALID_TIERS = new Set(['opus', 'sonnet', 'haiku', 'inherit']);
|
|
130
|
+
const tier = (phaseTypeTier && VALID_TIERS.has(phaseTypeTier))
|
|
131
|
+
? phaseTypeTier
|
|
132
|
+
: (profile === 'inherit'
|
|
133
|
+
? 'inherit'
|
|
134
|
+
: (agentModels ? (agentModels[profile] || agentModels['balanced']) : null));
|
|
135
|
+
// 2.5. model_policy preset (#49)
|
|
136
|
+
const configRuntime = config['runtime'];
|
|
137
|
+
if (configRuntime && configRuntime !== 'claude' && tier && tier !== 'inherit') {
|
|
138
|
+
const mergedPolicy = config['model_policy']
|
|
139
|
+
? { ...config['model_policy'], runtime: configRuntime }
|
|
140
|
+
: null;
|
|
141
|
+
const policyModel = resolveModelPolicy(mergedPolicy, tier);
|
|
142
|
+
if (policyModel)
|
|
143
|
+
return policyModel;
|
|
144
|
+
}
|
|
145
|
+
// 3. Runtime-aware resolution (#2517)
|
|
146
|
+
if (configRuntime && configRuntime !== 'claude' && tier && tier !== 'inherit') {
|
|
147
|
+
const entry = _resolveRuntimeTier(config, tier);
|
|
148
|
+
if (entry?.model)
|
|
149
|
+
return entry.model;
|
|
150
|
+
}
|
|
151
|
+
// 4. resolve_model_ids: "omit"
|
|
152
|
+
if (config['resolve_model_ids'] === 'omit') {
|
|
153
|
+
return '';
|
|
154
|
+
}
|
|
155
|
+
// 5. Profile lookup (Claude-native default).
|
|
156
|
+
if (!agentModels) {
|
|
157
|
+
return profile === 'quality' ? 'opus'
|
|
158
|
+
: profile === 'budget' ? 'haiku'
|
|
159
|
+
: profile === 'inherit' ? 'inherit'
|
|
160
|
+
: 'sonnet';
|
|
161
|
+
}
|
|
162
|
+
if (tier === 'inherit')
|
|
163
|
+
return 'inherit';
|
|
164
|
+
const alias = tier;
|
|
165
|
+
if (config['resolve_model_ids']) {
|
|
166
|
+
return model_catalog_cjs_1.MODEL_ALIAS_MAP[alias] || alias;
|
|
167
|
+
}
|
|
168
|
+
return alias;
|
|
169
|
+
}
|
|
170
|
+
const VALID_GRANULARITIES = new Set(['coarse', 'standard', 'fine']);
|
|
171
|
+
/**
|
|
172
|
+
* Resolve the planning granularity for a phase type (#68).
|
|
173
|
+
*/
|
|
174
|
+
function resolveGranularityInternal(cwd, phaseType, override) {
|
|
175
|
+
if (override !== undefined && override !== null && override !== '') {
|
|
176
|
+
if (VALID_GRANULARITIES.has(override)) {
|
|
177
|
+
return override;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const config = loadConfig(cwd);
|
|
181
|
+
const configGranularities = config['granularities'];
|
|
182
|
+
const perPhase = (phaseType && configGranularities && typeof configGranularities === 'object')
|
|
183
|
+
? configGranularities[phaseType]
|
|
184
|
+
: undefined;
|
|
185
|
+
if (perPhase && VALID_GRANULARITIES.has(perPhase)) {
|
|
186
|
+
return perPhase;
|
|
187
|
+
}
|
|
188
|
+
if (config['granularity'] !== undefined && config['granularity'] !== null && config['granularity'] !== '') {
|
|
189
|
+
return config['granularity'];
|
|
190
|
+
}
|
|
191
|
+
const planning = config['planning'];
|
|
192
|
+
const planningGran = planning && planning['granularity'];
|
|
193
|
+
if (planningGran !== undefined && planningGran !== null && planningGran !== '') {
|
|
194
|
+
return planningGran;
|
|
195
|
+
}
|
|
196
|
+
return 'standard';
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Validate a CLI granularity override at the command boundary. Empty/null/undefined
|
|
200
|
+
* are treated as "no override" (no-op). An invalid non-empty value calls `fail`.
|
|
201
|
+
*/
|
|
202
|
+
function assertValidGranularityOverride(override, fail) {
|
|
203
|
+
if (override !== undefined && override !== null && override !== '' && !VALID_GRANULARITIES.has(override)) {
|
|
204
|
+
fail(`invalid granularity '${override}' (valid: ${[...VALID_GRANULARITIES].join(', ')})`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* #3024 — Resolve a model for a specific dynamic-routing attempt.
|
|
209
|
+
*/
|
|
210
|
+
function resolveModelForTier(cwd, agentType, attempt) {
|
|
211
|
+
const config = loadConfig(cwd);
|
|
212
|
+
const attemptN = Number.isInteger(attempt) && attempt > 0 ? attempt : 0;
|
|
213
|
+
const modelOverrides = config['model_overrides'];
|
|
214
|
+
const override = modelOverrides?.[agentType];
|
|
215
|
+
if (override)
|
|
216
|
+
return override;
|
|
217
|
+
if (config['model_policy'] && config['runtime'] && config['runtime'] !== 'claude') {
|
|
218
|
+
return resolveModelInternal(cwd, agentType);
|
|
219
|
+
}
|
|
220
|
+
const dr = config['dynamic_routing'];
|
|
221
|
+
if (!dr || typeof dr !== 'object' || dr['enabled'] !== true) {
|
|
222
|
+
return resolveModelInternal(cwd, agentType);
|
|
223
|
+
}
|
|
224
|
+
const tierModels = dr['tier_models'];
|
|
225
|
+
if (!tierModels || typeof tierModels !== 'object') {
|
|
226
|
+
return resolveModelInternal(cwd, agentType);
|
|
227
|
+
}
|
|
228
|
+
const defaultTier = (AGENT_DEFAULT_TIERS)[agentType];
|
|
229
|
+
if (!defaultTier || !(VALID_AGENT_TIERS).has(defaultTier)) {
|
|
230
|
+
return resolveModelInternal(cwd, agentType);
|
|
231
|
+
}
|
|
232
|
+
const maxEscalations = Number.isInteger(dr['max_escalations']) && dr['max_escalations'] >= 0
|
|
233
|
+
? dr['max_escalations']
|
|
234
|
+
: 1;
|
|
235
|
+
const escalationEnabled = dr['escalate_on_failure'] !== false;
|
|
236
|
+
const effectiveAttempt = escalationEnabled
|
|
237
|
+
? Math.min(attemptN, maxEscalations)
|
|
238
|
+
: 0;
|
|
239
|
+
let tier = defaultTier;
|
|
240
|
+
for (let i = 0; i < effectiveAttempt; i += 1) {
|
|
241
|
+
const next = (nextTier)(tier);
|
|
242
|
+
if (!next || next === tier)
|
|
243
|
+
break;
|
|
244
|
+
tier = next;
|
|
245
|
+
}
|
|
246
|
+
const alias = tierModels[tier];
|
|
247
|
+
if (typeof alias !== 'string' || alias.length === 0) {
|
|
248
|
+
return resolveModelInternal(cwd, agentType);
|
|
249
|
+
}
|
|
250
|
+
return alias;
|
|
251
|
+
}
|
|
252
|
+
// ─── #443 — Unified effort + fast_mode resolvers ─────────────────────────────
|
|
253
|
+
const VALID_EFFORTS = ['minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
|
|
254
|
+
const EFFORT_SET = new Set(VALID_EFFORTS);
|
|
255
|
+
/**
|
|
256
|
+
* Walk one step up the effort ladder from `e`.
|
|
257
|
+
*/
|
|
258
|
+
function nextEffort(e) {
|
|
259
|
+
const i = VALID_EFFORTS.indexOf(e);
|
|
260
|
+
if (i < 0)
|
|
261
|
+
return null;
|
|
262
|
+
return VALID_EFFORTS[Math.min(i + 1, VALID_EFFORTS.length - 1)];
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* #443 — Resolve a universal effort string for (cwd, agentType).
|
|
266
|
+
*/
|
|
267
|
+
function resolveEffortInternal(cwd, agentType, opts) {
|
|
268
|
+
// Step 1: invocation override
|
|
269
|
+
if (opts && typeof opts.override === 'string' && EFFORT_SET.has(opts.override)) {
|
|
270
|
+
return opts.override;
|
|
271
|
+
}
|
|
272
|
+
const config = loadConfig(cwd);
|
|
273
|
+
const effortCfg = (config['effort'] && typeof config['effort'] === 'object' && !Array.isArray(config['effort']))
|
|
274
|
+
? config['effort']
|
|
275
|
+
: null;
|
|
276
|
+
// Step 2: agent_overrides
|
|
277
|
+
if (effortCfg) {
|
|
278
|
+
const ao = effortCfg['agent_overrides'];
|
|
279
|
+
if (ao && typeof ao === 'object' && !Array.isArray(ao)) {
|
|
280
|
+
const v = ao[agentType];
|
|
281
|
+
if (typeof v === 'string' && EFFORT_SET.has(v))
|
|
282
|
+
return v;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
const canonicalEffort = (configuration_cjs_1.CONFIG_DEFAULTS)['effort'];
|
|
287
|
+
const mao = canonicalEffort && typeof canonicalEffort === 'object'
|
|
288
|
+
? canonicalEffort['agent_overrides']
|
|
289
|
+
: undefined;
|
|
290
|
+
if (mao && typeof mao === 'object' && !Array.isArray(mao)) {
|
|
291
|
+
const v = mao[agentType];
|
|
292
|
+
if (typeof v === 'string' && EFFORT_SET.has(v))
|
|
293
|
+
return v;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// Step 3: routing_tier_defaults by agent's default tier.
|
|
297
|
+
const agentTier = (AGENT_DEFAULT_TIERS)[agentType];
|
|
298
|
+
if (agentTier) {
|
|
299
|
+
if (effortCfg && effortCfg['routing_tier_defaults'] &&
|
|
300
|
+
typeof effortCfg['routing_tier_defaults'] === 'object' &&
|
|
301
|
+
!Array.isArray(effortCfg['routing_tier_defaults'])) {
|
|
302
|
+
const v = effortCfg['routing_tier_defaults'][agentTier];
|
|
303
|
+
if (typeof v === 'string' && EFFORT_SET.has(v))
|
|
304
|
+
return v;
|
|
305
|
+
}
|
|
306
|
+
else if (!effortCfg) {
|
|
307
|
+
const canonicalEffort = (configuration_cjs_1.CONFIG_DEFAULTS)['effort'];
|
|
308
|
+
const manifestDefaults = canonicalEffort && typeof canonicalEffort === 'object'
|
|
309
|
+
? canonicalEffort['routing_tier_defaults']
|
|
310
|
+
: undefined;
|
|
311
|
+
if (manifestDefaults && typeof manifestDefaults === 'object') {
|
|
312
|
+
const v = manifestDefaults[agentTier];
|
|
313
|
+
if (typeof v === 'string' && EFFORT_SET.has(v))
|
|
314
|
+
return v;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// Step 4: effort.default
|
|
319
|
+
if (effortCfg) {
|
|
320
|
+
const d = effortCfg['default'];
|
|
321
|
+
if (typeof d === 'string' && EFFORT_SET.has(d))
|
|
322
|
+
return d;
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
const canonicalEffort = (configuration_cjs_1.CONFIG_DEFAULTS)['effort'];
|
|
326
|
+
const d = canonicalEffort && typeof canonicalEffort === 'object'
|
|
327
|
+
? canonicalEffort['default']
|
|
328
|
+
: undefined;
|
|
329
|
+
if (typeof d === 'string' && EFFORT_SET.has(d))
|
|
330
|
+
return d;
|
|
331
|
+
}
|
|
332
|
+
// Step 5: hardcoded default
|
|
333
|
+
return 'high';
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* #443 — Resolve fast_mode boolean for (cwd, agentType).
|
|
337
|
+
*/
|
|
338
|
+
function resolveFastModeInternal(cwd, agentType, opts) {
|
|
339
|
+
// Step 1: invocation override
|
|
340
|
+
if (opts && typeof opts.override === 'boolean') {
|
|
341
|
+
return opts.override;
|
|
342
|
+
}
|
|
343
|
+
const config = loadConfig(cwd);
|
|
344
|
+
const fmCfg = (config['fast_mode'] && typeof config['fast_mode'] === 'object' && !Array.isArray(config['fast_mode']))
|
|
345
|
+
? config['fast_mode']
|
|
346
|
+
: null;
|
|
347
|
+
// Step 2: agent_overrides
|
|
348
|
+
if (fmCfg) {
|
|
349
|
+
const ao = fmCfg['agent_overrides'];
|
|
350
|
+
if (ao && typeof ao === 'object' && !Array.isArray(ao)) {
|
|
351
|
+
const v = ao[agentType];
|
|
352
|
+
if (typeof v === 'boolean')
|
|
353
|
+
return v;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
// Step 3: routing_tier_defaults by agent's default tier.
|
|
357
|
+
const agentTier = (AGENT_DEFAULT_TIERS)[agentType];
|
|
358
|
+
if (agentTier) {
|
|
359
|
+
if (fmCfg && fmCfg['routing_tier_defaults'] &&
|
|
360
|
+
typeof fmCfg['routing_tier_defaults'] === 'object' &&
|
|
361
|
+
!Array.isArray(fmCfg['routing_tier_defaults'])) {
|
|
362
|
+
const v = fmCfg['routing_tier_defaults'][agentTier];
|
|
363
|
+
if (typeof v === 'boolean')
|
|
364
|
+
return v;
|
|
365
|
+
}
|
|
366
|
+
else if (!fmCfg) {
|
|
367
|
+
const canonicalFm = (configuration_cjs_1.CONFIG_DEFAULTS)['fast_mode'];
|
|
368
|
+
const manifestDefaults = canonicalFm && typeof canonicalFm === 'object'
|
|
369
|
+
? canonicalFm['routing_tier_defaults']
|
|
370
|
+
: undefined;
|
|
371
|
+
if (manifestDefaults && typeof manifestDefaults === 'object') {
|
|
372
|
+
const v = manifestDefaults[agentTier];
|
|
373
|
+
if (typeof v === 'boolean')
|
|
374
|
+
return v;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
// Step 4: fast_mode.enabled
|
|
379
|
+
if (fmCfg && typeof fmCfg['enabled'] === 'boolean') {
|
|
380
|
+
return fmCfg['enabled'];
|
|
381
|
+
}
|
|
382
|
+
// Step 5: hardcoded default
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* #443 — Resolve effort for a dynamic-routing attempt (with escalation).
|
|
387
|
+
*/
|
|
388
|
+
function resolveEffortForTier(cwd, agentType, attempt) {
|
|
389
|
+
const base = resolveEffortInternal(cwd, agentType);
|
|
390
|
+
const config = loadConfig(cwd);
|
|
391
|
+
const dr = config['dynamic_routing'];
|
|
392
|
+
if (!dr || typeof dr !== 'object' || dr['enabled'] !== true) {
|
|
393
|
+
return base;
|
|
394
|
+
}
|
|
395
|
+
if (dr['escalate_on_failure'] === false) {
|
|
396
|
+
return base;
|
|
397
|
+
}
|
|
398
|
+
const maxEscalations = Number.isInteger(dr['max_escalations']) && dr['max_escalations'] >= 0
|
|
399
|
+
? dr['max_escalations']
|
|
400
|
+
: 1;
|
|
401
|
+
const attemptN = Number.isInteger(attempt) && attempt > 0 ? attempt : 0;
|
|
402
|
+
const effectiveAttempt = Math.min(attemptN, maxEscalations);
|
|
403
|
+
let current = base;
|
|
404
|
+
for (let i = 0; i < effectiveAttempt; i++) {
|
|
405
|
+
const next = nextEffort(current);
|
|
406
|
+
if (!next || next === current)
|
|
407
|
+
break;
|
|
408
|
+
current = next;
|
|
409
|
+
}
|
|
410
|
+
return current;
|
|
411
|
+
}
|
|
412
|
+
module.exports = {
|
|
413
|
+
resolveTierEntry,
|
|
414
|
+
resolveModelPolicy,
|
|
415
|
+
resolveModelInternal,
|
|
416
|
+
VALID_GRANULARITIES,
|
|
417
|
+
resolveGranularityInternal,
|
|
418
|
+
assertValidGranularityOverride,
|
|
419
|
+
resolveModelForTier,
|
|
420
|
+
VALID_EFFORTS,
|
|
421
|
+
EFFORT_SET,
|
|
422
|
+
nextEffort,
|
|
423
|
+
resolveEffortInternal,
|
|
424
|
+
resolveFastModeInternal,
|
|
425
|
+
resolveEffortForTier,
|
|
426
|
+
};
|
|
@@ -140,6 +140,26 @@ function routePhaseCommand({ phase, args, cwd, raw, error }) {
|
|
|
140
140
|
phase.cmdPhaseComplete(cwd, args[2], raw);
|
|
141
141
|
return { ok: true, data: null };
|
|
142
142
|
},
|
|
143
|
+
'uat-passed': (_ctx) => {
|
|
144
|
+
let requireVerification = false;
|
|
145
|
+
const positional = [];
|
|
146
|
+
for (const token of args.slice(2)) {
|
|
147
|
+
if (token === '--require-verification') {
|
|
148
|
+
requireVerification = true;
|
|
149
|
+
}
|
|
150
|
+
else if (token === '--raw') {
|
|
151
|
+
// --raw is handled by the outer CLI layer; accepted here silently
|
|
152
|
+
}
|
|
153
|
+
else if (token.startsWith('--')) {
|
|
154
|
+
return makeInvalidArgs(token, `phase uat-passed does not support ${token}`);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
positional.push(token);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
phase.cmdPhaseUatPassed(cwd, positional[0], raw, { policy: { requireVerification } });
|
|
161
|
+
return { ok: true, data: null };
|
|
162
|
+
},
|
|
143
163
|
},
|
|
144
164
|
};
|
|
145
165
|
// ── Build manifest (available subcommands for UnknownCommand detection) ─────
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Pure phase-id parsing/matching helpers — normalize, token match,
|
|
4
|
+
* milestone/phase-dir id parsing, phase-markdown regex builders.
|
|
5
|
+
*
|
|
6
|
+
* Extracted from core.cts (ADR-857 rollout phase 2a / issue #865).
|
|
7
|
+
* The hand-written bodies are preserved byte-for-behaviour; only the module
|
|
8
|
+
* boundary moved. core.cts re-exports every symbol here under its own
|
|
9
|
+
* `export =` object so existing consumers are unaffected.
|
|
10
|
+
*
|
|
11
|
+
* New imports should pull phase-id helpers from phase-id.cjs directly.
|
|
12
|
+
*
|
|
13
|
+
* Dependencies: none (pure string/regex, no Node built-ins required).
|
|
14
|
+
*/
|
|
15
|
+
// ─── Phase-id helpers ─────────────────────────────────────────────────────────
|
|
16
|
+
function escapeRegex(value) {
|
|
17
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
18
|
+
}
|
|
19
|
+
function normalizePhaseName(phase) {
|
|
20
|
+
const str = String(phase);
|
|
21
|
+
// Strip optional project_code prefix (e.g., 'CK-01' → '01')
|
|
22
|
+
const stripped = str.replace(/^[A-Z]{1,6}-(?=\d)/, '');
|
|
23
|
+
// Milestone-prefixed phase IDs: M-NN or M-N-N (deep decomposition).
|
|
24
|
+
const milestoneMatch = stripped.match(/^(\d+)((?:-\d+)+)([A-Z]?(?:\.\d+)*)$/i);
|
|
25
|
+
if (milestoneMatch) {
|
|
26
|
+
const major = milestoneMatch[1].padStart(2, '0');
|
|
27
|
+
const subSegments = milestoneMatch[2].slice(1).split('-').map(s => s.padStart(2, '0'));
|
|
28
|
+
const suffix = milestoneMatch[3] || '';
|
|
29
|
+
return `${major}-${subSegments.join('-')}${suffix}`;
|
|
30
|
+
}
|
|
31
|
+
// Standard numeric phases: 1, 01, 12A, 12.1
|
|
32
|
+
const match = stripped.match(/^(\d+)([A-Z])?((?:\.\d+)*)/i);
|
|
33
|
+
if (match) {
|
|
34
|
+
const padded = match[1].padStart(2, '0');
|
|
35
|
+
// Preserve original case of letter suffix (#1962).
|
|
36
|
+
const letter = match[2] || '';
|
|
37
|
+
const decimal = match[3] || '';
|
|
38
|
+
return padded + letter + decimal;
|
|
39
|
+
}
|
|
40
|
+
// Custom phase IDs (e.g. PROJ-42, AUTH-101): return as-is
|
|
41
|
+
return str;
|
|
42
|
+
}
|
|
43
|
+
function getMilestoneFromPhaseId(phaseId) {
|
|
44
|
+
const str = String(phaseId);
|
|
45
|
+
const stripped = str.replace(/^[A-Z]{1,6}-(?=\d)/i, '');
|
|
46
|
+
const m = stripped.match(/^0*(\d+)-\d/);
|
|
47
|
+
if (!m)
|
|
48
|
+
return null;
|
|
49
|
+
const major = parseInt(m[1], 10);
|
|
50
|
+
if (major === 0 || major === 999)
|
|
51
|
+
return null;
|
|
52
|
+
return `v${major}.0`;
|
|
53
|
+
}
|
|
54
|
+
function getPhaseDirFromPhaseId(phaseId, phaseName, projectCode) {
|
|
55
|
+
const str = String(phaseId);
|
|
56
|
+
const stripped = str.replace(/^[A-Z]{1,6}-(?=\d)/i, '');
|
|
57
|
+
const m = stripped.match(/^0*(\d+)-(0*(\d+(?:-\d+)*))$/);
|
|
58
|
+
if (!m)
|
|
59
|
+
return null;
|
|
60
|
+
const milestone = String(parseInt(m[1], 10)).padStart(2, '0');
|
|
61
|
+
const subParts = m[2].split('-').map(p => String(parseInt(p, 10)).padStart(2, '0'));
|
|
62
|
+
const sub = subParts.join('-');
|
|
63
|
+
const slug = phaseName
|
|
64
|
+
? phaseName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '')
|
|
65
|
+
: '';
|
|
66
|
+
const parts = [milestone, sub, slug].filter(Boolean);
|
|
67
|
+
const base = parts.join('-');
|
|
68
|
+
return projectCode ? `${projectCode}-${base}` : base;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Render a regex source fragment matching a phase number against ROADMAP/STATE
|
|
72
|
+
* prose regardless of zero-padding on either side.
|
|
73
|
+
*/
|
|
74
|
+
function phaseMarkdownRegexSource(phaseNum) {
|
|
75
|
+
const stripped = String(phaseNum).replace(/^[A-Z]{1,6}-(?=\d)/i, '');
|
|
76
|
+
// Milestone-prefixed IDs: M-NN or M-N-N (deep).
|
|
77
|
+
const milestoneSegments = stripped.match(/^(\d+)((?:-\d+)*)([A-Z]?(?:\.\d+)*)$/i);
|
|
78
|
+
if (milestoneSegments && milestoneSegments[2]) {
|
|
79
|
+
const majorUnpadded = milestoneSegments[1].replace(/^0+/, '') || '0';
|
|
80
|
+
const subParts = milestoneSegments[2].slice(1).split('-');
|
|
81
|
+
const subFragments = subParts.map(s => {
|
|
82
|
+
const unpadded = s.replace(/^0+/, '') || '0';
|
|
83
|
+
return `0*${escapeRegex(unpadded)}`;
|
|
84
|
+
});
|
|
85
|
+
const suffix = milestoneSegments[3] || '';
|
|
86
|
+
const suffixFragment = suffix ? escapeRegex(suffix) : '';
|
|
87
|
+
return `0*${escapeRegex(majorUnpadded)}-${subFragments.join('-')}${suffixFragment}`;
|
|
88
|
+
}
|
|
89
|
+
// Plain numeric phase: 1, 01, 12A, 12.1
|
|
90
|
+
const match = stripped.match(/^0*(\d+)([A-Z])?((?:\.\d+)*)$/i);
|
|
91
|
+
if (!match)
|
|
92
|
+
return escapeRegex(phaseNum);
|
|
93
|
+
const integer = match[1].replace(/^0+/, '') || '0';
|
|
94
|
+
const letter = match[2] ? escapeRegex(match[2]) : '';
|
|
95
|
+
const decimal = match[3] ? escapeRegex(match[3]) : '';
|
|
96
|
+
return `0*${escapeRegex(integer)}${letter}${decimal}`;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* #3599: when the caller passed a project-code-prefixed ID like `PROJ-42`,
|
|
100
|
+
* return the exact-escaped form.
|
|
101
|
+
*/
|
|
102
|
+
function phaseMarkdownRegexSourceExact(phaseNum) {
|
|
103
|
+
const raw = String(phaseNum);
|
|
104
|
+
if (!/^[A-Z]{1,6}-(?=\d)/i.test(raw))
|
|
105
|
+
return null;
|
|
106
|
+
return escapeRegex(raw);
|
|
107
|
+
}
|
|
108
|
+
function comparePhaseNum(a, b) {
|
|
109
|
+
// Strip optional project_code prefix before comparing
|
|
110
|
+
const sa = String(a).replace(/^[A-Z]{1,6}-(?=\d)/i, '');
|
|
111
|
+
const sb = String(b).replace(/^[A-Z]{1,6}-(?=\d)/i, '');
|
|
112
|
+
const milestoneA = sa.match(/^(\d+)((?:-\d+)+)([A-Z]?(?:\.\d+)*)$/i);
|
|
113
|
+
const milestoneB = sb.match(/^(\d+)((?:-\d+)+)([A-Z]?(?:\.\d+)*)$/i);
|
|
114
|
+
if (milestoneA && milestoneB) {
|
|
115
|
+
const segsA = [parseInt(milestoneA[1], 10), ...milestoneA[2].slice(1).split('-').map(s => parseInt(s, 10))];
|
|
116
|
+
const segsB = [parseInt(milestoneB[1], 10), ...milestoneB[2].slice(1).split('-').map(s => parseInt(s, 10))];
|
|
117
|
+
const maxSegs = Math.max(segsA.length, segsB.length);
|
|
118
|
+
for (let i = 0; i < maxSegs; i++) {
|
|
119
|
+
const av = segsA[i] !== undefined ? segsA[i] : 0;
|
|
120
|
+
const bv = segsB[i] !== undefined ? segsB[i] : 0;
|
|
121
|
+
if (av !== bv)
|
|
122
|
+
return av - bv;
|
|
123
|
+
}
|
|
124
|
+
const sufA = milestoneA[3] || '';
|
|
125
|
+
const sufB = milestoneB[3] || '';
|
|
126
|
+
if (sufA !== sufB)
|
|
127
|
+
return sufA < sufB ? -1 : 1;
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
if (milestoneA || milestoneB)
|
|
131
|
+
return String(a).localeCompare(String(b));
|
|
132
|
+
const pa = sa.match(/^(\d+)([A-Z])?((?:\.\d+)*)/i);
|
|
133
|
+
const pb = sb.match(/^(\d+)([A-Z])?((?:\.\d+)*)/i);
|
|
134
|
+
if (!pa || !pb)
|
|
135
|
+
return String(a).localeCompare(String(b));
|
|
136
|
+
const intDiff = parseInt(pa[1], 10) - parseInt(pb[1], 10);
|
|
137
|
+
if (intDiff !== 0)
|
|
138
|
+
return intDiff;
|
|
139
|
+
const la = (pa[2] || '').toUpperCase();
|
|
140
|
+
const lb = (pb[2] || '').toUpperCase();
|
|
141
|
+
if (la !== lb) {
|
|
142
|
+
if (!la)
|
|
143
|
+
return -1;
|
|
144
|
+
if (!lb)
|
|
145
|
+
return 1;
|
|
146
|
+
return la < lb ? -1 : 1;
|
|
147
|
+
}
|
|
148
|
+
const aDecParts = pa[3] ? pa[3].slice(1).split('.').map(p => parseInt(p, 10)) : [];
|
|
149
|
+
const bDecParts = pb[3] ? pb[3].slice(1).split('.').map(p => parseInt(p, 10)) : [];
|
|
150
|
+
const maxLen = Math.max(aDecParts.length, bDecParts.length);
|
|
151
|
+
if (aDecParts.length === 0 && bDecParts.length > 0)
|
|
152
|
+
return -1;
|
|
153
|
+
if (bDecParts.length === 0 && aDecParts.length > 0)
|
|
154
|
+
return 1;
|
|
155
|
+
for (let i = 0; i < maxLen; i++) {
|
|
156
|
+
const av = Number.isFinite(aDecParts[i]) ? aDecParts[i] : 0;
|
|
157
|
+
const bv = Number.isFinite(bDecParts[i]) ? bDecParts[i] : 0;
|
|
158
|
+
if (av !== bv)
|
|
159
|
+
return av - bv;
|
|
160
|
+
}
|
|
161
|
+
return 0;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Extract the phase token from a directory name.
|
|
165
|
+
*/
|
|
166
|
+
function extractPhaseToken(dirName) {
|
|
167
|
+
const codePrefixMatch = dirName.match(/^([A-Z]{1,6})-(\d.*)/i);
|
|
168
|
+
let prefix = '';
|
|
169
|
+
let rest = dirName;
|
|
170
|
+
if (codePrefixMatch) {
|
|
171
|
+
prefix = codePrefixMatch[1] + '-';
|
|
172
|
+
rest = codePrefixMatch[2];
|
|
173
|
+
}
|
|
174
|
+
const segments = rest.split('-');
|
|
175
|
+
const tokenSegments = [];
|
|
176
|
+
for (let i = 0; i < segments.length; i++) {
|
|
177
|
+
const seg = segments[i];
|
|
178
|
+
if (/^\d/.test(seg)) {
|
|
179
|
+
tokenSegments.push(seg);
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (tokenSegments.length === 0) {
|
|
186
|
+
return dirName;
|
|
187
|
+
}
|
|
188
|
+
return prefix + tokenSegments.join('-');
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Check if a directory name's phase token matches the normalized phase exactly.
|
|
192
|
+
*/
|
|
193
|
+
function phaseTokenMatches(dirName, normalized) {
|
|
194
|
+
const token = extractPhaseToken(dirName);
|
|
195
|
+
if (token.toUpperCase() === normalized.toUpperCase())
|
|
196
|
+
return true;
|
|
197
|
+
const stripped = dirName.replace(/^[A-Z]{1,6}-(?=\d)/i, '');
|
|
198
|
+
if (stripped !== dirName) {
|
|
199
|
+
const strippedToken = extractPhaseToken(stripped);
|
|
200
|
+
if (strippedToken.toUpperCase() === normalized.toUpperCase())
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
module.exports = {
|
|
206
|
+
escapeRegex,
|
|
207
|
+
normalizePhaseName,
|
|
208
|
+
getMilestoneFromPhaseId,
|
|
209
|
+
getPhaseDirFromPhaseId,
|
|
210
|
+
phaseMarkdownRegexSource,
|
|
211
|
+
phaseMarkdownRegexSourceExact,
|
|
212
|
+
comparePhaseNum,
|
|
213
|
+
extractPhaseToken,
|
|
214
|
+
phaseTokenMatches,
|
|
215
|
+
};
|