@harness-engineering/intelligence 0.4.3 → 0.6.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/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as _harness_engineering_types from '@harness-engineering/types';
2
- import { Issue, ConcernSignal, ScopeTier, EscalationConfig } from '@harness-engineering/types';
2
+ import { Issue, ConcernSignal, ScopeTier, EscalationConfig, ComplexityLevel, ComplexityVerdict, CapabilityTier, RoutingRisk, RoutingPolicy } from '@harness-engineering/types';
3
3
  import { z } from 'zod';
4
4
  import { GraphStore } from '@harness-engineering/graph';
5
5
 
@@ -406,6 +406,14 @@ interface OpenAICompatibleProviderOptions {
406
406
  baseUrl: string;
407
407
  /** Default model name (e.g., 'deepseek-coder-v2'). */
408
408
  defaultModel?: string;
409
+ /**
410
+ * Consumption Phase 1 (T3): live model resolver, read at request time. When
411
+ * provided and it returns a non-empty name, that name is used in preference to
412
+ * `defaultModel` so a pool install/swap is consumed by analysis without a
413
+ * restart. `request.model` (an explicit per-call override) still wins; a
414
+ * null/undefined/empty return falls through to `defaultModel`.
415
+ */
416
+ getModel?: () => string | null | undefined;
409
417
  /** Request timeout in ms (default: 90000). */
410
418
  timeoutMs?: number;
411
419
  /**
@@ -430,6 +438,7 @@ interface OpenAICompatibleProviderOptions {
430
438
  declare class OpenAICompatibleAnalysisProvider implements AnalysisProvider {
431
439
  private readonly client;
432
440
  private readonly defaultModel;
441
+ private readonly getModel?;
433
442
  private readonly promptSuffix;
434
443
  private readonly jsonMode;
435
444
  constructor(options: OpenAICompatibleProviderOptions);
@@ -1333,4 +1342,153 @@ declare function saveProfiles(projectRoot: string, store: ProfileStore): void;
1333
1342
  */
1334
1343
  declare function refreshProfiles(projectRoot: string, graphStore: GraphStore, opts?: Omit<SpecializationOptions, 'persona'>): ProfileStore;
1335
1344
 
1336
- export { ACCEPTANCE_EVAL_SYSTEM_PROMPT, type AcceptanceEvalInput, AcceptanceEvaluator, type AcceptanceEvaluatorOptions, type AcceptanceVerdict, type AffectedSystem, type AnalysisProvider, type AnalysisRequest, type AnalysisResponse, AnthropicAnalysisProvider, type Authority, type BlastRadius, type BlindSpot, type CanaryAdapter, type CanaryDegradeReason, type CanaryExec, type CanaryFinding, type CanaryProbe, ClaudeCliAnalysisProvider, type ComplexityScore, type Confidence, type EnrichedSpec, type ExecutionOutcome, ExecutionOutcomeConnector, type ExpertiseLevel, type Finding, type FrameworkRecommendation, type GitHubIssue, GraphValidator, IntelligencePipeline, type JiraIssue, type JudgedAgainst, type LinearIssue, type LlmAcceptanceVerdict, type LlmVerdict, type ManualInput, type Measurability, OUTCOME_EVAL_SYSTEM_PROMPT, OpenAICompatibleAnalysisProvider, type OutcomeEvalInput, OutcomeEvaluator, type OutcomeEvaluatorOptions, type OutcomeIngestResult, type OutcomeVerdict, type PersonaEffectivenessScore, type PersonaRecommendation, PeslSimulator, type PreprocessResult, type ProfileStore, type RawWorkItem, type ResolvedSection, type SimulationResult, type SpecializationEntry, type SpecializationProfile, type SpecializationScore, type TaskType, type TemporalConfig, type Verdict, type WeightedRecommendation, acceptanceVerdictSchema, buildUserPrompt as buildAcceptanceUserPrompt, buildSpecializationProfile, buildUserPrompt$1 as buildUserPrompt, computeExpertiseLevel, computeHistoricalComplexity, computePersonaEffectiveness, computeSemanticComplexity, computeSpecialization, computeStructuralComplexity, createCanaryAdapter, decayWeight, deriveAcceptanceAuthority, deriveAuthority, detectBlindSpots, enrich, findingSchema, githubToRawWorkItem, jiraToRawWorkItem, linearToRawWorkItem, loadProfiles, manualToRawWorkItem, recommendPersona, refreshProfiles, resolveSection, runGraphOnlyChecks, runLlmSimulation, saveProfiles, score as scoreCML, scoreToConcernSignals, temporalSuccessRate, toRawWorkItem, verdictSchema, weightedRecommendPersona };
1345
+ /** Which signal set is available at this invocation phase (S3-001). */
1346
+ type Phase = 'pre-diff' | 'post-diff';
1347
+ /** Raw signals gathered for the static pass. Diff-based fields are undefined pre-diff. */
1348
+ interface ComplexitySignals {
1349
+ /** Files touched by the diff/target. Undefined pre-diff. */
1350
+ filesTouched?: number;
1351
+ /** Distinct architectural layers touched. Undefined pre-diff. */
1352
+ layersTouched?: number;
1353
+ /** compute_blast_radius result. Undefined pre-diff. */
1354
+ blastRadius?: number;
1355
+ /** hotspot × churn heat. Undefined pre-diff. */
1356
+ hotspotChurn?: number;
1357
+ /** Text-only fallback signals (always available). */
1358
+ descriptionLength: number;
1359
+ specExists: boolean;
1360
+ acceptanceMeasurable: boolean;
1361
+ }
1362
+ /** Provisional static verdict before any LLM tie-break. */
1363
+ interface StaticVerdict {
1364
+ level: ComplexityLevel;
1365
+ confidence: 'high' | 'medium' | 'low';
1366
+ /** Serialized subset of signals for the ComplexityVerdict.signals map. */
1367
+ signals: Record<string, number | boolean | string>;
1368
+ }
1369
+
1370
+ /** Inputs to the cheap-first complexity cascade (D4). */
1371
+ interface ClassifyInput {
1372
+ signals: ComplexitySignals;
1373
+ phase: Phase;
1374
+ /** Whether the invocation's risk band is high (drives standard-tier escalation). */
1375
+ riskHigh: boolean;
1376
+ /** Prompt handed to the LLM tie-break / escalation. */
1377
+ prompt: string;
1378
+ }
1379
+ /**
1380
+ * D4 cascade: static pass → (if low confidence) fast-tier tie-break → (if still
1381
+ * low AND risk high) standard-tier escalation. Emits a `ComplexityVerdict` whose
1382
+ * `source` records which stage produced it. The static-only path resolves
1383
+ * without any LLM call ("never pay strong to route"); when `provider` is absent
1384
+ * the classifier stays fully offline and returns the static verdict.
1385
+ *
1386
+ * The LLM only sets `level`/`confidence`; the tier is always TS-derived (D3).
1387
+ */
1388
+ declare function classify(input: ClassifyInput, provider?: AnalysisProvider, models?: {
1389
+ fast?: string;
1390
+ standard?: string;
1391
+ }): Promise<ComplexityVerdict>;
1392
+
1393
+ /**
1394
+ * Documented seed weights for the free static pass (D4a). Each raw signal is
1395
+ * normalized against a reference maximum, multiplied by its weight, and the
1396
+ * weighted contributions are summed and re-normalized to a [0,1] score.
1397
+ *
1398
+ * Diff-based signals dominate post-diff; text-only signals carry the pre-diff
1399
+ * pass. Weights are tunable (DEFERRABLE per plan) — this is a documented seed.
1400
+ */
1401
+ declare const STATIC_WEIGHTS: {
1402
+ readonly filesTouched: 0.25;
1403
+ readonly layersTouched: 0.2;
1404
+ readonly blastRadius: 0.3;
1405
+ readonly hotspotChurn: 0.1;
1406
+ /** Text-only signals — the only inputs available pre-diff. */
1407
+ readonly descriptionLength: 0.1;
1408
+ /** A present spec / measurable acceptance *lowers* complexity (well-scoped). */
1409
+ readonly specExists: 0.025;
1410
+ readonly acceptanceMeasurable: 0.025;
1411
+ };
1412
+ /**
1413
+ * D4a: free static pass. Phase-aware (S3-001): pre-diff uses text-only signals
1414
+ * and caps confidence at `medium`; post-diff uses the full signal set. Pure —
1415
+ * never calls an LLM. `source` is stamped by the classifier, not here.
1416
+ */
1417
+ declare function runStaticPass(signals: ComplexitySignals, phase: Phase): StaticVerdict;
1418
+
1419
+ interface TiebreakResult {
1420
+ level: ComplexityLevel;
1421
+ confidence: 'high' | 'medium' | 'low';
1422
+ }
1423
+ /** D4b: fast-tier structured tie-break. Never sets a tier; falls back conservatively on error. */
1424
+ declare function llmTiebreak(provider: AnalysisProvider, prompt: string, fastModel?: string): Promise<TiebreakResult>;
1425
+
1426
+ declare const TIER_RANK: {
1427
+ fast: number;
1428
+ standard: number;
1429
+ strong: number;
1430
+ };
1431
+ declare const RANK_TIER: readonly ["fast", "standard", "strong"];
1432
+ /**
1433
+ * Documented seed threshold for the D5 high-blast veto. The spec lists
1434
+ * sensitive-path / core|types layer / public API explicitly but pins no numeric
1435
+ * blast threshold, so this is a plan-chosen seed — overridable later.
1436
+ */
1437
+ declare const SENSITIVE_BLAST_THRESHOLD = 25;
1438
+ /**
1439
+ * D5: any sensitive-path / core|types layer / public API / high blast → force
1440
+ * `strong`, regardless of complexity (SC5). This is a HARD floor; downstream
1441
+ * budget clamping (D8) must not undercut it.
1442
+ */
1443
+ declare function blastRadiusVeto(risk?: RoutingRisk): boolean;
1444
+ /**
1445
+ * Pre-budget tier resolution (Task 7): skillTierOverride → matrix → D5 veto →
1446
+ * low-confidence bump. Pure; the LLM never influences it (mirrors
1447
+ * outcome-eval/authority.ts). Task 8's `deriveRequiredTier` calls this then
1448
+ * applies the D8 budget clamp and D10 escalation floor.
1449
+ *
1450
+ * SC6: a `low`-confidence verdict degrades the tier UP one step (never below the
1451
+ * matrix default and never to a cheaper tier) — uncertainty routes to a more
1452
+ * capable model, never a Tier-A cheap one.
1453
+ */
1454
+ declare function baseTier(complexity: ComplexityVerdict, risk: RoutingRisk | undefined, policy: RoutingPolicy, skillKey?: string): CapabilityTier;
1455
+ /** Default budget degrade threshold (% of cap) when policy omits one (D8). */
1456
+ declare const DEFAULT_DEGRADE_AT_PCT = 90;
1457
+ /**
1458
+ * D8: budget-pressure tier clamp. Two thresholds:
1459
+ *
1460
+ * - **Soft (`degradeAtPct`, default 90%):** clamp the tier DOWN one step
1461
+ * (`strong→standard→fast`, `fast→fast`) — a lagging degrade signal.
1462
+ * - **Hard (100% of `capUsd`):** for `degrade`/`pause` policies, force the tier
1463
+ * all the way to `fast` — the strongest sound floor on the routing DECISION
1464
+ * (it is not an admission gate; the monotonic accumulator lags under
1465
+ * concurrency, so it cannot prevent overshoot, only route the cheapest tier
1466
+ * once the read crosses the cap). `human` mode is handled one layer up in
1467
+ * `AdaptiveRouter.route()`, which surfaces the unit to a steward instead of
1468
+ * routing; here it falls through to the soft one-step degrade.
1469
+ *
1470
+ * The D5 blast-radius veto is a HARD `strong` floor the clamp must NOT undercut
1471
+ * (SC5 says sensitive-path / core|types / publicApi force `strong` "regardless"),
1472
+ * so a vetoed request stays `strong` even at the hard cap — the veto guard sits
1473
+ * ABOVE both the hard-floor and the soft-step branches. No budget block ⇒ no clamp.
1474
+ */
1475
+ declare function applyBudgetClamp(tier: CapabilityTier, risk: RoutingRisk | undefined, policy: RoutingPolicy, spend: {
1476
+ spentUsd: number;
1477
+ }): CapabilityTier;
1478
+ /**
1479
+ * Pure (complexity × risk × policy × spend × floor) → required tier.
1480
+ *
1481
+ * `baseTier` resolves override → matrix → D5 veto → low-confidence bump; the D8
1482
+ * budget clamp lowers one step under budget pressure (never below the veto
1483
+ * floor); the D10 escalation floor raises the result but never lowers it. The
1484
+ * LLM never influences this — mirrors outcome-eval/authority.ts (TS-derived
1485
+ * authority). Referentially transparent; never mutates `policy`.
1486
+ */
1487
+ declare function deriveRequiredTier(complexity: ComplexityVerdict, risk: RoutingRisk | undefined, policy: RoutingPolicy, spend: {
1488
+ spentUsd: number;
1489
+ }, escalationFloor: CapabilityTier, skillKey?: string): CapabilityTier;
1490
+
1491
+ /** Flatten signals into the ComplexityVerdict.signals map, dropping undefined fields. */
1492
+ declare function serializeSignals(s: ComplexitySignals): Record<string, number | boolean | string>;
1493
+
1494
+ export { ACCEPTANCE_EVAL_SYSTEM_PROMPT, type AcceptanceEvalInput, AcceptanceEvaluator, type AcceptanceEvaluatorOptions, type AcceptanceVerdict, type AffectedSystem, type AnalysisProvider, type AnalysisRequest, type AnalysisResponse, AnthropicAnalysisProvider, type Authority, type BlastRadius, type BlindSpot, type CanaryAdapter, type CanaryDegradeReason, type CanaryExec, type CanaryFinding, type CanaryProbe, type ClassifyInput, ClaudeCliAnalysisProvider, type ComplexityScore, type ComplexitySignals, type Confidence, DEFAULT_DEGRADE_AT_PCT, type EnrichedSpec, type ExecutionOutcome, ExecutionOutcomeConnector, type ExpertiseLevel, type Finding, type FrameworkRecommendation, type GitHubIssue, GraphValidator, IntelligencePipeline, type JiraIssue, type JudgedAgainst, type LinearIssue, type LlmAcceptanceVerdict, type LlmVerdict, type ManualInput, type Measurability, OUTCOME_EVAL_SYSTEM_PROMPT, OpenAICompatibleAnalysisProvider, type OutcomeEvalInput, OutcomeEvaluator, type OutcomeEvaluatorOptions, type OutcomeIngestResult, type OutcomeVerdict, type PersonaEffectivenessScore, type PersonaRecommendation, PeslSimulator, type Phase, type PreprocessResult, type ProfileStore, RANK_TIER, type RawWorkItem, type ResolvedSection, SENSITIVE_BLAST_THRESHOLD, STATIC_WEIGHTS, type SimulationResult, type SpecializationEntry, type SpecializationProfile, type SpecializationScore, type StaticVerdict, TIER_RANK, type TaskType, type TemporalConfig, type TiebreakResult, type Verdict, type WeightedRecommendation, acceptanceVerdictSchema, applyBudgetClamp, baseTier, blastRadiusVeto, buildUserPrompt as buildAcceptanceUserPrompt, buildSpecializationProfile, buildUserPrompt$1 as buildUserPrompt, classify, computeExpertiseLevel, computeHistoricalComplexity, computePersonaEffectiveness, computeSemanticComplexity, computeSpecialization, computeStructuralComplexity, createCanaryAdapter, decayWeight, deriveAcceptanceAuthority, deriveAuthority, deriveRequiredTier, detectBlindSpots, enrich, findingSchema, githubToRawWorkItem, jiraToRawWorkItem, linearToRawWorkItem, llmTiebreak, loadProfiles, manualToRawWorkItem, recommendPersona, refreshProfiles, resolveSection, runGraphOnlyChecks, runLlmSimulation, runStaticPass, saveProfiles, score as scoreCML, scoreToConcernSignals, serializeSignals, temporalSuccessRate, toRawWorkItem, verdictSchema, weightedRecommendPersona };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as _harness_engineering_types from '@harness-engineering/types';
2
- import { Issue, ConcernSignal, ScopeTier, EscalationConfig } from '@harness-engineering/types';
2
+ import { Issue, ConcernSignal, ScopeTier, EscalationConfig, ComplexityLevel, ComplexityVerdict, CapabilityTier, RoutingRisk, RoutingPolicy } from '@harness-engineering/types';
3
3
  import { z } from 'zod';
4
4
  import { GraphStore } from '@harness-engineering/graph';
5
5
 
@@ -406,6 +406,14 @@ interface OpenAICompatibleProviderOptions {
406
406
  baseUrl: string;
407
407
  /** Default model name (e.g., 'deepseek-coder-v2'). */
408
408
  defaultModel?: string;
409
+ /**
410
+ * Consumption Phase 1 (T3): live model resolver, read at request time. When
411
+ * provided and it returns a non-empty name, that name is used in preference to
412
+ * `defaultModel` so a pool install/swap is consumed by analysis without a
413
+ * restart. `request.model` (an explicit per-call override) still wins; a
414
+ * null/undefined/empty return falls through to `defaultModel`.
415
+ */
416
+ getModel?: () => string | null | undefined;
409
417
  /** Request timeout in ms (default: 90000). */
410
418
  timeoutMs?: number;
411
419
  /**
@@ -430,6 +438,7 @@ interface OpenAICompatibleProviderOptions {
430
438
  declare class OpenAICompatibleAnalysisProvider implements AnalysisProvider {
431
439
  private readonly client;
432
440
  private readonly defaultModel;
441
+ private readonly getModel?;
433
442
  private readonly promptSuffix;
434
443
  private readonly jsonMode;
435
444
  constructor(options: OpenAICompatibleProviderOptions);
@@ -1333,4 +1342,153 @@ declare function saveProfiles(projectRoot: string, store: ProfileStore): void;
1333
1342
  */
1334
1343
  declare function refreshProfiles(projectRoot: string, graphStore: GraphStore, opts?: Omit<SpecializationOptions, 'persona'>): ProfileStore;
1335
1344
 
1336
- export { ACCEPTANCE_EVAL_SYSTEM_PROMPT, type AcceptanceEvalInput, AcceptanceEvaluator, type AcceptanceEvaluatorOptions, type AcceptanceVerdict, type AffectedSystem, type AnalysisProvider, type AnalysisRequest, type AnalysisResponse, AnthropicAnalysisProvider, type Authority, type BlastRadius, type BlindSpot, type CanaryAdapter, type CanaryDegradeReason, type CanaryExec, type CanaryFinding, type CanaryProbe, ClaudeCliAnalysisProvider, type ComplexityScore, type Confidence, type EnrichedSpec, type ExecutionOutcome, ExecutionOutcomeConnector, type ExpertiseLevel, type Finding, type FrameworkRecommendation, type GitHubIssue, GraphValidator, IntelligencePipeline, type JiraIssue, type JudgedAgainst, type LinearIssue, type LlmAcceptanceVerdict, type LlmVerdict, type ManualInput, type Measurability, OUTCOME_EVAL_SYSTEM_PROMPT, OpenAICompatibleAnalysisProvider, type OutcomeEvalInput, OutcomeEvaluator, type OutcomeEvaluatorOptions, type OutcomeIngestResult, type OutcomeVerdict, type PersonaEffectivenessScore, type PersonaRecommendation, PeslSimulator, type PreprocessResult, type ProfileStore, type RawWorkItem, type ResolvedSection, type SimulationResult, type SpecializationEntry, type SpecializationProfile, type SpecializationScore, type TaskType, type TemporalConfig, type Verdict, type WeightedRecommendation, acceptanceVerdictSchema, buildUserPrompt as buildAcceptanceUserPrompt, buildSpecializationProfile, buildUserPrompt$1 as buildUserPrompt, computeExpertiseLevel, computeHistoricalComplexity, computePersonaEffectiveness, computeSemanticComplexity, computeSpecialization, computeStructuralComplexity, createCanaryAdapter, decayWeight, deriveAcceptanceAuthority, deriveAuthority, detectBlindSpots, enrich, findingSchema, githubToRawWorkItem, jiraToRawWorkItem, linearToRawWorkItem, loadProfiles, manualToRawWorkItem, recommendPersona, refreshProfiles, resolveSection, runGraphOnlyChecks, runLlmSimulation, saveProfiles, score as scoreCML, scoreToConcernSignals, temporalSuccessRate, toRawWorkItem, verdictSchema, weightedRecommendPersona };
1345
+ /** Which signal set is available at this invocation phase (S3-001). */
1346
+ type Phase = 'pre-diff' | 'post-diff';
1347
+ /** Raw signals gathered for the static pass. Diff-based fields are undefined pre-diff. */
1348
+ interface ComplexitySignals {
1349
+ /** Files touched by the diff/target. Undefined pre-diff. */
1350
+ filesTouched?: number;
1351
+ /** Distinct architectural layers touched. Undefined pre-diff. */
1352
+ layersTouched?: number;
1353
+ /** compute_blast_radius result. Undefined pre-diff. */
1354
+ blastRadius?: number;
1355
+ /** hotspot × churn heat. Undefined pre-diff. */
1356
+ hotspotChurn?: number;
1357
+ /** Text-only fallback signals (always available). */
1358
+ descriptionLength: number;
1359
+ specExists: boolean;
1360
+ acceptanceMeasurable: boolean;
1361
+ }
1362
+ /** Provisional static verdict before any LLM tie-break. */
1363
+ interface StaticVerdict {
1364
+ level: ComplexityLevel;
1365
+ confidence: 'high' | 'medium' | 'low';
1366
+ /** Serialized subset of signals for the ComplexityVerdict.signals map. */
1367
+ signals: Record<string, number | boolean | string>;
1368
+ }
1369
+
1370
+ /** Inputs to the cheap-first complexity cascade (D4). */
1371
+ interface ClassifyInput {
1372
+ signals: ComplexitySignals;
1373
+ phase: Phase;
1374
+ /** Whether the invocation's risk band is high (drives standard-tier escalation). */
1375
+ riskHigh: boolean;
1376
+ /** Prompt handed to the LLM tie-break / escalation. */
1377
+ prompt: string;
1378
+ }
1379
+ /**
1380
+ * D4 cascade: static pass → (if low confidence) fast-tier tie-break → (if still
1381
+ * low AND risk high) standard-tier escalation. Emits a `ComplexityVerdict` whose
1382
+ * `source` records which stage produced it. The static-only path resolves
1383
+ * without any LLM call ("never pay strong to route"); when `provider` is absent
1384
+ * the classifier stays fully offline and returns the static verdict.
1385
+ *
1386
+ * The LLM only sets `level`/`confidence`; the tier is always TS-derived (D3).
1387
+ */
1388
+ declare function classify(input: ClassifyInput, provider?: AnalysisProvider, models?: {
1389
+ fast?: string;
1390
+ standard?: string;
1391
+ }): Promise<ComplexityVerdict>;
1392
+
1393
+ /**
1394
+ * Documented seed weights for the free static pass (D4a). Each raw signal is
1395
+ * normalized against a reference maximum, multiplied by its weight, and the
1396
+ * weighted contributions are summed and re-normalized to a [0,1] score.
1397
+ *
1398
+ * Diff-based signals dominate post-diff; text-only signals carry the pre-diff
1399
+ * pass. Weights are tunable (DEFERRABLE per plan) — this is a documented seed.
1400
+ */
1401
+ declare const STATIC_WEIGHTS: {
1402
+ readonly filesTouched: 0.25;
1403
+ readonly layersTouched: 0.2;
1404
+ readonly blastRadius: 0.3;
1405
+ readonly hotspotChurn: 0.1;
1406
+ /** Text-only signals — the only inputs available pre-diff. */
1407
+ readonly descriptionLength: 0.1;
1408
+ /** A present spec / measurable acceptance *lowers* complexity (well-scoped). */
1409
+ readonly specExists: 0.025;
1410
+ readonly acceptanceMeasurable: 0.025;
1411
+ };
1412
+ /**
1413
+ * D4a: free static pass. Phase-aware (S3-001): pre-diff uses text-only signals
1414
+ * and caps confidence at `medium`; post-diff uses the full signal set. Pure —
1415
+ * never calls an LLM. `source` is stamped by the classifier, not here.
1416
+ */
1417
+ declare function runStaticPass(signals: ComplexitySignals, phase: Phase): StaticVerdict;
1418
+
1419
+ interface TiebreakResult {
1420
+ level: ComplexityLevel;
1421
+ confidence: 'high' | 'medium' | 'low';
1422
+ }
1423
+ /** D4b: fast-tier structured tie-break. Never sets a tier; falls back conservatively on error. */
1424
+ declare function llmTiebreak(provider: AnalysisProvider, prompt: string, fastModel?: string): Promise<TiebreakResult>;
1425
+
1426
+ declare const TIER_RANK: {
1427
+ fast: number;
1428
+ standard: number;
1429
+ strong: number;
1430
+ };
1431
+ declare const RANK_TIER: readonly ["fast", "standard", "strong"];
1432
+ /**
1433
+ * Documented seed threshold for the D5 high-blast veto. The spec lists
1434
+ * sensitive-path / core|types layer / public API explicitly but pins no numeric
1435
+ * blast threshold, so this is a plan-chosen seed — overridable later.
1436
+ */
1437
+ declare const SENSITIVE_BLAST_THRESHOLD = 25;
1438
+ /**
1439
+ * D5: any sensitive-path / core|types layer / public API / high blast → force
1440
+ * `strong`, regardless of complexity (SC5). This is a HARD floor; downstream
1441
+ * budget clamping (D8) must not undercut it.
1442
+ */
1443
+ declare function blastRadiusVeto(risk?: RoutingRisk): boolean;
1444
+ /**
1445
+ * Pre-budget tier resolution (Task 7): skillTierOverride → matrix → D5 veto →
1446
+ * low-confidence bump. Pure; the LLM never influences it (mirrors
1447
+ * outcome-eval/authority.ts). Task 8's `deriveRequiredTier` calls this then
1448
+ * applies the D8 budget clamp and D10 escalation floor.
1449
+ *
1450
+ * SC6: a `low`-confidence verdict degrades the tier UP one step (never below the
1451
+ * matrix default and never to a cheaper tier) — uncertainty routes to a more
1452
+ * capable model, never a Tier-A cheap one.
1453
+ */
1454
+ declare function baseTier(complexity: ComplexityVerdict, risk: RoutingRisk | undefined, policy: RoutingPolicy, skillKey?: string): CapabilityTier;
1455
+ /** Default budget degrade threshold (% of cap) when policy omits one (D8). */
1456
+ declare const DEFAULT_DEGRADE_AT_PCT = 90;
1457
+ /**
1458
+ * D8: budget-pressure tier clamp. Two thresholds:
1459
+ *
1460
+ * - **Soft (`degradeAtPct`, default 90%):** clamp the tier DOWN one step
1461
+ * (`strong→standard→fast`, `fast→fast`) — a lagging degrade signal.
1462
+ * - **Hard (100% of `capUsd`):** for `degrade`/`pause` policies, force the tier
1463
+ * all the way to `fast` — the strongest sound floor on the routing DECISION
1464
+ * (it is not an admission gate; the monotonic accumulator lags under
1465
+ * concurrency, so it cannot prevent overshoot, only route the cheapest tier
1466
+ * once the read crosses the cap). `human` mode is handled one layer up in
1467
+ * `AdaptiveRouter.route()`, which surfaces the unit to a steward instead of
1468
+ * routing; here it falls through to the soft one-step degrade.
1469
+ *
1470
+ * The D5 blast-radius veto is a HARD `strong` floor the clamp must NOT undercut
1471
+ * (SC5 says sensitive-path / core|types / publicApi force `strong` "regardless"),
1472
+ * so a vetoed request stays `strong` even at the hard cap — the veto guard sits
1473
+ * ABOVE both the hard-floor and the soft-step branches. No budget block ⇒ no clamp.
1474
+ */
1475
+ declare function applyBudgetClamp(tier: CapabilityTier, risk: RoutingRisk | undefined, policy: RoutingPolicy, spend: {
1476
+ spentUsd: number;
1477
+ }): CapabilityTier;
1478
+ /**
1479
+ * Pure (complexity × risk × policy × spend × floor) → required tier.
1480
+ *
1481
+ * `baseTier` resolves override → matrix → D5 veto → low-confidence bump; the D8
1482
+ * budget clamp lowers one step under budget pressure (never below the veto
1483
+ * floor); the D10 escalation floor raises the result but never lowers it. The
1484
+ * LLM never influences this — mirrors outcome-eval/authority.ts (TS-derived
1485
+ * authority). Referentially transparent; never mutates `policy`.
1486
+ */
1487
+ declare function deriveRequiredTier(complexity: ComplexityVerdict, risk: RoutingRisk | undefined, policy: RoutingPolicy, spend: {
1488
+ spentUsd: number;
1489
+ }, escalationFloor: CapabilityTier, skillKey?: string): CapabilityTier;
1490
+
1491
+ /** Flatten signals into the ComplexityVerdict.signals map, dropping undefined fields. */
1492
+ declare function serializeSignals(s: ComplexitySignals): Record<string, number | boolean | string>;
1493
+
1494
+ export { ACCEPTANCE_EVAL_SYSTEM_PROMPT, type AcceptanceEvalInput, AcceptanceEvaluator, type AcceptanceEvaluatorOptions, type AcceptanceVerdict, type AffectedSystem, type AnalysisProvider, type AnalysisRequest, type AnalysisResponse, AnthropicAnalysisProvider, type Authority, type BlastRadius, type BlindSpot, type CanaryAdapter, type CanaryDegradeReason, type CanaryExec, type CanaryFinding, type CanaryProbe, type ClassifyInput, ClaudeCliAnalysisProvider, type ComplexityScore, type ComplexitySignals, type Confidence, DEFAULT_DEGRADE_AT_PCT, type EnrichedSpec, type ExecutionOutcome, ExecutionOutcomeConnector, type ExpertiseLevel, type Finding, type FrameworkRecommendation, type GitHubIssue, GraphValidator, IntelligencePipeline, type JiraIssue, type JudgedAgainst, type LinearIssue, type LlmAcceptanceVerdict, type LlmVerdict, type ManualInput, type Measurability, OUTCOME_EVAL_SYSTEM_PROMPT, OpenAICompatibleAnalysisProvider, type OutcomeEvalInput, OutcomeEvaluator, type OutcomeEvaluatorOptions, type OutcomeIngestResult, type OutcomeVerdict, type PersonaEffectivenessScore, type PersonaRecommendation, PeslSimulator, type Phase, type PreprocessResult, type ProfileStore, RANK_TIER, type RawWorkItem, type ResolvedSection, SENSITIVE_BLAST_THRESHOLD, STATIC_WEIGHTS, type SimulationResult, type SpecializationEntry, type SpecializationProfile, type SpecializationScore, type StaticVerdict, TIER_RANK, type TaskType, type TemporalConfig, type TiebreakResult, type Verdict, type WeightedRecommendation, acceptanceVerdictSchema, applyBudgetClamp, baseTier, blastRadiusVeto, buildUserPrompt as buildAcceptanceUserPrompt, buildSpecializationProfile, buildUserPrompt$1 as buildUserPrompt, classify, computeExpertiseLevel, computeHistoricalComplexity, computePersonaEffectiveness, computeSemanticComplexity, computeSpecialization, computeStructuralComplexity, createCanaryAdapter, decayWeight, deriveAcceptanceAuthority, deriveAuthority, deriveRequiredTier, detectBlindSpots, enrich, findingSchema, githubToRawWorkItem, jiraToRawWorkItem, linearToRawWorkItem, llmTiebreak, loadProfiles, manualToRawWorkItem, recommendPersona, refreshProfiles, resolveSection, runGraphOnlyChecks, runLlmSimulation, runStaticPass, saveProfiles, score as scoreCML, scoreToConcernSignals, serializeSignals, temporalSuccessRate, toRawWorkItem, verdictSchema, weightedRecommendPersona };
package/dist/index.js CHANGED
@@ -34,6 +34,7 @@ __export(index_exports, {
34
34
  AcceptanceEvaluator: () => AcceptanceEvaluator,
35
35
  AnthropicAnalysisProvider: () => AnthropicAnalysisProvider,
36
36
  ClaudeCliAnalysisProvider: () => ClaudeCliAnalysisProvider,
37
+ DEFAULT_DEGRADE_AT_PCT: () => DEFAULT_DEGRADE_AT_PCT,
37
38
  ExecutionOutcomeConnector: () => ExecutionOutcomeConnector,
38
39
  GraphValidator: () => GraphValidator,
39
40
  IntelligencePipeline: () => IntelligencePipeline,
@@ -41,10 +42,18 @@ __export(index_exports, {
41
42
  OpenAICompatibleAnalysisProvider: () => OpenAICompatibleAnalysisProvider,
42
43
  OutcomeEvaluator: () => OutcomeEvaluator,
43
44
  PeslSimulator: () => PeslSimulator,
45
+ RANK_TIER: () => RANK_TIER,
46
+ SENSITIVE_BLAST_THRESHOLD: () => SENSITIVE_BLAST_THRESHOLD,
47
+ STATIC_WEIGHTS: () => STATIC_WEIGHTS,
48
+ TIER_RANK: () => TIER_RANK,
44
49
  acceptanceVerdictSchema: () => acceptanceVerdictSchema,
50
+ applyBudgetClamp: () => applyBudgetClamp,
51
+ baseTier: () => baseTier,
52
+ blastRadiusVeto: () => blastRadiusVeto,
45
53
  buildAcceptanceUserPrompt: () => buildUserPrompt3,
46
54
  buildSpecializationProfile: () => buildSpecializationProfile,
47
55
  buildUserPrompt: () => buildUserPrompt2,
56
+ classify: () => classify,
48
57
  computeExpertiseLevel: () => computeExpertiseLevel,
49
58
  computeHistoricalComplexity: () => computeHistoricalComplexity,
50
59
  computePersonaEffectiveness: () => computePersonaEffectiveness,
@@ -55,12 +64,14 @@ __export(index_exports, {
55
64
  decayWeight: () => decayWeight,
56
65
  deriveAcceptanceAuthority: () => deriveAcceptanceAuthority,
57
66
  deriveAuthority: () => deriveAuthority,
67
+ deriveRequiredTier: () => deriveRequiredTier,
58
68
  detectBlindSpots: () => detectBlindSpots,
59
69
  enrich: () => enrich,
60
70
  findingSchema: () => findingSchema,
61
71
  githubToRawWorkItem: () => githubToRawWorkItem,
62
72
  jiraToRawWorkItem: () => jiraToRawWorkItem,
63
73
  linearToRawWorkItem: () => linearToRawWorkItem,
74
+ llmTiebreak: () => llmTiebreak,
64
75
  loadProfiles: () => loadProfiles,
65
76
  manualToRawWorkItem: () => manualToRawWorkItem,
66
77
  recommendPersona: () => recommendPersona,
@@ -68,9 +79,11 @@ __export(index_exports, {
68
79
  resolveSection: () => resolveSection,
69
80
  runGraphOnlyChecks: () => runGraphOnlyChecks,
70
81
  runLlmSimulation: () => runLlmSimulation,
82
+ runStaticPass: () => runStaticPass,
71
83
  saveProfiles: () => saveProfiles,
72
84
  scoreCML: () => score,
73
85
  scoreToConcernSignals: () => scoreToConcernSignals,
86
+ serializeSignals: () => serializeSignals,
74
87
  temporalSuccessRate: () => temporalSuccessRate,
75
88
  toRawWorkItem: () => toRawWorkItem,
76
89
  verdictSchema: () => verdictSchema,
@@ -415,6 +428,7 @@ var DEFAULT_TIMEOUT_MS = 9e4;
415
428
  var OpenAICompatibleAnalysisProvider = class {
416
429
  client;
417
430
  defaultModel;
431
+ getModel;
418
432
  promptSuffix;
419
433
  jsonMode;
420
434
  constructor(options) {
@@ -424,11 +438,15 @@ var OpenAICompatibleAnalysisProvider = class {
424
438
  timeout: options.timeoutMs ?? DEFAULT_TIMEOUT_MS
425
439
  });
426
440
  this.defaultModel = options.defaultModel ?? DEFAULT_MODEL2;
441
+ if (options.getModel !== void 0) {
442
+ this.getModel = options.getModel;
443
+ }
427
444
  this.promptSuffix = options.promptSuffix ?? null;
428
445
  this.jsonMode = options.jsonMode ?? true;
429
446
  }
430
447
  async analyze(request) {
431
- const model = request.model ?? this.defaultModel;
448
+ const resolved = this.getModel?.();
449
+ const model = request.model ?? (resolved != null && resolved !== "" ? resolved : this.defaultModel);
432
450
  const maxTokens = request.maxTokens ?? DEFAULT_MAX_TOKENS2;
433
451
  const jsonSchema = zodToJsonSchema(request.responseSchema);
434
452
  const systemParts = [];
@@ -2115,12 +2133,224 @@ function refreshProfiles(projectRoot, graphStore, opts) {
2115
2133
  saveProfiles(projectRoot, store);
2116
2134
  return store;
2117
2135
  }
2136
+
2137
+ // src/complexity/signals.ts
2138
+ function serializeSignals(s) {
2139
+ const out = {
2140
+ descriptionLength: s.descriptionLength,
2141
+ specExists: s.specExists,
2142
+ acceptanceMeasurable: s.acceptanceMeasurable
2143
+ };
2144
+ if (s.filesTouched !== void 0) out.filesTouched = s.filesTouched;
2145
+ if (s.layersTouched !== void 0) out.layersTouched = s.layersTouched;
2146
+ if (s.blastRadius !== void 0) out.blastRadius = s.blastRadius;
2147
+ if (s.hotspotChurn !== void 0) out.hotspotChurn = s.hotspotChurn;
2148
+ return out;
2149
+ }
2150
+
2151
+ // src/complexity/static-pass.ts
2152
+ var STATIC_WEIGHTS = {
2153
+ filesTouched: 0.25,
2154
+ layersTouched: 0.2,
2155
+ blastRadius: 0.3,
2156
+ hotspotChurn: 0.1,
2157
+ /** Text-only signals — the only inputs available pre-diff. */
2158
+ descriptionLength: 0.1,
2159
+ /** A present spec / measurable acceptance *lowers* complexity (well-scoped). */
2160
+ specExists: 0.025,
2161
+ acceptanceMeasurable: 0.025
2162
+ };
2163
+ var REF_MAX = {
2164
+ filesTouched: 20,
2165
+ layersTouched: 4,
2166
+ blastRadius: 40,
2167
+ hotspotChurn: 1,
2168
+ descriptionLength: 500
2169
+ };
2170
+ var CONFIDENCE_RANK = {
2171
+ low: 0,
2172
+ medium: 1,
2173
+ high: 2
2174
+ };
2175
+ function clamp01(n) {
2176
+ return Math.max(0, Math.min(1, n));
2177
+ }
2178
+ function scoreToLevel(score2) {
2179
+ if (score2 < 0.2) return "trivial";
2180
+ if (score2 < 0.45) return "simple";
2181
+ if (score2 < 0.7) return "moderate";
2182
+ return "complex";
2183
+ }
2184
+ function scoreToConfidence(score2) {
2185
+ const boundaries = [0.2, 0.45, 0.7];
2186
+ const nearest = Math.min(...boundaries.map((b) => Math.abs(score2 - b)));
2187
+ const atExtremeEnd = score2 < 0.2 || score2 >= 0.7;
2188
+ if (nearest < 0.1) return "low";
2189
+ if (nearest < 0.18 && !atExtremeEnd) return "medium";
2190
+ return "high";
2191
+ }
2192
+ function runStaticPass(signals, phase) {
2193
+ const isPostDiff = phase === "post-diff";
2194
+ const contributions = [];
2195
+ const weightsUsed = [];
2196
+ if (isPostDiff) {
2197
+ if (signals.filesTouched !== void 0) {
2198
+ contributions.push(
2199
+ clamp01(signals.filesTouched / REF_MAX.filesTouched) * STATIC_WEIGHTS.filesTouched
2200
+ );
2201
+ weightsUsed.push(STATIC_WEIGHTS.filesTouched);
2202
+ }
2203
+ if (signals.layersTouched !== void 0) {
2204
+ contributions.push(
2205
+ clamp01(signals.layersTouched / REF_MAX.layersTouched) * STATIC_WEIGHTS.layersTouched
2206
+ );
2207
+ weightsUsed.push(STATIC_WEIGHTS.layersTouched);
2208
+ }
2209
+ if (signals.blastRadius !== void 0) {
2210
+ contributions.push(
2211
+ clamp01(signals.blastRadius / REF_MAX.blastRadius) * STATIC_WEIGHTS.blastRadius
2212
+ );
2213
+ weightsUsed.push(STATIC_WEIGHTS.blastRadius);
2214
+ }
2215
+ if (signals.hotspotChurn !== void 0) {
2216
+ contributions.push(
2217
+ clamp01(signals.hotspotChurn / REF_MAX.hotspotChurn) * STATIC_WEIGHTS.hotspotChurn
2218
+ );
2219
+ weightsUsed.push(STATIC_WEIGHTS.hotspotChurn);
2220
+ }
2221
+ }
2222
+ contributions.push(
2223
+ clamp01(signals.descriptionLength / REF_MAX.descriptionLength) * STATIC_WEIGHTS.descriptionLength
2224
+ );
2225
+ weightsUsed.push(STATIC_WEIGHTS.descriptionLength);
2226
+ const scopeReduction = (signals.specExists ? STATIC_WEIGHTS.specExists : 0) + (signals.acceptanceMeasurable ? STATIC_WEIGHTS.acceptanceMeasurable : 0);
2227
+ const totalWeight = weightsUsed.reduce((a, b) => a + b, 0);
2228
+ const rawScore = contributions.reduce((a, b) => a + b, 0);
2229
+ const normalized = totalWeight > 0 ? rawScore / totalWeight : 0;
2230
+ const score2 = clamp01(normalized - scopeReduction);
2231
+ const level = scoreToLevel(score2);
2232
+ let confidence = scoreToConfidence(score2);
2233
+ if (!isPostDiff && CONFIDENCE_RANK[confidence] > CONFIDENCE_RANK.medium) {
2234
+ confidence = "medium";
2235
+ }
2236
+ return {
2237
+ level,
2238
+ confidence,
2239
+ signals: serializeSignals(signals)
2240
+ };
2241
+ }
2242
+
2243
+ // src/complexity/tiebreak.ts
2244
+ var import_zod6 = require("zod");
2245
+ var TiebreakSchema = import_zod6.z.object({
2246
+ level: import_zod6.z.enum(["trivial", "simple", "moderate", "complex"]),
2247
+ confidence: import_zod6.z.enum(["high", "medium", "low"])
2248
+ });
2249
+ async function llmTiebreak(provider, prompt, fastModel) {
2250
+ try {
2251
+ const { result } = await provider.analyze({
2252
+ prompt,
2253
+ responseSchema: TiebreakSchema,
2254
+ // Only include `model` when supplied (exactOptionalPropertyTypes).
2255
+ ...fastModel !== void 0 ? { model: fastModel } : {},
2256
+ maxTokens: 256
2257
+ });
2258
+ return result;
2259
+ } catch {
2260
+ return { level: "moderate", confidence: "low" };
2261
+ }
2262
+ }
2263
+
2264
+ // src/complexity/classifier.ts
2265
+ var CONFIDENCE_RANK2 = { low: 0, medium: 1, high: 2 };
2266
+ function capForPhase(confidence, phase) {
2267
+ if (phase === "pre-diff" && CONFIDENCE_RANK2[confidence] > CONFIDENCE_RANK2.medium) {
2268
+ return "medium";
2269
+ }
2270
+ return confidence;
2271
+ }
2272
+ async function classify(input, provider, models) {
2273
+ const staticVerdict = runStaticPass(input.signals, input.phase);
2274
+ if (staticVerdict.confidence !== "low" || provider === void 0) {
2275
+ return {
2276
+ level: staticVerdict.level,
2277
+ confidence: staticVerdict.confidence,
2278
+ signals: staticVerdict.signals,
2279
+ source: "static"
2280
+ };
2281
+ }
2282
+ const tiebreak = await llmTiebreak(provider, input.prompt, models?.fast);
2283
+ if (tiebreak.confidence === "low" && input.riskHigh) {
2284
+ const escalated = await llmTiebreak(provider, input.prompt, models?.standard);
2285
+ return {
2286
+ level: escalated.level,
2287
+ confidence: capForPhase(escalated.confidence, input.phase),
2288
+ signals: staticVerdict.signals,
2289
+ source: "escalated"
2290
+ };
2291
+ }
2292
+ return {
2293
+ level: tiebreak.level,
2294
+ confidence: capForPhase(tiebreak.confidence, input.phase),
2295
+ signals: staticVerdict.signals,
2296
+ source: "llm-tiebreak"
2297
+ };
2298
+ }
2299
+
2300
+ // src/complexity/derive-tier.ts
2301
+ var TIER_RANK = { fast: 0, standard: 1, strong: 2 };
2302
+ var RANK_TIER = ["fast", "standard", "strong"];
2303
+ var SENSITIVE_BLAST_THRESHOLD = 25;
2304
+ var DEFAULT_MATRIX = {
2305
+ trivial: "fast",
2306
+ simple: "fast",
2307
+ moderate: "standard",
2308
+ complex: "strong"
2309
+ };
2310
+ function tierAtRank(rank) {
2311
+ const clamped = Math.max(0, Math.min(RANK_TIER.length - 1, rank));
2312
+ return RANK_TIER[clamped];
2313
+ }
2314
+ function blastRadiusVeto(risk) {
2315
+ if (!risk) return false;
2316
+ return risk.sensitivePath === true || risk.publicApi === true || risk.layer === "core" || risk.layer === "types" || typeof risk.blastRadius === "number" && risk.blastRadius >= SENSITIVE_BLAST_THRESHOLD;
2317
+ }
2318
+ function baseTier(complexity, risk, policy, skillKey) {
2319
+ const veto = blastRadiusVeto(risk);
2320
+ const override = skillKey !== void 0 ? policy.skillTierOverrides?.[skillKey] : void 0;
2321
+ const matrix = policy.complexityTierMatrix ?? {};
2322
+ const fromMatrix = matrix[complexity.level] ?? DEFAULT_MATRIX[complexity.level];
2323
+ let rank = Math.max(override !== void 0 ? TIER_RANK[override] : 0, TIER_RANK[fromMatrix]);
2324
+ if (complexity.confidence === "low") {
2325
+ rank = rank + 1;
2326
+ }
2327
+ if (veto) rank = Math.max(rank, TIER_RANK.strong);
2328
+ return tierAtRank(rank);
2329
+ }
2330
+ var DEFAULT_DEGRADE_AT_PCT = 90;
2331
+ function applyBudgetClamp(tier, risk, policy, spend) {
2332
+ const budget = policy.budget;
2333
+ if (!budget || budget.capUsd <= 0) return tier;
2334
+ const degradeAt = (budget.degradeAtPct ?? DEFAULT_DEGRADE_AT_PCT) / 100;
2335
+ const spentFraction = spend.spentUsd / budget.capUsd;
2336
+ if (spentFraction < degradeAt) return tier;
2337
+ if (blastRadiusVeto(risk)) return tier;
2338
+ const mode = budget.onBudgetExhausted ?? "degrade";
2339
+ if (spentFraction >= 1 && mode !== "human") return "fast";
2340
+ return tierAtRank(TIER_RANK[tier] - 1);
2341
+ }
2342
+ function deriveRequiredTier(complexity, risk, policy, spend, escalationFloor, skillKey) {
2343
+ const base = baseTier(complexity, risk, policy, skillKey);
2344
+ const clamped = applyBudgetClamp(base, risk, policy, spend);
2345
+ return tierAtRank(Math.max(TIER_RANK[escalationFloor], TIER_RANK[clamped]));
2346
+ }
2118
2347
  // Annotate the CommonJS export names for ESM import in node:
2119
2348
  0 && (module.exports = {
2120
2349
  ACCEPTANCE_EVAL_SYSTEM_PROMPT,
2121
2350
  AcceptanceEvaluator,
2122
2351
  AnthropicAnalysisProvider,
2123
2352
  ClaudeCliAnalysisProvider,
2353
+ DEFAULT_DEGRADE_AT_PCT,
2124
2354
  ExecutionOutcomeConnector,
2125
2355
  GraphValidator,
2126
2356
  IntelligencePipeline,
@@ -2128,10 +2358,18 @@ function refreshProfiles(projectRoot, graphStore, opts) {
2128
2358
  OpenAICompatibleAnalysisProvider,
2129
2359
  OutcomeEvaluator,
2130
2360
  PeslSimulator,
2361
+ RANK_TIER,
2362
+ SENSITIVE_BLAST_THRESHOLD,
2363
+ STATIC_WEIGHTS,
2364
+ TIER_RANK,
2131
2365
  acceptanceVerdictSchema,
2366
+ applyBudgetClamp,
2367
+ baseTier,
2368
+ blastRadiusVeto,
2132
2369
  buildAcceptanceUserPrompt,
2133
2370
  buildSpecializationProfile,
2134
2371
  buildUserPrompt,
2372
+ classify,
2135
2373
  computeExpertiseLevel,
2136
2374
  computeHistoricalComplexity,
2137
2375
  computePersonaEffectiveness,
@@ -2142,12 +2380,14 @@ function refreshProfiles(projectRoot, graphStore, opts) {
2142
2380
  decayWeight,
2143
2381
  deriveAcceptanceAuthority,
2144
2382
  deriveAuthority,
2383
+ deriveRequiredTier,
2145
2384
  detectBlindSpots,
2146
2385
  enrich,
2147
2386
  findingSchema,
2148
2387
  githubToRawWorkItem,
2149
2388
  jiraToRawWorkItem,
2150
2389
  linearToRawWorkItem,
2390
+ llmTiebreak,
2151
2391
  loadProfiles,
2152
2392
  manualToRawWorkItem,
2153
2393
  recommendPersona,
@@ -2155,9 +2395,11 @@ function refreshProfiles(projectRoot, graphStore, opts) {
2155
2395
  resolveSection,
2156
2396
  runGraphOnlyChecks,
2157
2397
  runLlmSimulation,
2398
+ runStaticPass,
2158
2399
  saveProfiles,
2159
2400
  scoreCML,
2160
2401
  scoreToConcernSignals,
2402
+ serializeSignals,
2161
2403
  temporalSuccessRate,
2162
2404
  toRawWorkItem,
2163
2405
  verdictSchema,
package/dist/index.mjs CHANGED
@@ -335,6 +335,7 @@ var DEFAULT_TIMEOUT_MS = 9e4;
335
335
  var OpenAICompatibleAnalysisProvider = class {
336
336
  client;
337
337
  defaultModel;
338
+ getModel;
338
339
  promptSuffix;
339
340
  jsonMode;
340
341
  constructor(options) {
@@ -344,11 +345,15 @@ var OpenAICompatibleAnalysisProvider = class {
344
345
  timeout: options.timeoutMs ?? DEFAULT_TIMEOUT_MS
345
346
  });
346
347
  this.defaultModel = options.defaultModel ?? DEFAULT_MODEL2;
348
+ if (options.getModel !== void 0) {
349
+ this.getModel = options.getModel;
350
+ }
347
351
  this.promptSuffix = options.promptSuffix ?? null;
348
352
  this.jsonMode = options.jsonMode ?? true;
349
353
  }
350
354
  async analyze(request) {
351
- const model = request.model ?? this.defaultModel;
355
+ const resolved = this.getModel?.();
356
+ const model = request.model ?? (resolved != null && resolved !== "" ? resolved : this.defaultModel);
352
357
  const maxTokens = request.maxTokens ?? DEFAULT_MAX_TOKENS2;
353
358
  const jsonSchema = zodToJsonSchema(request.responseSchema);
354
359
  const systemParts = [];
@@ -2035,11 +2040,223 @@ function refreshProfiles(projectRoot, graphStore, opts) {
2035
2040
  saveProfiles(projectRoot, store);
2036
2041
  return store;
2037
2042
  }
2043
+
2044
+ // src/complexity/signals.ts
2045
+ function serializeSignals(s) {
2046
+ const out = {
2047
+ descriptionLength: s.descriptionLength,
2048
+ specExists: s.specExists,
2049
+ acceptanceMeasurable: s.acceptanceMeasurable
2050
+ };
2051
+ if (s.filesTouched !== void 0) out.filesTouched = s.filesTouched;
2052
+ if (s.layersTouched !== void 0) out.layersTouched = s.layersTouched;
2053
+ if (s.blastRadius !== void 0) out.blastRadius = s.blastRadius;
2054
+ if (s.hotspotChurn !== void 0) out.hotspotChurn = s.hotspotChurn;
2055
+ return out;
2056
+ }
2057
+
2058
+ // src/complexity/static-pass.ts
2059
+ var STATIC_WEIGHTS = {
2060
+ filesTouched: 0.25,
2061
+ layersTouched: 0.2,
2062
+ blastRadius: 0.3,
2063
+ hotspotChurn: 0.1,
2064
+ /** Text-only signals — the only inputs available pre-diff. */
2065
+ descriptionLength: 0.1,
2066
+ /** A present spec / measurable acceptance *lowers* complexity (well-scoped). */
2067
+ specExists: 0.025,
2068
+ acceptanceMeasurable: 0.025
2069
+ };
2070
+ var REF_MAX = {
2071
+ filesTouched: 20,
2072
+ layersTouched: 4,
2073
+ blastRadius: 40,
2074
+ hotspotChurn: 1,
2075
+ descriptionLength: 500
2076
+ };
2077
+ var CONFIDENCE_RANK = {
2078
+ low: 0,
2079
+ medium: 1,
2080
+ high: 2
2081
+ };
2082
+ function clamp01(n) {
2083
+ return Math.max(0, Math.min(1, n));
2084
+ }
2085
+ function scoreToLevel(score2) {
2086
+ if (score2 < 0.2) return "trivial";
2087
+ if (score2 < 0.45) return "simple";
2088
+ if (score2 < 0.7) return "moderate";
2089
+ return "complex";
2090
+ }
2091
+ function scoreToConfidence(score2) {
2092
+ const boundaries = [0.2, 0.45, 0.7];
2093
+ const nearest = Math.min(...boundaries.map((b) => Math.abs(score2 - b)));
2094
+ const atExtremeEnd = score2 < 0.2 || score2 >= 0.7;
2095
+ if (nearest < 0.1) return "low";
2096
+ if (nearest < 0.18 && !atExtremeEnd) return "medium";
2097
+ return "high";
2098
+ }
2099
+ function runStaticPass(signals, phase) {
2100
+ const isPostDiff = phase === "post-diff";
2101
+ const contributions = [];
2102
+ const weightsUsed = [];
2103
+ if (isPostDiff) {
2104
+ if (signals.filesTouched !== void 0) {
2105
+ contributions.push(
2106
+ clamp01(signals.filesTouched / REF_MAX.filesTouched) * STATIC_WEIGHTS.filesTouched
2107
+ );
2108
+ weightsUsed.push(STATIC_WEIGHTS.filesTouched);
2109
+ }
2110
+ if (signals.layersTouched !== void 0) {
2111
+ contributions.push(
2112
+ clamp01(signals.layersTouched / REF_MAX.layersTouched) * STATIC_WEIGHTS.layersTouched
2113
+ );
2114
+ weightsUsed.push(STATIC_WEIGHTS.layersTouched);
2115
+ }
2116
+ if (signals.blastRadius !== void 0) {
2117
+ contributions.push(
2118
+ clamp01(signals.blastRadius / REF_MAX.blastRadius) * STATIC_WEIGHTS.blastRadius
2119
+ );
2120
+ weightsUsed.push(STATIC_WEIGHTS.blastRadius);
2121
+ }
2122
+ if (signals.hotspotChurn !== void 0) {
2123
+ contributions.push(
2124
+ clamp01(signals.hotspotChurn / REF_MAX.hotspotChurn) * STATIC_WEIGHTS.hotspotChurn
2125
+ );
2126
+ weightsUsed.push(STATIC_WEIGHTS.hotspotChurn);
2127
+ }
2128
+ }
2129
+ contributions.push(
2130
+ clamp01(signals.descriptionLength / REF_MAX.descriptionLength) * STATIC_WEIGHTS.descriptionLength
2131
+ );
2132
+ weightsUsed.push(STATIC_WEIGHTS.descriptionLength);
2133
+ const scopeReduction = (signals.specExists ? STATIC_WEIGHTS.specExists : 0) + (signals.acceptanceMeasurable ? STATIC_WEIGHTS.acceptanceMeasurable : 0);
2134
+ const totalWeight = weightsUsed.reduce((a, b) => a + b, 0);
2135
+ const rawScore = contributions.reduce((a, b) => a + b, 0);
2136
+ const normalized = totalWeight > 0 ? rawScore / totalWeight : 0;
2137
+ const score2 = clamp01(normalized - scopeReduction);
2138
+ const level = scoreToLevel(score2);
2139
+ let confidence = scoreToConfidence(score2);
2140
+ if (!isPostDiff && CONFIDENCE_RANK[confidence] > CONFIDENCE_RANK.medium) {
2141
+ confidence = "medium";
2142
+ }
2143
+ return {
2144
+ level,
2145
+ confidence,
2146
+ signals: serializeSignals(signals)
2147
+ };
2148
+ }
2149
+
2150
+ // src/complexity/tiebreak.ts
2151
+ import { z as z6 } from "zod";
2152
+ var TiebreakSchema = z6.object({
2153
+ level: z6.enum(["trivial", "simple", "moderate", "complex"]),
2154
+ confidence: z6.enum(["high", "medium", "low"])
2155
+ });
2156
+ async function llmTiebreak(provider, prompt, fastModel) {
2157
+ try {
2158
+ const { result } = await provider.analyze({
2159
+ prompt,
2160
+ responseSchema: TiebreakSchema,
2161
+ // Only include `model` when supplied (exactOptionalPropertyTypes).
2162
+ ...fastModel !== void 0 ? { model: fastModel } : {},
2163
+ maxTokens: 256
2164
+ });
2165
+ return result;
2166
+ } catch {
2167
+ return { level: "moderate", confidence: "low" };
2168
+ }
2169
+ }
2170
+
2171
+ // src/complexity/classifier.ts
2172
+ var CONFIDENCE_RANK2 = { low: 0, medium: 1, high: 2 };
2173
+ function capForPhase(confidence, phase) {
2174
+ if (phase === "pre-diff" && CONFIDENCE_RANK2[confidence] > CONFIDENCE_RANK2.medium) {
2175
+ return "medium";
2176
+ }
2177
+ return confidence;
2178
+ }
2179
+ async function classify(input, provider, models) {
2180
+ const staticVerdict = runStaticPass(input.signals, input.phase);
2181
+ if (staticVerdict.confidence !== "low" || provider === void 0) {
2182
+ return {
2183
+ level: staticVerdict.level,
2184
+ confidence: staticVerdict.confidence,
2185
+ signals: staticVerdict.signals,
2186
+ source: "static"
2187
+ };
2188
+ }
2189
+ const tiebreak = await llmTiebreak(provider, input.prompt, models?.fast);
2190
+ if (tiebreak.confidence === "low" && input.riskHigh) {
2191
+ const escalated = await llmTiebreak(provider, input.prompt, models?.standard);
2192
+ return {
2193
+ level: escalated.level,
2194
+ confidence: capForPhase(escalated.confidence, input.phase),
2195
+ signals: staticVerdict.signals,
2196
+ source: "escalated"
2197
+ };
2198
+ }
2199
+ return {
2200
+ level: tiebreak.level,
2201
+ confidence: capForPhase(tiebreak.confidence, input.phase),
2202
+ signals: staticVerdict.signals,
2203
+ source: "llm-tiebreak"
2204
+ };
2205
+ }
2206
+
2207
+ // src/complexity/derive-tier.ts
2208
+ var TIER_RANK = { fast: 0, standard: 1, strong: 2 };
2209
+ var RANK_TIER = ["fast", "standard", "strong"];
2210
+ var SENSITIVE_BLAST_THRESHOLD = 25;
2211
+ var DEFAULT_MATRIX = {
2212
+ trivial: "fast",
2213
+ simple: "fast",
2214
+ moderate: "standard",
2215
+ complex: "strong"
2216
+ };
2217
+ function tierAtRank(rank) {
2218
+ const clamped = Math.max(0, Math.min(RANK_TIER.length - 1, rank));
2219
+ return RANK_TIER[clamped];
2220
+ }
2221
+ function blastRadiusVeto(risk) {
2222
+ if (!risk) return false;
2223
+ return risk.sensitivePath === true || risk.publicApi === true || risk.layer === "core" || risk.layer === "types" || typeof risk.blastRadius === "number" && risk.blastRadius >= SENSITIVE_BLAST_THRESHOLD;
2224
+ }
2225
+ function baseTier(complexity, risk, policy, skillKey) {
2226
+ const veto = blastRadiusVeto(risk);
2227
+ const override = skillKey !== void 0 ? policy.skillTierOverrides?.[skillKey] : void 0;
2228
+ const matrix = policy.complexityTierMatrix ?? {};
2229
+ const fromMatrix = matrix[complexity.level] ?? DEFAULT_MATRIX[complexity.level];
2230
+ let rank = Math.max(override !== void 0 ? TIER_RANK[override] : 0, TIER_RANK[fromMatrix]);
2231
+ if (complexity.confidence === "low") {
2232
+ rank = rank + 1;
2233
+ }
2234
+ if (veto) rank = Math.max(rank, TIER_RANK.strong);
2235
+ return tierAtRank(rank);
2236
+ }
2237
+ var DEFAULT_DEGRADE_AT_PCT = 90;
2238
+ function applyBudgetClamp(tier, risk, policy, spend) {
2239
+ const budget = policy.budget;
2240
+ if (!budget || budget.capUsd <= 0) return tier;
2241
+ const degradeAt = (budget.degradeAtPct ?? DEFAULT_DEGRADE_AT_PCT) / 100;
2242
+ const spentFraction = spend.spentUsd / budget.capUsd;
2243
+ if (spentFraction < degradeAt) return tier;
2244
+ if (blastRadiusVeto(risk)) return tier;
2245
+ const mode = budget.onBudgetExhausted ?? "degrade";
2246
+ if (spentFraction >= 1 && mode !== "human") return "fast";
2247
+ return tierAtRank(TIER_RANK[tier] - 1);
2248
+ }
2249
+ function deriveRequiredTier(complexity, risk, policy, spend, escalationFloor, skillKey) {
2250
+ const base = baseTier(complexity, risk, policy, skillKey);
2251
+ const clamped = applyBudgetClamp(base, risk, policy, spend);
2252
+ return tierAtRank(Math.max(TIER_RANK[escalationFloor], TIER_RANK[clamped]));
2253
+ }
2038
2254
  export {
2039
2255
  ACCEPTANCE_EVAL_SYSTEM_PROMPT,
2040
2256
  AcceptanceEvaluator,
2041
2257
  AnthropicAnalysisProvider,
2042
2258
  ClaudeCliAnalysisProvider,
2259
+ DEFAULT_DEGRADE_AT_PCT,
2043
2260
  ExecutionOutcomeConnector,
2044
2261
  GraphValidator,
2045
2262
  IntelligencePipeline,
@@ -2047,10 +2264,18 @@ export {
2047
2264
  OpenAICompatibleAnalysisProvider,
2048
2265
  OutcomeEvaluator,
2049
2266
  PeslSimulator,
2267
+ RANK_TIER,
2268
+ SENSITIVE_BLAST_THRESHOLD,
2269
+ STATIC_WEIGHTS,
2270
+ TIER_RANK,
2050
2271
  acceptanceVerdictSchema,
2272
+ applyBudgetClamp,
2273
+ baseTier,
2274
+ blastRadiusVeto,
2051
2275
  buildUserPrompt3 as buildAcceptanceUserPrompt,
2052
2276
  buildSpecializationProfile,
2053
2277
  buildUserPrompt2 as buildUserPrompt,
2278
+ classify,
2054
2279
  computeExpertiseLevel,
2055
2280
  computeHistoricalComplexity,
2056
2281
  computePersonaEffectiveness,
@@ -2061,12 +2286,14 @@ export {
2061
2286
  decayWeight,
2062
2287
  deriveAcceptanceAuthority,
2063
2288
  deriveAuthority,
2289
+ deriveRequiredTier,
2064
2290
  detectBlindSpots,
2065
2291
  enrich,
2066
2292
  findingSchema,
2067
2293
  githubToRawWorkItem,
2068
2294
  jiraToRawWorkItem,
2069
2295
  linearToRawWorkItem,
2296
+ llmTiebreak,
2070
2297
  loadProfiles,
2071
2298
  manualToRawWorkItem,
2072
2299
  recommendPersona,
@@ -2074,9 +2301,11 @@ export {
2074
2301
  resolveSection,
2075
2302
  runGraphOnlyChecks,
2076
2303
  runLlmSimulation,
2304
+ runStaticPass,
2077
2305
  saveProfiles,
2078
2306
  score as scoreCML,
2079
2307
  scoreToConcernSignals,
2308
+ serializeSignals,
2080
2309
  temporalSuccessRate,
2081
2310
  toRawWorkItem,
2082
2311
  verdictSchema,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harness-engineering/intelligence",
3
- "version": "0.4.3",
3
+ "version": "0.6.0",
4
4
  "description": "Intelligence pipeline for spec enrichment, complexity modeling, and pre-execution simulation",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -40,8 +40,8 @@
40
40
  "@anthropic-ai/sdk": "^0.95.1",
41
41
  "openai": "^6.0.0",
42
42
  "zod": "^3.25.76",
43
- "@harness-engineering/graph": "0.11.5",
44
- "@harness-engineering/types": "0.19.0"
43
+ "@harness-engineering/graph": "0.11.7",
44
+ "@harness-engineering/types": "0.21.0"
45
45
  },
46
46
  "optionalDependencies": {
47
47
  "canary-test-cli": "^5.4.0"