@opengsd/gsd-core 1.8.0 → 1.9.1
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 +31 -1
- package/agents/gsd-code-fixer.md +107 -34
- package/agents/gsd-codebase-mapper.md +1 -1
- package/agents/gsd-debug-session-manager.md +36 -0
- package/agents/gsd-executor.md +20 -7
- 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 +15 -23
- package/agents/gsd-project-researcher.md +2 -2
- package/agents/gsd-ui-auditor.md +0 -40
- package/bin/install.js +236 -107
- package/commands/gsd/plan-review-convergence.md +5 -1
- package/gsd-core/bin/gsd-tools.cjs +882 -4
- package/gsd-core/bin/lib/api-coverage.cjs +22 -8
- package/gsd-core/bin/lib/audit.cjs +8 -8
- 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 +1353 -132
- 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/check-command-router.cjs +12 -2
- package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +15 -0
- package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +102 -12
- package/gsd-core/bin/lib/claude-orchestration.cjs +125 -22
- package/gsd-core/bin/lib/commands.cjs +246 -18
- package/gsd-core/bin/lib/config-loader.cjs +200 -28
- package/gsd-core/bin/lib/config.cjs +90 -5
- package/gsd-core/bin/lib/estimate-cli.cjs +336 -0
- package/gsd-core/bin/lib/frontmatter.cjs +125 -15
- package/gsd-core/bin/lib/host-integration.cjs +215 -8
- package/gsd-core/bin/lib/init.cjs +44 -19
- package/gsd-core/bin/lib/install-engine.cjs +1 -0
- package/gsd-core/bin/lib/milestone.cjs +36 -9
- package/gsd-core/bin/lib/model-catalog.cjs +51 -1
- package/gsd-core/bin/lib/observability/logger.cjs +7 -2
- 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 +278 -5
- package/gsd-core/bin/lib/phase.cjs +61 -6
- package/gsd-core/bin/lib/plan-drift-guard.cjs +1 -1
- package/gsd-core/bin/lib/plan-scan.cjs +1 -1
- 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/project-root.cjs +48 -0
- 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 +54 -6
- package/gsd-core/bin/lib/roadmap.cjs +10 -4
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +31 -4
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +1 -1
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +140 -0
- package/gsd-core/bin/lib/runtime-name-policy.cjs +15 -2
- package/gsd-core/bin/lib/smart-entry.cjs +1 -1
- package/gsd-core/bin/lib/state-document.cjs +164 -20
- package/gsd-core/bin/lib/state-transition.cjs +28 -10
- package/gsd-core/bin/lib/state.cjs +141 -21
- package/gsd-core/bin/lib/uat-predicate.cjs +6 -4
- package/gsd-core/bin/lib/uat.cjs +9 -7
- 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 +32 -0
- package/gsd-core/bin/lib/verification.cjs +51 -14
- package/gsd-core/bin/lib/verify.cjs +146 -22
- 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 +1 -13
- 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/context-budget.md +40 -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/planning-config.md +2 -1
- package/gsd-core/references/reviewer-instances.md +28 -21
- package/gsd-core/references/runtime-aware-dispatch.md +42 -0
- package/gsd-core/references/ui-consideration-probe.md +2 -2
- package/gsd-core/references/worktree-branch-check.md +4 -4
- 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/ai-integration-phase.md +4 -4
- package/gsd-core/workflows/audit-fix.md +4 -0
- package/gsd-core/workflows/audit-milestone.md +8 -0
- package/gsd-core/workflows/autonomous.md +19 -15
- package/gsd-core/workflows/check-todos.md +2 -2
- package/gsd-core/workflows/code-review-fix.md +14 -6
- package/gsd-core/workflows/code-review.md +93 -21
- package/gsd-core/workflows/debug.md +10 -2
- package/gsd-core/workflows/diagnose-issues.md +4 -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-assumptions.md +15 -9
- package/gsd-core/workflows/discuss-phase.md +2 -2
- package/gsd-core/workflows/docs-update.md +8 -0
- package/gsd-core/workflows/eval-review.md +1 -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.md +85 -115
- package/gsd-core/workflows/execute-plan.md +5 -4
- package/gsd-core/workflows/explore.md +4 -0
- package/gsd-core/workflows/extract-learnings.md +21 -0
- package/gsd-core/workflows/help/modes/full.md +3 -3
- package/gsd-core/workflows/import.md +4 -1
- package/gsd-core/workflows/ingest-docs.md +4 -0
- package/gsd-core/workflows/map-codebase.md +13 -6
- package/gsd-core/workflows/new-milestone.md +10 -2
- package/gsd-core/workflows/new-project.md +11 -4
- package/gsd-core/workflows/next.md +5 -2
- package/gsd-core/workflows/plan-phase.md +42 -46
- package/gsd-core/workflows/plan-review-convergence.md +18 -14
- package/gsd-core/workflows/progress.md +1 -1
- package/gsd-core/workflows/quick.md +14 -3
- package/gsd-core/workflows/review.md +146 -575
- package/gsd-core/workflows/scan.md +9 -1
- package/gsd-core/workflows/secure-phase.md +10 -2
- package/gsd-core/workflows/ship.md +41 -11
- package/gsd-core/workflows/smart-entry.md +1 -1
- package/gsd-core/workflows/ui-phase.md +8 -1
- package/gsd-core/workflows/ui-review.md +8 -1
- package/gsd-core/workflows/update.md +104 -5
- package/gsd-core/workflows/validate-phase.md +10 -2
- package/gsd-core/workflows/verify-work.md +8 -1
- 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 +9 -6
- 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-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 +9 -6
- 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 +7 -7
- package/pi/gsd.cjs +26 -1
- package/scripts/check-coverage-gate.cjs +51 -0
- package/scripts/check-glossary-refs.cjs +24 -0
- package/scripts/ci-test-scope.cjs +67 -17
- package/scripts/gen-adr-index.cjs +6 -4
- 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-registry.cjs +39 -15
- 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-resolution-provenance.cjs +9 -0
- package/scripts/mutation-matrix.cjs +4 -0
- package/scripts/prompt-injection-scan.sh +6 -0
- package/scripts/registry-schema.cjs +372 -94
- package/scripts/release-notes/conventional-title.cjs +19 -1
- package/scripts/release-notes/format-github-release-notes.cjs +7 -3
- package/scripts/validate-registry.cjs +10 -6
- package/scripts/workflow-size.cjs +16 -8
- package/skills/gsd-plan-review-convergence/SKILL.md +5 -1
- package/vscode/package.json +1 -1
- package/scripts/gen-golden-install-parity-zcode.cjs +0 -77
- package/scripts/update-size-baseline.cjs +0 -68
|
@@ -10,10 +10,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
10
10
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
11
11
|
};
|
|
12
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
-
exports.RUNTIMES_WITH_FAST_MODE = exports.EFFORT_RENDERING = exports.KNOWN_PROVIDERS = exports.PROVIDER_PRESETS = exports.RUNTIMES_WITH_REASONING_EFFORT = exports.KNOWN_RUNTIMES = exports.RUNTIME_PROFILE_MAP = exports.MODEL_ALIAS_MAP = exports.AGENT_DEFAULT_TIERS = exports.AGENT_TO_PHASE_TYPE = exports.MODEL_PROFILES = exports.ADAPTIVE_TIER_VALUES = exports.VALID_TIERS = exports.VALID_AGENT_TIERS = exports.VALID_PHASE_TYPES = exports.VALID_PROFILES = exports.catalog = void 0;
|
|
13
|
+
exports.RUNTIMES_WITH_FAST_MODE = exports.EFFORT_ARGV = exports.EFFORT_RENDERING = exports.KNOWN_PROVIDERS = exports.PROVIDER_PRESETS = exports.RUNTIMES_WITH_REASONING_EFFORT = exports.KNOWN_RUNTIMES = exports.RUNTIME_PROFILE_MAP = exports.MODEL_ALIAS_MAP = exports.AGENT_DEFAULT_TIERS = exports.AGENT_TO_PHASE_TYPE = exports.MODEL_PROFILES = exports.ADAPTIVE_TIER_VALUES = exports.VALID_TIERS = exports.VALID_AGENT_TIERS = exports.VALID_PHASE_TYPES = exports.VALID_PROFILES = exports.catalog = void 0;
|
|
14
14
|
exports.nextTier = nextTier;
|
|
15
15
|
exports.formatAgentToModelMapAsTable = formatAgentToModelMapAsTable;
|
|
16
16
|
exports.getAgentToModelMapForProfile = getAgentToModelMapForProfile;
|
|
17
|
+
exports.renderEffortArgv = renderEffortArgv;
|
|
17
18
|
exports.renderEffortForRuntime = renderEffortForRuntime;
|
|
18
19
|
const node_path_1 = __importDefault(require("node:path"));
|
|
19
20
|
// In .cts (CommonJS output) files, `require` is available as a global;
|
|
@@ -151,6 +152,55 @@ exports.EFFORT_RENDERING = {
|
|
|
151
152
|
},
|
|
152
153
|
},
|
|
153
154
|
};
|
|
155
|
+
exports.EFFORT_ARGV = {
|
|
156
|
+
// Verified against `claude --help`: `--effort <level>`.
|
|
157
|
+
claude: {
|
|
158
|
+
render: (level) => ['--effort', level],
|
|
159
|
+
supported: new Set(['low', 'medium', 'high', 'xhigh', 'max']),
|
|
160
|
+
clamp: (level) => (level === 'minimal' ? 'low' : level),
|
|
161
|
+
},
|
|
162
|
+
// Verified against `opencode run --help`: `--variant` — "model variant
|
|
163
|
+
// (provider-specific reasoning effort, e.g., high, max, minimal)".
|
|
164
|
+
opencode: {
|
|
165
|
+
render: (level) => ['--variant', level],
|
|
166
|
+
supported: new Set(['minimal', 'low', 'medium', 'high', 'xhigh', 'max']),
|
|
167
|
+
clamp: (level) => level,
|
|
168
|
+
},
|
|
169
|
+
// First-party Codex docs: `model_reasoning_effort` is a config-only key with no
|
|
170
|
+
// dedicated flag, so the generic `-c key=value` override is the only argv route.
|
|
171
|
+
codex: {
|
|
172
|
+
render: (level) => ['-c', `model_reasoning_effort=${level}`],
|
|
173
|
+
supported: new Set(['minimal', 'low', 'medium', 'high', 'xhigh']),
|
|
174
|
+
clamp: (level) => (level === 'max' ? 'xhigh' : level),
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
/**
|
|
178
|
+
* Render the invocation-time effort argument for a host.
|
|
179
|
+
*
|
|
180
|
+
* `effortSurface` is the host's negotiated axis value. Only `argv` produces an
|
|
181
|
+
* argument; `none`, `undocumented`, and anything unrecognised produce nothing.
|
|
182
|
+
* Never throws.
|
|
183
|
+
*/
|
|
184
|
+
function renderEffortArgv(host, universalEffort, effortSurface) {
|
|
185
|
+
const empty = { argv: [], value: null, host };
|
|
186
|
+
if (effortSurface !== 'argv')
|
|
187
|
+
return empty;
|
|
188
|
+
// Own-property lookup only. A plain `EFFORT_ARGV[host]` resolves `__proto__`
|
|
189
|
+
// (and `constructor`/`toString`) to inherited members, which are truthy but
|
|
190
|
+
// carry no `clamp`/`render` — a hostile host id would throw instead of
|
|
191
|
+
// degrading. The host id reaches here from a descriptor, i.e. untrusted JSON.
|
|
192
|
+
if (typeof host !== 'string' || !Object.prototype.hasOwnProperty.call(exports.EFFORT_ARGV, host))
|
|
193
|
+
return empty;
|
|
194
|
+
const spec = exports.EFFORT_ARGV[host];
|
|
195
|
+
if (!spec || typeof spec.clamp !== 'function' || typeof spec.render !== 'function')
|
|
196
|
+
return empty;
|
|
197
|
+
if (typeof universalEffort !== 'string' || universalEffort.length === 0)
|
|
198
|
+
return empty;
|
|
199
|
+
const clamped = spec.clamp(universalEffort);
|
|
200
|
+
if (!spec.supported.has(clamped))
|
|
201
|
+
return empty;
|
|
202
|
+
return { argv: spec.render(clamped), value: clamped, host };
|
|
203
|
+
}
|
|
154
204
|
/**
|
|
155
205
|
* Render a universal effort string for a specific runtime.
|
|
156
206
|
*/
|
|
@@ -41,7 +41,12 @@ function _safeStringify(value) {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
/**
|
|
44
|
-
* Determine whether
|
|
44
|
+
* Determine whether observability is opt-in enabled — via the GSD_AUDIT env var
|
|
45
|
+
* or config.audit.enabled. Exported (#2620) so the live dispatch seam can decide
|
|
46
|
+
* whether to inject the reference logger at all: when observability is off we
|
|
47
|
+
* inject nothing (the Hub stays byte-for-byte silent, preserving the default
|
|
48
|
+
* dispatch output contract, incl. --json-errors); when on, the caller injects
|
|
49
|
+
* createDefaultLogger and gets the stderr-on-error line + opt-in file audit.
|
|
45
50
|
*/
|
|
46
51
|
function _isAuditEnabled(config) {
|
|
47
52
|
if (process.env['GSD_AUDIT'] === '1')
|
|
@@ -143,4 +148,4 @@ function createDefaultLogger({ cwd = process.cwd(), config } = {}) {
|
|
|
143
148
|
},
|
|
144
149
|
};
|
|
145
150
|
}
|
|
146
|
-
module.exports = { createDefaultLogger, createNoOpLogger };
|
|
151
|
+
module.exports = { createDefaultLogger, createNoOpLogger, isAuditEnabled: _isAuditEnabled };
|
|
@@ -20,6 +20,12 @@ const command_aliases_cjs_1 = require("./command-aliases.cjs");
|
|
|
20
20
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
21
21
|
const commandRoutingHub = require("./command-routing-hub.cjs");
|
|
22
22
|
const { createHub, ERROR_KINDS, makeInvalidArgs } = commandRoutingHub;
|
|
23
|
+
// #2620 (ADR-0174 §6): inject the reference DispatchLogger on the live phase
|
|
24
|
+
// dispatch path, but only when observability is opt-in enabled; otherwise the
|
|
25
|
+
// Hub stays byte-for-byte silent via its no-op fallback.
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
27
|
+
const observabilityLogger = require("./observability/logger.cjs");
|
|
28
|
+
const { createDefaultLogger, isAuditEnabled } = observabilityLogger;
|
|
23
29
|
// ─── Implementation ───────────────────────────────────────────────────────────
|
|
24
30
|
function routePhaseCommand({ phase, args, cwd, raw, error }) {
|
|
25
31
|
// ── Unsupported subcommands ─────────────────────────────────────────────────
|
|
@@ -229,7 +235,10 @@ function routePhaseCommand({ phase, args, cwd, raw, error }) {
|
|
|
229
235
|
const manifest = { phase: manifestSubcommands };
|
|
230
236
|
// ── Construct hub ──────────────────────────────────────────────────────────
|
|
231
237
|
// #175: Hub is CJS-only — no mode param, no sdkLoader.
|
|
232
|
-
|
|
238
|
+
// #2620: wire the reference logger (ADR-0174 §6) only when observability is
|
|
239
|
+
// opt-in enabled; otherwise leave it unset so the Hub stays byte-for-byte
|
|
240
|
+
// silent via its no-op fallback.
|
|
241
|
+
const hub = createHub({ cjsRegistry, manifest, logger: isAuditEnabled() ? createDefaultLogger({ cwd }) : undefined });
|
|
233
242
|
// ── Dispatch ────────────────────────────────────────────────────────────────
|
|
234
243
|
const result = hub.dispatch({
|
|
235
244
|
family: 'phase',
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Phase Estimation — estimate/actuals schema, smart-zone threshold policy, and
|
|
4
|
+
* estimate-vs-actual calibration.
|
|
5
|
+
*
|
|
6
|
+
* Epic #1952, Phase 1 (#2630). Design lock: docs/adr/2629-phase-effort-estimation-calibration.md.
|
|
7
|
+
*
|
|
8
|
+
* Pure functions only — no I/O, no config reads. Callers supply the budget and
|
|
9
|
+
* the raw calibration document; this module decides policy over them. The CLI
|
|
10
|
+
* seam (gsd-tools) owns reading `.planning/config.json` and
|
|
11
|
+
* `.planning/estimation-calibration.json`.
|
|
12
|
+
*
|
|
13
|
+
* Two properties this module exists to preserve, both from ADR-2629:
|
|
14
|
+
*
|
|
15
|
+
* 1. Every signal is EXOGENOUS. The correction routes on a measured
|
|
16
|
+
* actual/estimate ratio; `confidence` routes on a calibration sample
|
|
17
|
+
* count. Nothing routes on a model's self-assessment. This project
|
|
18
|
+
* measured self-rated confidence and found it weak
|
|
19
|
+
* (gsd-core/references/honest-verifier.md:25-29 — "on a true blind spot it
|
|
20
|
+
* stays confidently wrong"), which is why deriveConfidence() takes a
|
|
21
|
+
* sample count and there is no "how sure are you?" input anywhere here.
|
|
22
|
+
*
|
|
23
|
+
* 2. Estimate and actual share ONE measurement scale — estimateTokens() from
|
|
24
|
+
* prompt-budget. A ratio between two different measurement methods would
|
|
25
|
+
* measure the methods, not the miss. measureTokens() below is the single
|
|
26
|
+
* re-export so no consumer reaches for a second estimator.
|
|
27
|
+
*
|
|
28
|
+
* ADR-457 build-at-publish: source here, compiled to
|
|
29
|
+
* gsd-core/bin/lib/phase-estimation.cjs (gitignored).
|
|
30
|
+
*/
|
|
31
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
+
exports.CALIBRATION_SCHEMA_VERSION = exports.CALIBRATION_FACTOR_MAX = exports.CALIBRATION_FACTOR_MIN = exports.CONFIDENCE_HIGH_MIN_SAMPLES = exports.CONFIDENCE_MED_MIN_SAMPLES = exports.MIN_CALIBRATION_SAMPLES = exports.CONFIDENCE_VALUES = void 0;
|
|
33
|
+
exports.asRawTokens = asRawTokens;
|
|
34
|
+
exports.asCalibratedTokens = asCalibratedTokens;
|
|
35
|
+
exports.measureTokens = measureTokens;
|
|
36
|
+
exports.deriveConfidence = deriveConfidence;
|
|
37
|
+
exports.classifyAgainstBudget = classifyAgainstBudget;
|
|
38
|
+
exports.computeCalibration = computeCalibration;
|
|
39
|
+
exports.applyCalibration = applyCalibration;
|
|
40
|
+
exports.extractFrontmatterBlock = extractFrontmatterBlock;
|
|
41
|
+
exports.parseEstimate = parseEstimate;
|
|
42
|
+
exports.parseActuals = parseActuals;
|
|
43
|
+
exports.renderEstimate = renderEstimate;
|
|
44
|
+
exports.calibrationBasis = calibrationBasis;
|
|
45
|
+
exports.renderActuals = renderActuals;
|
|
46
|
+
exports.parseCalibrationDocument = parseCalibrationDocument;
|
|
47
|
+
exports.renderCalibrationDocument = renderCalibrationDocument;
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- prompt-budget.cjs is an export= CommonJS module
|
|
49
|
+
const promptBudget = require("./prompt-budget.cjs");
|
|
50
|
+
const { estimateTokens } = promptBudget;
|
|
51
|
+
/**
|
|
52
|
+
* Assert that a bare number is an UNCORRECTED projection.
|
|
53
|
+
*
|
|
54
|
+
* Call this only where a number crosses a trust boundary carrying a basis the
|
|
55
|
+
* type system cannot see — argv, disk frontmatter, a persisted document. The
|
|
56
|
+
* parameter type refuses a `CalibratedTokens`, so a corrected figure cannot be
|
|
57
|
+
* laundered back into the basis; without that the brand would be decorative and
|
|
58
|
+
* #2632 would be one keystroke away again.
|
|
59
|
+
*/
|
|
60
|
+
function asRawTokens(tokens) {
|
|
61
|
+
return tokens;
|
|
62
|
+
}
|
|
63
|
+
/** Assert that a bare number already has the correction applied. Refuses a `RawTokens`. */
|
|
64
|
+
function asCalibratedTokens(tokens) {
|
|
65
|
+
return tokens;
|
|
66
|
+
}
|
|
67
|
+
exports.CONFIDENCE_VALUES = Object.freeze(['low', 'med', 'high']);
|
|
68
|
+
/** Below this many calibration samples, no correction is applied (ADR-2629 Decision 4). */
|
|
69
|
+
exports.MIN_CALIBRATION_SAMPLES = 3;
|
|
70
|
+
/** Sample-count thresholds for derived confidence (ADR-2629 Decision 1). */
|
|
71
|
+
exports.CONFIDENCE_MED_MIN_SAMPLES = 3;
|
|
72
|
+
exports.CONFIDENCE_HIGH_MIN_SAMPLES = 6;
|
|
73
|
+
/** Correction-factor clamp. Outside this range the estimator is wrong in kind, not degree. */
|
|
74
|
+
exports.CALIBRATION_FACTOR_MIN = 0.5;
|
|
75
|
+
exports.CALIBRATION_FACTOR_MAX = 3.0;
|
|
76
|
+
/** Schema version for the persisted calibration document. */
|
|
77
|
+
exports.CALIBRATION_SCHEMA_VERSION = 1;
|
|
78
|
+
/**
|
|
79
|
+
* A positive, finite, safe integer. Rejects NaN, Infinity, negatives, zero,
|
|
80
|
+
* non-integers, and anything past MAX_SAFE_INTEGER (where integer arithmetic
|
|
81
|
+
* silently stops being exact).
|
|
82
|
+
*/
|
|
83
|
+
function isPositiveInt(value) {
|
|
84
|
+
return typeof value === 'number'
|
|
85
|
+
&& Number.isSafeInteger(value)
|
|
86
|
+
&& value > 0;
|
|
87
|
+
}
|
|
88
|
+
function isPositiveFinite(value) {
|
|
89
|
+
return typeof value === 'number' && Number.isFinite(value) && value > 0;
|
|
90
|
+
}
|
|
91
|
+
function isConfidence(value) {
|
|
92
|
+
return typeof value === 'string' && exports.CONFIDENCE_VALUES.includes(value);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* A usable calibration sample: both sides present, positive, and finite.
|
|
96
|
+
* A zero or negative estimate would divide to Infinity or flip the ratio's
|
|
97
|
+
* sign, so those are dropped rather than coerced.
|
|
98
|
+
*/
|
|
99
|
+
function isCalibrationSample(value) {
|
|
100
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
101
|
+
return false;
|
|
102
|
+
const record = value;
|
|
103
|
+
// The RawTokens brand on estimateTokens is asserted here, at the disk trust
|
|
104
|
+
// boundary — a persisted sample's basis is a fact about the writer, and the
|
|
105
|
+
// only writers are collectCalibrationSamples() (which reads it through
|
|
106
|
+
// calibrationBasis()) and this module's own renderCalibrationDocument().
|
|
107
|
+
return isPositiveFinite(record['estimateTokens']) && isPositiveFinite(record['actualTokens']);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Measure text on the canonical scale. The ONE estimator both the estimate and
|
|
111
|
+
* the actuals must use — see property 2 in the module header.
|
|
112
|
+
*/
|
|
113
|
+
function measureTokens(text) {
|
|
114
|
+
return estimateTokens(text);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Derive confidence from how much measured history backs the estimate.
|
|
118
|
+
*
|
|
119
|
+
* Exogenous by construction: the input is a count, not a judgment. A non-integer
|
|
120
|
+
* or negative count degrades to 'low' rather than throwing — an unusable history
|
|
121
|
+
* is exactly the low-confidence case.
|
|
122
|
+
*/
|
|
123
|
+
function deriveConfidence(sampleCount) {
|
|
124
|
+
if (typeof sampleCount !== 'number' || !Number.isFinite(sampleCount) || sampleCount < 0)
|
|
125
|
+
return 'low';
|
|
126
|
+
if (sampleCount >= exports.CONFIDENCE_HIGH_MIN_SAMPLES)
|
|
127
|
+
return 'high';
|
|
128
|
+
if (sampleCount >= exports.CONFIDENCE_MED_MIN_SAMPLES)
|
|
129
|
+
return 'med';
|
|
130
|
+
return 'low';
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Classify an estimate against the smart-zone budget.
|
|
134
|
+
*
|
|
135
|
+
* Boundary contract (ADR-2629 Decision 3 + RULESET.TESTS.boundary-coverage.fixtures):
|
|
136
|
+
* budget-1 → under, budget → under, budget+1 → over. The comparison is strictly
|
|
137
|
+
* greater-than, so landing exactly on the budget is not a violation.
|
|
138
|
+
*
|
|
139
|
+
* An unusable budget (hand-edited config, missing key) never fabricates a
|
|
140
|
+
* violation: it reports budgetValid=false and overBudget=false, so a broken
|
|
141
|
+
* config cannot spam split recommendations.
|
|
142
|
+
*/
|
|
143
|
+
function classifyAgainstBudget(estimate, budget) {
|
|
144
|
+
// Kept for untyped `.cjs` callers — see the note in applyCalibration. A
|
|
145
|
+
// hand-edited config reaches `budget` as anything at runtime regardless of
|
|
146
|
+
// what the TypeScript signature promises.
|
|
147
|
+
if (!isPositiveFinite(budget) || !isPositiveFinite(estimate)) {
|
|
148
|
+
return { overBudget: false, ratio: 0, recommendation: null, budgetValid: isPositiveFinite(budget) };
|
|
149
|
+
}
|
|
150
|
+
const ratio = estimate / budget;
|
|
151
|
+
if (estimate <= budget) {
|
|
152
|
+
return { overBudget: false, ratio, recommendation: null, budgetValid: true };
|
|
153
|
+
}
|
|
154
|
+
const slices = Math.ceil(ratio);
|
|
155
|
+
return {
|
|
156
|
+
overBudget: true,
|
|
157
|
+
ratio,
|
|
158
|
+
recommendation: `Estimated ${estimate} tokens exceeds the ${budget}-token smart-zone budget `
|
|
159
|
+
+ `(${ratio.toFixed(2)}x). Consider splitting this phase into about ${slices} `
|
|
160
|
+
+ `slices — a tracer plus ${slices - 1} expansion slice(s) — so each runs inside the budget.`,
|
|
161
|
+
budgetValid: true,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/** Median of a non-empty numeric array. Caller guarantees non-empty. */
|
|
165
|
+
function median(sorted) {
|
|
166
|
+
const mid = Math.floor(sorted.length / 2);
|
|
167
|
+
if (sorted.length % 2 === 1)
|
|
168
|
+
return sorted[mid];
|
|
169
|
+
return (sorted[mid - 1] + sorted[mid]) / 2;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Compute the correction factor from estimate/actual history.
|
|
173
|
+
*
|
|
174
|
+
* Median, not mean — one pathological phase (an aborted run, a mass rename)
|
|
175
|
+
* must not swing every later projection. Clamped, because a ratio outside
|
|
176
|
+
* [0.5, 3.0] means the estimator is wrong in kind and amplifying it would make
|
|
177
|
+
* the next estimate worse, not better.
|
|
178
|
+
*
|
|
179
|
+
* Samples missing either side, or carrying a non-positive/non-finite value, are
|
|
180
|
+
* dropped rather than coerced — a zero estimate would divide to Infinity.
|
|
181
|
+
*/
|
|
182
|
+
function computeCalibration(samples) {
|
|
183
|
+
const candidates = Array.isArray(samples) ? samples : [];
|
|
184
|
+
const usable = candidates.filter(isCalibrationSample);
|
|
185
|
+
const sampleCount = usable.length;
|
|
186
|
+
const confidence = deriveConfidence(sampleCount);
|
|
187
|
+
if (sampleCount < exports.MIN_CALIBRATION_SAMPLES) {
|
|
188
|
+
return { factor: 1, sampleCount, applied: false, confidence, clamped: false };
|
|
189
|
+
}
|
|
190
|
+
const ratios = usable.map((s) => s.actualTokens / s.estimateTokens).sort((a, b) => a - b);
|
|
191
|
+
const raw = median(ratios);
|
|
192
|
+
const factor = Math.min(exports.CALIBRATION_FACTOR_MAX, Math.max(exports.CALIBRATION_FACTOR_MIN, raw));
|
|
193
|
+
return { factor, sampleCount, applied: true, confidence, clamped: factor !== raw };
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Apply a correction factor to a raw estimate. Rounds to an integer because
|
|
197
|
+
* `estimate.tokens` is an integer field; floors at 1 so a heavy shrink factor
|
|
198
|
+
* can never produce a zero-token estimate.
|
|
199
|
+
*/
|
|
200
|
+
function applyCalibration(rawTokens, factor) {
|
|
201
|
+
// These two guards look dead to the type-checker and are not: this module is
|
|
202
|
+
// compiled to `.cjs` and consumed by untyped callers (gsd-tools.cjs, the test
|
|
203
|
+
// suite), which reach it with NaN, null, 0 and worse. The brands are a
|
|
204
|
+
// compile-time contract for TypeScript callers; validation is what defends
|
|
205
|
+
// everyone else. Do not delete either one because the parameter is now typed.
|
|
206
|
+
if (!isPositiveFinite(rawTokens))
|
|
207
|
+
return asCalibratedTokens(0);
|
|
208
|
+
if (!isPositiveFinite(factor))
|
|
209
|
+
return asCalibratedTokens(Math.max(1, Math.round(rawTokens)));
|
|
210
|
+
// Bound the product: an inexact float past MAX_SAFE_INTEGER would masquerade
|
|
211
|
+
// as an integer token count. Unreachable through today's CLI (which is
|
|
212
|
+
// safe-integer bounded) but the function is exported and must not depend on
|
|
213
|
+
// its caller for that guarantee.
|
|
214
|
+
const scaled = Math.round(rawTokens * factor);
|
|
215
|
+
return asCalibratedTokens(Math.min(Number.MAX_SAFE_INTEGER, Math.max(1, scaled)));
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Extract a two-space-indented scalar block (`estimate:` / `actuals:`) out of a
|
|
219
|
+
* document's leading YAML frontmatter.
|
|
220
|
+
*
|
|
221
|
+
* Hand-rolled because gsd-core ships no external dependencies (CONTRIBUTING.md
|
|
222
|
+
* "No external dependencies in core") — js-yaml is a devDependency and is not
|
|
223
|
+
* available at runtime. Scope is deliberately narrow: the leading `---` block
|
|
224
|
+
* only, so a `estimate:` line inside a fenced code block in the body cannot be
|
|
225
|
+
* mistaken for frontmatter (the DEFECT.FRONTMATTER-SCALAR-BROAD-GREP class).
|
|
226
|
+
*
|
|
227
|
+
* Numeric-looking values are returned as numbers so parseEstimate/parseActuals
|
|
228
|
+
* see the types they validate; everything else stays a string.
|
|
229
|
+
*/
|
|
230
|
+
function extractFrontmatterBlock(text, key) {
|
|
231
|
+
if (typeof text !== 'string')
|
|
232
|
+
return null;
|
|
233
|
+
// Anchor at byte 0 — CRLF-tolerant.
|
|
234
|
+
const fm = /^---\r?\n([\s\S]*?)\r?\n---\r?(?:\n|$)/.exec(text);
|
|
235
|
+
if (fm === null)
|
|
236
|
+
return null;
|
|
237
|
+
const lines = fm[1].split(/\r?\n/);
|
|
238
|
+
const startIdx = lines.findIndex((l) => l === `${key}:` || l.startsWith(`${key}:`));
|
|
239
|
+
if (startIdx === -1)
|
|
240
|
+
return null;
|
|
241
|
+
const out = Object.create(null);
|
|
242
|
+
for (let i = startIdx + 1; i < lines.length; i += 1) {
|
|
243
|
+
const line = lines[i];
|
|
244
|
+
if (!/^\s/.test(line))
|
|
245
|
+
break; // dedent ends the block
|
|
246
|
+
const m = /^\s+([A-Za-z_][\w-]*):\s*(.*)$/.exec(line);
|
|
247
|
+
if (m === null)
|
|
248
|
+
continue;
|
|
249
|
+
const rawValue = m[2].replace(/\s+#.*$/, '').trim();
|
|
250
|
+
if (rawValue === '')
|
|
251
|
+
continue;
|
|
252
|
+
const asNumber = Number(rawValue);
|
|
253
|
+
out[m[1]] = /^-?\d+(?:\.\d+)?$/.test(rawValue) && Number.isFinite(asNumber)
|
|
254
|
+
? asNumber
|
|
255
|
+
: rawValue.replace(/^['"]|['"]$/g, '');
|
|
256
|
+
}
|
|
257
|
+
return Object.keys(out).length > 0 ? { ...out } : null;
|
|
258
|
+
}
|
|
259
|
+
/** Pull the `estimate:` mapping out of an already-parsed frontmatter object. */
|
|
260
|
+
function estimateBlockOf(input) {
|
|
261
|
+
if (input === null || typeof input !== 'object')
|
|
262
|
+
return null;
|
|
263
|
+
const record = input;
|
|
264
|
+
return Object.prototype.hasOwnProperty.call(record, 'estimate') ? record['estimate'] : record;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Parse an estimate block. Returns null for anything that is not a complete,
|
|
268
|
+
* well-typed estimate — a partial block is not a usable estimate, and silently
|
|
269
|
+
* defaulting a missing field would fabricate data the planner never produced.
|
|
270
|
+
*
|
|
271
|
+
* Accepts either the whole frontmatter object (`{estimate: {...}}`) or the
|
|
272
|
+
* estimate mapping itself, so callers need not unwrap.
|
|
273
|
+
*/
|
|
274
|
+
function parseEstimate(input) {
|
|
275
|
+
const block = estimateBlockOf(input);
|
|
276
|
+
if (block === null || typeof block !== 'object' || Array.isArray(block))
|
|
277
|
+
return null;
|
|
278
|
+
const record = block;
|
|
279
|
+
const tokens = record['tokens'];
|
|
280
|
+
const tasks = record['tasks'];
|
|
281
|
+
const confidence = record['confidence'];
|
|
282
|
+
if (!isPositiveInt(tokens) || !isPositiveInt(tasks) || !isConfidence(confidence))
|
|
283
|
+
return null;
|
|
284
|
+
// The frontmatter trust boundary: `tokens` is calibrated-at-emission and
|
|
285
|
+
// `raw_tokens` is the uncorrected projection (ADR-2629 Decision 1/4), so this
|
|
286
|
+
// is where each figure's basis becomes a type rather than a field name.
|
|
287
|
+
const rawTokens = record['raw_tokens'];
|
|
288
|
+
return isPositiveInt(rawTokens)
|
|
289
|
+
? { tokens: asCalibratedTokens(tokens), tasks, confidence, rawTokens: asRawTokens(rawTokens) }
|
|
290
|
+
: { tokens: asCalibratedTokens(tokens), tasks, confidence };
|
|
291
|
+
}
|
|
292
|
+
/** Pull the `actuals:` mapping out of an already-parsed frontmatter object. */
|
|
293
|
+
function actualsBlockOf(input) {
|
|
294
|
+
if (input === null || typeof input !== 'object')
|
|
295
|
+
return null;
|
|
296
|
+
const record = input;
|
|
297
|
+
return Object.prototype.hasOwnProperty.call(record, 'actuals') ? record['actuals'] : record;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Parse an actuals block. `commits` may be 0 — a phase can legitimately record
|
|
301
|
+
* zero commits — so it is validated as a non-negative integer while tokens and
|
|
302
|
+
* tasks stay strictly positive.
|
|
303
|
+
*/
|
|
304
|
+
function parseActuals(input) {
|
|
305
|
+
const block = actualsBlockOf(input);
|
|
306
|
+
if (block === null || typeof block !== 'object' || Array.isArray(block))
|
|
307
|
+
return null;
|
|
308
|
+
const record = block;
|
|
309
|
+
const tokens = record['tokens'];
|
|
310
|
+
const tasks = record['tasks'];
|
|
311
|
+
const commits = record['commits'];
|
|
312
|
+
if (!isPositiveInt(tokens) || !isPositiveInt(tasks))
|
|
313
|
+
return null;
|
|
314
|
+
if (typeof commits !== 'number' || !Number.isSafeInteger(commits) || commits < 0)
|
|
315
|
+
return null;
|
|
316
|
+
return { tokens, tasks, commits };
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Render an estimate as the YAML block that lands in PLAN.md frontmatter.
|
|
320
|
+
* Inverse of parseEstimate over the same value domain — the bijection the
|
|
321
|
+
* property test pins.
|
|
322
|
+
*/
|
|
323
|
+
function renderEstimate(estimate) {
|
|
324
|
+
const lines = [
|
|
325
|
+
'estimate:',
|
|
326
|
+
` tokens: ${estimate.tokens}`,
|
|
327
|
+
];
|
|
328
|
+
if (isPositiveInt(estimate.rawTokens))
|
|
329
|
+
lines.push(` raw_tokens: ${estimate.rawTokens}`);
|
|
330
|
+
lines.push(` tasks: ${estimate.tasks}`, ` confidence: ${estimate.confidence}`);
|
|
331
|
+
return lines.join('\n');
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* The figure calibration must measure against: the uncalibrated projection when
|
|
335
|
+
* the plan recorded one, else the stored value (pre-#2632 plans, where the two
|
|
336
|
+
* were the same because no factor had yet been applied).
|
|
337
|
+
*/
|
|
338
|
+
function calibrationBasis(estimate) {
|
|
339
|
+
if (isPositiveInt(estimate.rawTokens))
|
|
340
|
+
return estimate.rawTokens;
|
|
341
|
+
// THE one legitimate crossover in this module, and the reason asRawTokens()
|
|
342
|
+
// refuses a CalibratedTokens rather than being permissive: on a plan written
|
|
343
|
+
// before #2632 no factor had been applied yet, so `tokens` IS the raw
|
|
344
|
+
// projection. Deliberately an explicit assertion so it stays a single
|
|
345
|
+
// auditable line instead of a hole in the brand.
|
|
346
|
+
return estimate.tokens;
|
|
347
|
+
}
|
|
348
|
+
/** Render an actuals block for SUMMARY.md frontmatter. Inverse of parseActuals. */
|
|
349
|
+
function renderActuals(actuals) {
|
|
350
|
+
return [
|
|
351
|
+
'actuals:',
|
|
352
|
+
` tokens: ${actuals.tokens}`,
|
|
353
|
+
` tasks: ${actuals.tasks}`,
|
|
354
|
+
` commits: ${actuals.commits}`,
|
|
355
|
+
].join('\n');
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Parse the persisted calibration document.
|
|
359
|
+
*
|
|
360
|
+
* This is a trust boundary: the file is on disk, may be hand-edited, and its
|
|
361
|
+
* contents steer planning output. Every failure mode degrades to an empty
|
|
362
|
+
* sample set rather than throwing or partially trusting — malformed JSON, a
|
|
363
|
+
* non-object root, a missing/!== current schema_version, a non-array samples
|
|
364
|
+
* field, or individual malformed samples.
|
|
365
|
+
*
|
|
366
|
+
* A schema_version we do not recognize is refused outright rather than
|
|
367
|
+
* best-effort read: a future writer may change the ratio's meaning, and
|
|
368
|
+
* misreading it would silently corrupt every subsequent estimate.
|
|
369
|
+
*/
|
|
370
|
+
function parseCalibrationDocument(raw) {
|
|
371
|
+
if (typeof raw !== 'string' || raw.trim() === '')
|
|
372
|
+
return [];
|
|
373
|
+
let parsed;
|
|
374
|
+
try {
|
|
375
|
+
parsed = JSON.parse(raw);
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
return [];
|
|
379
|
+
}
|
|
380
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
381
|
+
return [];
|
|
382
|
+
const doc = parsed;
|
|
383
|
+
if (doc['schema_version'] !== exports.CALIBRATION_SCHEMA_VERSION)
|
|
384
|
+
return [];
|
|
385
|
+
if (!Array.isArray(doc['samples']))
|
|
386
|
+
return [];
|
|
387
|
+
// Rebuild each sample from its two known fields rather than passing the
|
|
388
|
+
// parsed object through — a hostile document cannot smuggle extra keys
|
|
389
|
+
// (or a __proto__ payload) into anything downstream.
|
|
390
|
+
return doc['samples']
|
|
391
|
+
.filter(isCalibrationSample)
|
|
392
|
+
.map((s) => ({ estimateTokens: s.estimateTokens, actualTokens: s.actualTokens }));
|
|
393
|
+
}
|
|
394
|
+
/** Serialize a calibration document. Inverse of parseCalibrationDocument. */
|
|
395
|
+
function renderCalibrationDocument(samples) {
|
|
396
|
+
const doc = { schema_version: exports.CALIBRATION_SCHEMA_VERSION, samples };
|
|
397
|
+
return `${JSON.stringify(doc, null, 2)}\n`;
|
|
398
|
+
}
|