open-multi-agent-kit 0.90.4 → 0.90.5
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/CHANGELOG.md +22 -0
- package/README.md +8 -8
- package/dist/core/adaptorch-bridge.d.ts +14 -21
- package/dist/core/adaptorch-bridge.d.ts.map +1 -1
- package/dist/core/adaptorch-bridge.js +11 -18
- package/dist/core/adaptorch-bridge.js.map +1 -1
- package/dist/core/agent-session.d.ts +6 -39
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +7 -99
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/context-budget-v2-planner.d.ts.map +1 -1
- package/dist/core/context-budget-v2-planner.js +10 -2
- package/dist/core/context-budget-v2-planner.js.map +1 -1
- package/dist/core/context-budget-v2-types.d.ts +7 -1
- package/dist/core/context-budget-v2-types.d.ts.map +1 -1
- package/dist/core/context-budget-v2-types.js.map +1 -1
- package/dist/core/reasoning-router-bias.d.ts +11 -23
- package/dist/core/reasoning-router-bias.d.ts.map +1 -1
- package/dist/core/reasoning-router-bias.js +9 -18
- package/dist/core/reasoning-router-bias.js.map +1 -1
- package/dist/core/reasoning-router-resolver.d.ts +44 -0
- package/dist/core/reasoning-router-resolver.d.ts.map +1 -0
- package/dist/core/reasoning-router-resolver.js +79 -0
- package/dist/core/reasoning-router-resolver.js.map +1 -0
- package/dist/core/reasoning-router-v4-weights.d.ts +28 -72
- package/dist/core/reasoning-router-v4-weights.d.ts.map +1 -1
- package/dist/core/reasoning-router-v4-weights.js +16 -60
- package/dist/core/reasoning-router-v4-weights.js.map +1 -1
- package/dist/core/reasoning-router-v4.d.ts +25 -70
- package/dist/core/reasoning-router-v4.d.ts.map +1 -1
- package/dist/core/reasoning-router-v4.js +46 -144
- package/dist/core/reasoning-router-v4.js.map +1 -1
- package/dist/core/router-feedback-collector.d.ts +13 -16
- package/dist/core/router-feedback-collector.d.ts.map +1 -1
- package/dist/core/router-feedback-collector.js +5 -8
- package/dist/core/router-feedback-collector.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +1 -1
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +16 -98
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/docs/usage.md +11 -12
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/subagent/agent-capability-router.test.ts +123 -0
- package/examples/extensions/subagent/agent-capability-router.ts +274 -0
- package/examples/extensions/subagent/agents/planner.md +3 -0
- package/examples/extensions/subagent/agents/reviewer.md +3 -0
- package/examples/extensions/subagent/agents/scout.md +3 -0
- package/examples/extensions/subagent/agents/worker.md +2 -0
- package/examples/extensions/subagent/agents.ts +14 -0
- package/examples/extensions/subagent/capabilities.ts +368 -0
- package/examples/extensions/subagent/domain-profiles.ts +1089 -0
- package/examples/extensions/subagent/index.ts +71 -3
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/examples/sdk/12-full-control.ts +1 -1
- package/npm-shrinkwrap.json +12 -12
- package/package.json +4 -4
- package/dist/core/compactor.d.ts +0 -42
- package/dist/core/compactor.d.ts.map +0 -1
- package/dist/core/compactor.js +0 -128
- package/dist/core/compactor.js.map +0 -1
- package/dist/core/reasoning-router-v2.d.ts +0 -94
- package/dist/core/reasoning-router-v2.d.ts.map +0 -1
- package/dist/core/reasoning-router-v2.js +0 -288
- package/dist/core/reasoning-router-v2.js.map +0 -1
- package/dist/core/reasoning-router-v3.d.ts +0 -15
- package/dist/core/reasoning-router-v3.d.ts.map +0 -1
- package/dist/core/reasoning-router-v3.js +0 -193
- package/dist/core/reasoning-router-v3.js.map +0 -1
- package/dist/core/reasoning-router-weights.d.ts +0 -155
- package/dist/core/reasoning-router-weights.d.ts.map +0 -1
- package/dist/core/reasoning-router-weights.js +0 -204
- package/dist/core/reasoning-router-weights.js.map +0 -1
- package/dist/core/reasoning-router.d.ts +0 -44
- package/dist/core/reasoning-router.d.ts.map +0 -1
- package/dist/core/reasoning-router.js +0 -138
- package/dist/core/reasoning-router.js.map +0 -1
- package/dist/core/token-optimizer.d.ts +0 -58
- package/dist/core/token-optimizer.d.ts.map +0 -1
- package/dist/core/token-optimizer.js +0 -179
- package/dist/core/token-optimizer.js.map +0 -1
|
@@ -1,13 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Reasoning-router v4 —
|
|
3
|
-
* specs/008-reasoning-router-advanced-accuracy Requirement 2).
|
|
4
|
-
*
|
|
5
|
-
* v4 is a NEW, OPT-IN, PURE sibling module. It does not import from, and is not
|
|
6
|
-
* imported by, reasoning-router.ts (v1), reasoning-router-v2.ts,
|
|
7
|
-
* reasoning-router-v3.ts, reasoning-router-weights.ts, or agent-session.ts.
|
|
8
|
-
* Activating v4 in the product (a `/think auto-v4` mode, settings wiring, etc.)
|
|
9
|
-
* is explicitly out of scope for this lane and is left to a later,
|
|
10
|
-
* single-writer integration lane (specs/008 plan.md Wave 3).
|
|
2
|
+
* Reasoning-router v4 — the sole `/think auto` classifier/resolver.
|
|
11
3
|
*
|
|
12
4
|
* Deterministic by construction: same (input, weights) -> same
|
|
13
5
|
* ClassifierVerdictV4; same (verdict, availableLevels, laneType, bias, hint) ->
|
|
@@ -15,57 +7,17 @@
|
|
|
15
7
|
* or state mutation anywhere in this file (ThinkingLevel is a type-only import
|
|
16
8
|
* and is erased).
|
|
17
9
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* enum, number, boolean, or a closed set of short diagnostic-id strings.
|
|
30
|
-
* 2. WEIGHTS TABLE, NOT LITERALS. Every score bump v3 hard-coded as an inline
|
|
31
|
-
* integer now reads from a `RouterWeightsV4` (see
|
|
32
|
-
* reasoning-router-v4-weights.ts). `DEFAULT_WEIGHTS_V4` reproduces v3's
|
|
33
|
-
* literals exactly, so classifyTaskV4(input, DEFAULT_WEIGHTS_V4).taskClass
|
|
34
|
-
* matches classifyTaskV3(input) on the full non-holdout GOLD_SET and every
|
|
35
|
-
* focused v3 sentinel (see the 013 regression test and this lane's
|
|
36
|
-
* evidence file). Passing a different `RouterWeightsV4` recalibrates
|
|
37
|
-
* without touching this file.
|
|
38
|
-
* 3. BOUNDED NEGATION. Every whole-prompt keyword/contextual pattern scan
|
|
39
|
-
* (the ones v3 ran as a bare `PATTERN.test(prompt)`) is now negation-aware:
|
|
40
|
-
* a match is only counted if no negation cue (don't/doesn't/isn't/never/
|
|
41
|
-
* avoid/skip/without/instead of/rather than/no need to|for/not a/...) appears within
|
|
42
|
-
* `weights.negationWindowChars` characters immediately before it, and the
|
|
43
|
-
* scan never crosses a `.,;!?` clause boundary. Patterns anchored to the
|
|
44
|
-
* START of the leading clause (hasLeadingDebugAction, hasLeadingReviewIntent,
|
|
45
|
-
* etc.) are untouched and need no negation handling: "don't refactor" can
|
|
46
|
-
* never match an anchored `^refactor` test in the first place. Concrete
|
|
47
|
-
* fix: "don't refactor this, just fix the crash" now classifies as debug
|
|
48
|
-
* (v3 misclassifies this as refactor; see the 013 test).
|
|
49
|
-
* 4. BOUNDED COMPOUND INTENT. A short-range conjunction split (bare "then",
|
|
50
|
-
* "and then", "and also", or ";", only within the first 300 characters)
|
|
51
|
-
* looks for a second, distinct leading-verb intent after the split point.
|
|
52
|
-
* When found and distinct from the primary leading intent, it contributes
|
|
53
|
-
* a bounded `secondClauseIntent` bump and sets `compoundIntent: true` on
|
|
54
|
-
* the verdict. This is proven inert on every current GOLD_SET row (no
|
|
55
|
-
* gold-set prompt produces a non-null, distinct second-clause intent), so
|
|
56
|
-
* it never changes an existing classification under DEFAULT_WEIGHTS_V4 —
|
|
57
|
-
* it only sharpens confidence/margin metadata for genuinely compound
|
|
58
|
-
* prompts and gives future calibration a lever.
|
|
59
|
-
* 5. CONFIDENCE NEVER LOWERS EFFORT. `resolveThinkingLevelV4WithUncertainty`
|
|
60
|
-
* computes the exact same base target as v2/v3's resolvers (rule table +
|
|
61
|
-
* lane step + bias + hint), then ONLY ADDS a bounded escalation step when
|
|
62
|
-
* confidence is low or the class came from the fallback cascade. A prompt
|
|
63
|
-
* like "don't think hard, just fix the crash" cannot use its own text to
|
|
64
|
-
* talk the resolver down: the escalation term is strictly non-negative, so
|
|
65
|
-
* the output is always >= what the same taskClass would resolve to via
|
|
66
|
-
* `resolveThinkingLevelV4ForAuto` (mirrors spec 008 Req 2 acceptance
|
|
67
|
-
* criterion "confidence cannot lower effort").
|
|
68
|
-
* ============================================================================
|
|
10
|
+
* `classifyTaskV4` returns a confidence-bearing verdict: task class, per-class
|
|
11
|
+
* scores, runner-up, margin, confidence band, tie-break flag, fallback reason,
|
|
12
|
+
* bounded-negation audit ids, and compound-intent metadata. None of this carries
|
|
13
|
+
* prompt text — every field is a bounded enum, number, boolean, or a closed set
|
|
14
|
+
* of short diagnostic-id strings.
|
|
15
|
+
*
|
|
16
|
+
* `resolveThinkingLevelV4WithUncertainty` starts from the canonical task-class
|
|
17
|
+
* rule table, applies lane/bias/hint adjustments, and only adds non-negative
|
|
18
|
+
* confidence escalation. A prompt cannot lower its own effort by asking the
|
|
19
|
+
* router to "think less"; low confidence or fallback routing can only hold or
|
|
20
|
+
* raise the resolved level.
|
|
69
21
|
*/
|
|
70
22
|
import type { ThinkingLevel } from "omk-agent-core";
|
|
71
23
|
import { type ReasoningLaneTypeV4, type RouterWeightsV4, type TaskClassV4 } from "./reasoning-router-v4-weights.ts";
|
|
@@ -124,28 +76,31 @@ export interface ClassifierVerdictV4 {
|
|
|
124
76
|
* class from `weights` -> argmax with `TASK_CLASSES_V4` precedence tie-break,
|
|
125
77
|
* tracking the runner-up -> compute margin/confidence/confidenceBand/tieBreak
|
|
126
78
|
* from the RAW scores -> if `topScore <= 0`, replace `taskClass` (and record
|
|
127
|
-
* `fallbackReason`) via the
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
79
|
+
* `fallbackReason`) via the zero-score cascade (fence/diff -> code-gen;
|
|
80
|
+
* length < 40 -> trivial; length >= 2400 -> plan; lane fallback; default
|
|
81
|
+
* code-gen) — the raw scores/margin/runnerUp/tieBreak fields still reflect the
|
|
82
|
+
* pre-cascade computation, for audit purposes.
|
|
131
83
|
*
|
|
132
84
|
* Pure: same (input, weights) -> same ClassifierVerdictV4.
|
|
133
85
|
*/
|
|
134
86
|
export declare function classifyTaskV4(input: TaskClassifierInputV4, weights?: RouterWeightsV4): ClassifierVerdictV4;
|
|
135
|
-
/**
|
|
136
|
-
|
|
87
|
+
/**
|
|
88
|
+
* Static rule table: task class -> recommended ThinkingLevel. Re-exported
|
|
89
|
+
* from the shared resolver core (byte-identical values; previously a
|
|
90
|
+
* duplicated literal here — see reasoning-router-resolver.ts).
|
|
91
|
+
*/
|
|
92
|
+
export declare const TASK_CLASS_THINKING_LEVELS_V4: Readonly<Record<import("./reasoning-router-resolver.ts").ReasoningTaskClass, ThinkingLevel>>;
|
|
137
93
|
/**
|
|
138
94
|
* Thin auto-mode wrapper with no uncertainty adjustment: base rule table ->
|
|
139
|
-
* lane step -> clamp to availableLevels.
|
|
140
|
-
*
|
|
141
|
-
* same (taskClass, availableLevels, laneType) — i.e. v4's "confident" path.
|
|
95
|
+
* lane step -> clamp to availableLevels. This is v4's "confident" path and
|
|
96
|
+
* delegates to the shared resolver core with bias=0, hint=null, escalationSteps=0.
|
|
142
97
|
*/
|
|
143
98
|
export declare function resolveThinkingLevelV4ForAuto(taskClass: TaskClassV4, availableLevels: readonly ThinkingLevel[], laneType: ReasoningLaneTypeV4 | undefined): ThinkingLevel;
|
|
144
99
|
/**
|
|
145
100
|
* Uncertainty-aware resolver (spec 008 Req 2 / plan.md step 5).
|
|
146
101
|
*
|
|
147
102
|
* Pipeline: base rule table for `verdict.taskClass` -> lane step -> bounded
|
|
148
|
-
* bias [-2,+2] -> optional hint fusion (±2 bounded
|
|
103
|
+
* bias [-2,+2] -> optional hint fusion (±2 bounded) -> bounded
|
|
149
104
|
* non-negative confidence escalation (+1 ladder step exactly when
|
|
150
105
|
* `verdict.confidenceBand === "low"` or `verdict.fallbackReason !== null`) ->
|
|
151
106
|
* clamp to `availableLevels`.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reasoning-router-v4.d.ts","sourceRoot":"","sources":["../../src/core/reasoning-router-v4.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoEG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,EAEN,KAAK,mBAAmB,EACxB,KAAK,eAAe,EAEpB,KAAK,WAAW,EAChB,MAAM,kCAAkC,CAAC;AAE1C,YAAY,EAAE,mBAAmB,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AAC1G,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,kCAAkC,CAAC;AA4gBvF;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,mBAAmB,CAAC;IAC/B,OAAO,CAAC,EAAE,SAAS,WAAW,EAAE,CAAC;IACjC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;CAC/B;AAED,gIAAgI;AAChI,MAAM,MAAM,gBAAgB,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;AAEzD,wHAAwH;AACxH,MAAM,MAAM,gBAAgB,GACzB,oBAAoB,GACpB,gBAAgB,GAChB,sBAAsB,GACtB,YAAY,GACZ,eAAe,GACf,SAAS,CAAC;AAEb;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IACnC,qEAAqE;IACrE,QAAQ,CAAC,SAAS,EAAE,WAAW,CAAC;IAChC,mFAAmF;IACnF,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC;IACvD,mGAAmG;IACnG,QAAQ,CAAC,QAAQ,EAAE,WAAW,GAAG,IAAI,CAAC;IACtC,oIAAoI;IACpI,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,gHAAgH;IAChH,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,2FAA2F;IAC3F,QAAQ,CAAC,cAAc,EAAE,gBAAgB,CAAC;IAC1C,qHAAqH;IACrH,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,qGAAqG;IACrG,QAAQ,CAAC,cAAc,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACjD,oHAAoH;IACpH,QAAQ,CAAC,oBAAoB,EAAE,SAAS,MAAM,EAAE,CAAC;IACjD,0GAA0G;IAC1G,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC,oHAAoH;IACpH,QAAQ,CAAC,kBAAkB,EAAE,WAAW,GAAG,IAAI,CAAC;CAChD;AAMD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAC7B,KAAK,EAAE,qBAAqB,EAC5B,OAAO,GAAE,eAAoC,GAC3C,mBAAmB,CAqErB;AASD,yFAAyF;AACzF,eAAO,MAAM,6BAA6B,EAAE,QAAQ,CAAC,MAAM,CAAC,WAAW,EAAE,aAAa,CAAC,CAQtF,CAAC;AAmCF;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAC5C,SAAS,EAAE,WAAW,EACtB,eAAe,EAAE,SAAS,aAAa,EAAE,EACzC,QAAQ,EAAE,mBAAmB,GAAG,SAAS,GACvC,aAAa,CAKf;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,qCAAqC,CACpD,OAAO,EAAE,mBAAmB,EAC5B,eAAe,EAAE,SAAS,aAAa,EAAE,EACzC,QAAQ,EAAE,mBAAmB,GAAG,SAAS,EACzC,IAAI,SAAI,EACR,IAAI,GAAE;IAAE,KAAK,EAAE,aAAa,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAAG,IAAW,GAC9D,aAAa,CAoBf","sourcesContent":["/**\n * Reasoning-router v4 — confidence-bearing classifier (Goal 009 Wave 1 Lane A,\n * specs/008-reasoning-router-advanced-accuracy Requirement 2).\n *\n * v4 is a NEW, OPT-IN, PURE sibling module. It does not import from, and is not\n * imported by, reasoning-router.ts (v1), reasoning-router-v2.ts,\n * reasoning-router-v3.ts, reasoning-router-weights.ts, or agent-session.ts.\n * Activating v4 in the product (a `/think auto-v4` mode, settings wiring, etc.)\n * is explicitly out of scope for this lane and is left to a later,\n * single-writer integration lane (specs/008 plan.md Wave 3).\n *\n * Deterministic by construction: same (input, weights) -> same\n * ClassifierVerdictV4; same (verdict, availableLevels, laneType, bias, hint) ->\n * same ThinkingLevel. No clock, randomness, I/O, model calls, network access,\n * or state mutation anywhere in this file (ThinkingLevel is a type-only import\n * and is erased).\n *\n * ============================================================================\n * WHAT'S NEW VS v3 (reasoning-router-v3.ts)\n * ============================================================================\n * 1. VERDICT, NOT JUST A CLASS. `classifyTaskV4` returns a `ClassifierVerdictV4`\n * carrying the full per-class score map, the runner-up class, the margin,\n * a normalized confidence ratio, a confidence band, a `tieBreak` flag (the\n * top class won only via precedence order over an exact score tie), a\n * `fallbackReason` (non-null exactly when the zero-score cascade decided\n * the class instead of a real signal), and `suppressedFeatureIds` (an\n * audit trail of which whole-prompt signals were nulled by bounded\n * negation). None of this carries prompt text — every field is a bounded\n * enum, number, boolean, or a closed set of short diagnostic-id strings.\n * 2. WEIGHTS TABLE, NOT LITERALS. Every score bump v3 hard-coded as an inline\n * integer now reads from a `RouterWeightsV4` (see\n * reasoning-router-v4-weights.ts). `DEFAULT_WEIGHTS_V4` reproduces v3's\n * literals exactly, so classifyTaskV4(input, DEFAULT_WEIGHTS_V4).taskClass\n * matches classifyTaskV3(input) on the full non-holdout GOLD_SET and every\n * focused v3 sentinel (see the 013 regression test and this lane's\n * evidence file). Passing a different `RouterWeightsV4` recalibrates\n * without touching this file.\n * 3. BOUNDED NEGATION. Every whole-prompt keyword/contextual pattern scan\n * (the ones v3 ran as a bare `PATTERN.test(prompt)`) is now negation-aware:\n * a match is only counted if no negation cue (don't/doesn't/isn't/never/\n * avoid/skip/without/instead of/rather than/no need to|for/not a/...) appears within\n * `weights.negationWindowChars` characters immediately before it, and the\n * scan never crosses a `.,;!?` clause boundary. Patterns anchored to the\n * START of the leading clause (hasLeadingDebugAction, hasLeadingReviewIntent,\n * etc.) are untouched and need no negation handling: \"don't refactor\" can\n * never match an anchored `^refactor` test in the first place. Concrete\n * fix: \"don't refactor this, just fix the crash\" now classifies as debug\n * (v3 misclassifies this as refactor; see the 013 test).\n * 4. BOUNDED COMPOUND INTENT. A short-range conjunction split (bare \"then\",\n * \"and then\", \"and also\", or \";\", only within the first 300 characters)\n * looks for a second, distinct leading-verb intent after the split point.\n * When found and distinct from the primary leading intent, it contributes\n * a bounded `secondClauseIntent` bump and sets `compoundIntent: true` on\n * the verdict. This is proven inert on every current GOLD_SET row (no\n * gold-set prompt produces a non-null, distinct second-clause intent), so\n * it never changes an existing classification under DEFAULT_WEIGHTS_V4 —\n * it only sharpens confidence/margin metadata for genuinely compound\n * prompts and gives future calibration a lever.\n * 5. CONFIDENCE NEVER LOWERS EFFORT. `resolveThinkingLevelV4WithUncertainty`\n * computes the exact same base target as v2/v3's resolvers (rule table +\n * lane step + bias + hint), then ONLY ADDS a bounded escalation step when\n * confidence is low or the class came from the fallback cascade. A prompt\n * like \"don't think hard, just fix the crash\" cannot use its own text to\n * talk the resolver down: the escalation term is strictly non-negative, so\n * the output is always >= what the same taskClass would resolve to via\n * `resolveThinkingLevelV4ForAuto` (mirrors spec 008 Req 2 acceptance\n * criterion \"confidence cannot lower effort\").\n * ============================================================================\n */\n\nimport type { ThinkingLevel } from \"omk-agent-core\";\nimport {\n\tDEFAULT_WEIGHTS_V4,\n\ttype ReasoningLaneTypeV4,\n\ttype RouterWeightsV4,\n\tTASK_CLASSES_V4,\n\ttype TaskClassV4,\n} from \"./reasoning-router-v4-weights.ts\";\n\nexport type { ReasoningLaneTypeV4, RouterWeightsV4, TaskClassV4 } from \"./reasoning-router-v4-weights.ts\";\nexport { DEFAULT_WEIGHTS_V4, TASK_CLASSES_V4 } from \"./reasoning-router-v4-weights.ts\";\n\n/** Prompts shorter than this (trimmed) fall back to trivial when no real signal scores > 0. */\nconst TRIVIAL_MAX_CHARS_V4 = 40;\n/** Prompts at/above this length with no real signal fall back to plan (long prose brief). */\nconst COMPLEX_PROSE_MIN_CHARS_V4 = 2400;\n/** Plan-brief prompts at/above this length count as a \"long brief\" even without PLAN_BRIEF_PATTERN. */\nconst LONG_BRIEF_MIN_CHARS_V4 = 512;\n/** Compound-clause split only applies within this many leading characters (short direct commands, not long prose). */\nconst COMPOUND_SPLIT_MAX_INDEX_V4 = 300;\n/** Minimum trimmed length of a candidate second clause to be worth checking for a leading intent. */\nconst COMPOUND_SECOND_CLAUSE_MIN_CHARS_V4 = 3;\n/** Bounded look-ahead cap when checking a second clause's leading intent (defense in depth; independent of regex cost). */\nconst COMPOUND_SECOND_CLAUSE_SCAN_CHARS_V4 = 200;\n\n/** Lane fallback class, used only when every scored class is <= 0 (zero-score cascade). Identical to v1/v2/v3. */\nconst LANE_FALLBACK_CLASS_V4: Readonly<Record<ReasoningLaneTypeV4, TaskClassV4>> = {\n\tplanner: \"plan\",\n\tsecurity: \"review\",\n\texplorer: \"review\",\n\tcoder: \"code-gen\",\n\treviewer: \"review\",\n\ttester: \"code-gen\",\n};\n\n// ============================================================================\n// Whole-prompt / leading-clause patterns (copied verbatim from\n// reasoning-router-v3.ts; identical regex source so DEFAULT_WEIGHTS_V4\n// reproduces v3 exactly whenever no negation cue is present).\n// ============================================================================\n\nconst LOCAL_EDIT_OBJECT_PATTERN =\n\t/\\b(spelling|grammar|capitalization|date\\s+format|author\\s+e-?mail|copyright\\s+year|headline|title|tooltip|placeholder|punctuation|comma|period|semicolon|closing\\s+html\\s+tag|closing\\s+tag|double\\s+space|whitespace|indentation|table\\s+alignment|typos?|one-?liner?|single\\s+line|sentence|stray|trailing)\\b|오타|맞춤법|띄어쓰기|문구|제목/i;\nconst LOCAL_EDIT_ACTION_OBJECT_PATTERN =\n\t/\\b(update|change|swap|remove|correct|fix|add|adjust|trim)\\s+(?:the\\s+|a\\s+|an\\s+)?(?:missing\\s+|stray\\s+|author\\s+|two\\s+)?(?:e-?mail|comma|period|semicolon|tag|word|words|headline|title|copyright|spelling|grammar)\\b/i;\nconst IMPLEMENTATION_OBJECT_PATTERN =\n\t/\\b(error\\s+handling|input\\s+validation|validation|auth(?:entication)?|retry|endpoint|migration|component|function|utility|helper|module|service|database|schema|column|middleware|rate\\s+limiter|cache|caching|oauth|jwt|webhook)\\b|에러\\s*처리|오류\\s*처리|입력\\s*검증|인증|재시도|엔드포인트|마이그레이션|컴포넌트|함수|유틸|헬퍼|모듈|서비스|데이터베이스|스키마|미들웨어|레이트\\s*리미터|캐시|웹훅|기능/i;\nconst HARD_DIAGNOSTIC_PATTERN =\n\t/\\b(stack\\s*trace|traceback|panic|segfault|crash(?:es|ing|ed)?|throws?|fails?|failing|flaky|hangs?|timeout|500|EADDRINUSE|silently\\s+fails?)\\b|\\b(?:TypeError|ReferenceError|RangeError|Error):|크래시|스택\\s*트레이스|세그폴트|먹통|멈춰|타임아웃/i;\nconst BUG_OBJECT_PATTERN =\n\t/\\b(null\\s+pointer|null\\s+deref|race(?:\\s+condition)?|heap(?:\\s+overflow)?|use-after-free|memory\\s+leak|deadlock|data\\s+corruption|stale\\s+data|off-by-one|encoding\\s+bug|regression\\s+(?:was\\s+)?introduced|exceptions?|assertion\\s+error|bugs?)\\b|버그|예외|메모리\\s*누수|데드락|무한\\s*루프/i;\nconst NON_DIAGNOSTIC_DEBUG_CONTEXT_PATTERN = /\\berror\\s+(?:handling|messages?|budgets?)\\b|에러\\s*처리|오류\\s*처리/i;\nconst GENERIC_DIAGNOSTIC_PATTERN = /\\b(errors?|broken|wrong\\s+results|rolls?\\s+back|rollback)\\b/i;\nconst PLAN_BRIEF_PATTERN =\n\t/\\b(context\\s+and\\s+constraints|starting\\s+state|target\\s+state|cross-cutting|deliver:|deliverables?|roadmap|migration\\s+wave|top\\s+(?:ten\\s+)?risks|component\\s+diagram|data\\s+model|architecture|go\\/no-go|phased\\s+(?:delivery|rollout)|bounded\\s+contexts?|strangler\\s+fig|event-driven|quarter-by-quarter|milestone)\\b/i;\nconst OPERATIONAL_RUNBOOK_SIGNAL_PATTERNS: readonly { pattern: RegExp; critical: boolean }[] = [\n\t{ pattern: /\\b(?:commit|commits|committed|committing)\\b|커밋/i, critical: false },\n\t{ pattern: /\\b(?:push|pushed|pushing)\\b|푸시|푸쉬/i, critical: true },\n\t{\n\t\tpattern: /\\b(?:release|releases|tag|version\\s+bump|bump\\s+(?:the\\s+)?version)\\b|릴리즈|태그|버전/i,\n\t\tcritical: true,\n\t},\n\t{ pattern: /\\bnpm\\s+publish\\b|\\bpublish(?:ing|ed)?\\b|퍼블리시|배포/i, critical: true },\n\t{ pattern: /\\b(?:CHANGELOG\\.md|changelog|release\\s+notes?)\\b|채널로그|체인지로그/i, critical: false },\n\t{ pattern: /\\bREADME\\.md\\b|\\breadme\\b/i, critical: false },\n\t{ pattern: /\\bci\\s*\\/\\s*cd\\b|\\bgithub\\s+actions?\\b|\\bworkflow\\b|깃허브\\s*액션|깃헙\\s*액션/i, critical: false },\n];\nconst EXPLICIT_RELEASE_RUNBOOK_PATTERN =\n\t/\\bnpm\\s+publish\\b|\\brelease\\b[^\\n]{0,80}\\bv?\\d+\\.\\d+\\.\\d+\\b|릴리즈[^\\n]{0,80}\\d+\\.\\d+\\.\\d+/i;\nconst REVIEW_SCOPE_PATTERN =\n\t/\\b(pr|pull\\s+request|diff|codebase|strategy|plan|spec|schema|design|api|coverage|security\\s+posture|licensing|risks?|edge\\s+cases|clarity|consistency|dependencies|third-party|ci\\s+pipeline|threat\\s+model|retry\\s+logic|error\\s+handling\\s+strategy|error\\s+messages?)\\b/i;\nconst REFACTOR_CUE_PATTERN =\n\t/\\b(refactor(?:ing|ed)?|extract|rename|deduplicate|consolidate|modularize|reorganize|simplify|clean\\s*up|restructure|split\\s+module|move\\s+logic|untangle|merge\\s+duplicate)\\b|리팩토링(?!하지\\s*마|하지\\s*말)|리팩터링(?!하지\\s*마|하지\\s*말)|구조\\s*개선/i;\nconst ADD_KEYWORD_PATTERN = /\\badd\\b/i;\nconst LOW_RISK_EDIT_ACTION_PATTERN = /^(?:correct|update|swap|remove|reword|tweak|bump|trim)\\b/i;\n\n/** Whole-prompt keyword-family patterns (mirrors v3's KEYWORD_PATTERNS_V3; refactor/simple-edit reuse the patterns above). */\nconst DEBUG_KEYWORD_FAMILY_PATTERN =\n\t/^(?:debug|investigate\\s+why|reproduce|trace\\b)|\\bfix\\s+this\\s+(?:traceback|panic|error)\\b|디버깅|디버그|재현|원인\\s*분석/i;\nconst REVIEW_KEYWORD_FAMILY_PATTERN =\n\t/\\b(review|critique|assess|inspect|approve|lgtm|double-?check|audit(?!\\s+log\\b))\\b|리뷰|검토|점검/i;\nconst PLAN_KEYWORD_FAMILY_PATTERN =\n\t/\\b(plan|design|architect|architecture|roadmap|spec(?:ification)?|strategy|decompose|milestones?|write\\s+a\\s+(?:technical\\s+)?spec|create\\s+a\\s+(?:roadmap|strategy|plan))\\b|설계|로드맵|아키텍처|계획\\s*세워|기획/i;\nconst CODE_GEN_KEYWORD_FAMILY_PATTERN =\n\t/\\b(implement|write|create|build|generate|scaffold|prototype)\\b|구현|만들어\\s*줘|생성|작성|추가해/i;\nconst KO_SHORT_DEBUG_SIGNAL_PATTERN_V4 =\n\t/오류|에러|실패|안\\s*돼|안\\s*됨|깨졌|깨짐|고장|디버깅|디버그|재현|원인\\s*(?:분석|파악)?/i;\nconst KO_SHORT_SIMPLE_EDIT_SIGNAL_PATTERN_V4 = /오타|맞춤법|띄어쓰기|문구|제목/i;\nconst KO_SHORT_REVIEW_SIGNAL_PATTERN_V4 = /리뷰|검토|점검/i;\nconst KO_SHORT_PLAN_SIGNAL_PATTERN_V4 = /설계|로드맵|아키텍처|계획\\s*세워|기획/i;\nconst KO_SHORT_REFACTOR_SIGNAL_PATTERN_V4 = /리팩토링|리팩터링|구조\\s*개선/i;\nconst KO_SHORT_CODE_GEN_SIGNAL_PATTERN_V4 =\n\t/구현|만들|생성|작성|추가|테스트|삭제|지워|지우|제거|고쳐|고치|수정|변경|바꿔|바꾸|옮겨|옮기|넣어/i;\n\n/** Short-range compound-clause conjunction boundary. Only bare/compound \"then\"/\"and also\"/\";\" — never bare \"and\". */\nconst COMPOUND_SPLIT_PATTERN = /\\band\\s+then\\b|\\bthen\\b|\\band\\s+also\\b|;/i;\n\n/**\n * Bounded negation-cue vocabulary (spec 008 plan.md \"bounded negation window\").\n * Deliberately excludes bare \"not\" (too broad; verified against the current\n * GOLD_SET that every listed cue here is either absent or occurs AFTER, not\n * before, a tracked keyword — see this lane's evidence file for the audit).\n */\nconst NEGATION_CUE_PATTERN =\n\t/\\b(?:don't|do\\s+not|doesn't|does\\s+not|didn't|did\\s+not|won't|will\\s+not|shouldn't|should\\s+not|wouldn't|would\\s+not|can't|cannot|isn't|is\\s+not|aren't|are\\s+not|never|avoid|skip|without|instead\\s+of|rather\\s+than|no\\s+need\\s+(?:to|for)|no\\s+longer\\s+need(?:\\s+(?:to|for))?)\\b/i;\nconst DIRECT_NOT_OBJECT_CUE_PATTERN_V4 = /\\b(?:not|no)\\s+(?:a\\s+|an\\s+|the\\s+)?$/i;\nconst DOUBLE_NEGATION_RESCUE_PATTERN_V4 =\n\t/\\b(?:don't|do\\s+not|doesn't|does\\s+not|didn't|did\\s+not|won't|will\\s+not|shouldn't|should\\s+not|wouldn't|would\\s+not|can't|cannot)\\s+(?:skip|avoid)\\s+(?:the\\s+|a\\s+|an\\s+)?$/i;\nconst DELIBERATIVE_QUESTION_RESCUE_PATTERN_V4 =\n\t/\\b(?:shouldn't|should\\s+not|wouldn't|would\\s+not|can't|cannot)\\s+(?:we|i|you)\\s*$/i;\nconst POSTPOSITIONED_NEGATION_CUE_PATTERN_V4 =\n\t/^\\s*(?:-\\s*free\\b|(?:is|are|was|were)?\\s*(?:not\\s+(?:needed|required|desired)|unnecessary|not\\s+necessary)|(?:[은는이가을를도만]\\s*)?(?:하지\\s*(?:마(?:라|세요|십시오)?|말(?:고|아|라)?|않(?:아|는|고|을|게)?)|말고|금지))/i;\n\n/** A closed set of clause-boundary characters; a negation cue never reaches across one of these into a prior clause. */\nconst CLAUSE_BOUNDARY_CHARS = [\".\", \"!\", \"?\", \";\", \",\"] as const;\n\nfunction hasCodeFence(text: string): boolean {\n\treturn text.includes(\"```\");\n}\n\n/** Identical to v1/v2/v3: explicit hunk headers or `diff --git` count alone; bare +/- only count together. */\nfunction hasDiffMarkers(text: string): boolean {\n\tif (/^@@[^\\n]*@@/m.test(text) || /^diff --git /m.test(text)) return true;\n\treturn /^\\+(?!\\+)/m.test(text) && /^-(?!-)/m.test(text);\n}\n\n/** Identical to v3: first line (after stripping one polite prefix), capped at 180 chars. */\nfunction firstClause(prompt: string): string {\n\tconst firstLine =\n\t\tprompt.replace(/^(?:please|pls|can you|could you|would you|help me|i need you to)\\s+/i, \"\").split(\"\\n\")[0] ?? \"\";\n\treturn firstLine.slice(0, 180);\n}\n\n// --- Leading-clause intent tests. All are `^`-anchored against the LEADING\n// clause text only, so a negation cue at the start (\"don't refactor...\") can\n// never match one of these: none of the alternations include \"don't\"/\"never\"/\n// etc, so they are negation-immune by construction and are left unchanged\n// from v3. ---\n\nfunction hasLeadingReviewIntent(text: string): boolean {\n\treturn /^(?:review|audit(?!\\s+log\\b)|critique|inspect|assess|approve|double-?check|lgtm)\\b/i.test(text);\n}\n\nfunction hasLeadingPlanIntent(text: string): boolean {\n\treturn /^(?:plan|design|architect|decompose)\\b|^(?:write|create)\\s+(?:a\\s+)?(?:technical\\s+)?(?:spec|roadmap|strategy|plan)\\b/i.test(\n\t\ttext,\n\t);\n}\n\nfunction hasLeadingRefactorIntent(text: string): boolean {\n\treturn /^(?:refactor(?:ing|ed)?|extract|rename|deduplicate|consolidate|modularize|reorganize|simplify|restructure|untangle)\\b|^clean\\s+up\\b|^split\\s+(?:the\\s+)?module\\b|^move\\s+logic\\b/i.test(\n\t\ttext,\n\t);\n}\n\nfunction hasLeadingDebugAction(text: string): boolean {\n\treturn /^(?:debug|investigate\\s+why|reproduce|trace\\b)|^fix\\s+this\\s+(?:traceback|panic|error)\\b/i.test(text);\n}\n\nfunction hasLeadingCodeGenIntent(text: string): boolean {\n\treturn /^(?:implement|write|create|build|generate|scaffold|prototype|add)\\b/i.test(text);\n}\n\nfunction hasLeadingSimpleEditIntent(text: string): boolean {\n\treturn /^(?:correct|update|swap|remove|adjust|fix|add|change|trim|reword|tweak|bump)\\b/i.test(text);\n}\n\n// ============================================================================\n// Bounded negation gating\n// ============================================================================\n\ninterface UnnegatedMatchResultV4 {\n\t/** True when the pattern matched at least once and at least one occurrence was NOT preceded by a negation cue. */\n\treadonly matched: boolean;\n\t/** True when the pattern matched at least once but EVERY occurrence was negated (useful for suppressedFeatureIds). */\n\treadonly suppressed: boolean;\n}\n\nconst NO_MATCH_RESULT_V4: UnnegatedMatchResultV4 = { matched: false, suppressed: false };\n\nfunction matchOperationalRunbookV4(prompt: string, windowChars: number): UnnegatedMatchResultV4 {\n\tlet distinctSignals = 0;\n\tlet criticalSignals = 0;\n\tlet sawSuppressed = false;\n\tfor (const { pattern, critical } of OPERATIONAL_RUNBOOK_SIGNAL_PATTERNS) {\n\t\tconst result = matchUnnegated(prompt, pattern, windowChars);\n\t\tif (result.matched) {\n\t\t\tdistinctSignals += 1;\n\t\t\tif (critical) criticalSignals += 1;\n\t\t}\n\t\tif (result.suppressed) sawSuppressed = true;\n\t}\n\tconst explicitRelease = matchUnnegated(prompt, EXPLICIT_RELEASE_RUNBOOK_PATTERN, windowChars).matched;\n\tconst matched = explicitRelease || distinctSignals >= 3 || (criticalSignals >= 2 && distinctSignals >= 2);\n\treturn { matched, suppressed: !matched && sawSuppressed };\n}\n\n/**\n * Negation-aware replacement for v3's bare `pattern.test(prompt)`. Finds every\n * occurrence of `pattern` in `prompt`; for each, looks back up to\n * `windowChars` characters (never crossing a `.,;!?` boundary into a prior\n * clause) for a `NEGATION_CUE_PATTERN` hit, and looks forward inside the same\n * bounded window for Hangul-only post-positioned negation cues such as\n * \"하지 말고\". `matched` is true iff at least one occurrence survives\n * un-negated. Deterministic, single pass per pattern, no shared regex state (a\n * fresh global-flag RegExp is constructed per call).\n */\nfunction hasPrePositionedNegationCueV4(scoped: string): boolean {\n\tif (!NEGATION_CUE_PATTERN.test(scoped) && !DIRECT_NOT_OBJECT_CUE_PATTERN_V4.test(scoped)) return false;\n\treturn !DOUBLE_NEGATION_RESCUE_PATTERN_V4.test(scoped) && !DELIBERATIVE_QUESTION_RESCUE_PATTERN_V4.test(scoped);\n}\n\nfunction hasPostPositionedNegationCueV4(prompt: string, matchEnd: number, windowChars: number): boolean {\n\tconst scoped = prompt.slice(matchEnd, Math.min(prompt.length, matchEnd + windowChars));\n\treturn POSTPOSITIONED_NEGATION_CUE_PATTERN_V4.test(scoped);\n}\n\nfunction matchUnnegated(prompt: string, pattern: RegExp, windowChars: number): UnnegatedMatchResultV4 {\n\tconst flags = pattern.flags.includes(\"g\") ? pattern.flags : `${pattern.flags}g`;\n\tconst globalPattern = new RegExp(pattern.source, flags);\n\tlet sawMatch = false;\n\tlet sawUnnegated = false;\n\tlet match = globalPattern.exec(prompt);\n\twhile (match !== null) {\n\t\tsawMatch = true;\n\t\tconst matchStart = match.index;\n\t\tconst windowStart = Math.max(0, matchStart - windowChars);\n\t\tconst windowText = prompt.slice(windowStart, matchStart);\n\t\tlet boundary = -1;\n\t\tfor (const boundaryChar of CLAUSE_BOUNDARY_CHARS) {\n\t\t\tconst idx = windowText.lastIndexOf(boundaryChar);\n\t\t\tif (idx > boundary) boundary = idx;\n\t\t}\n\t\tconst scoped = boundary >= 0 ? windowText.slice(boundary + 1) : windowText;\n\t\tconst preNegated = hasPrePositionedNegationCueV4(scoped);\n\t\tconst postNegated =\n\t\t\t!preNegated && hasPostPositionedNegationCueV4(prompt, matchStart + match[0].length, windowChars);\n\t\tif (!preNegated && !postNegated) sawUnnegated = true;\n\t\tif (match[0].length === 0) globalPattern.lastIndex += 1;\n\t\tmatch = globalPattern.exec(prompt);\n\t}\n\treturn { matched: sawUnnegated, suppressed: sawMatch && !sawUnnegated };\n}\n\n// ============================================================================\n// Bounded compound-intent detection\n// ============================================================================\n\n/**\n * Splits off a bounded second clause after a short-range conjunction\n * (\"then\"/\"and then\"/\"and also\"/\";\"), only when the split point is within the\n * first `COMPOUND_SPLIT_MAX_INDEX_V4` characters (short direct commands, not\n * long prose briefs — verified this never fires inside the GOLD_SET's\n * >=2400-char plan entries). Returns null when no qualifying split exists.\n */\nfunction splitCompoundClauseV4(prompt: string): string | null {\n\tconst match = COMPOUND_SPLIT_PATTERN.exec(prompt);\n\tif (match === null || match.index > COMPOUND_SPLIT_MAX_INDEX_V4) return null;\n\tconst second = prompt.slice(match.index + match[0].length).trim();\n\tif (second.length < COMPOUND_SECOND_CLAUSE_MIN_CHARS_V4) return null;\n\treturn second;\n}\n\n/**\n * Leading-intent test for a compound prompt's SECOND clause. Reuses the same\n * `^`-anchored leading-intent tests as the primary clause (so it is equally\n * negation-immune), without the primary clause's diagnosticEvidence/localEdit\n * gating (a short second-clause fragment carries no such context of its own).\n */\nfunction secondClauseLeadingIntentV4(clause: string): TaskClassV4 | null {\n\tconst bounded = clause.slice(0, COMPOUND_SECOND_CLAUSE_SCAN_CHARS_V4);\n\treturn leadingIntentFromLeadingClauseV4(bounded);\n}\n\nfunction leadingIntentFromLeadingClauseV4(leading: string): TaskClassV4 | null {\n\tif (hasLeadingDebugAction(leading)) return \"debug\";\n\tif (hasLeadingReviewIntent(leading)) return \"review\";\n\tif (hasLeadingPlanIntent(leading)) return \"plan\";\n\tif (hasLeadingRefactorIntent(leading)) return \"refactor\";\n\tif (hasLeadingSimpleEditIntent(leading)) return \"simple-edit\";\n\tif (hasLeadingCodeGenIntent(leading)) return \"code-gen\";\n\treturn null;\n}\n\nfunction leadingIntentIsPostNegatedV4(leading: string, windowChars: number): boolean {\n\tconst leadingIntentMatch =\n\t\t/^(?:debug|investigate\\s+why|reproduce|trace\\b|fix\\s+this\\s+(?:traceback|panic|error)|review|audit(?!\\s+log\\b)|critique|inspect|assess|approve|double-?check|lgtm|plan|design|architect|decompose|write\\s+(?:a\\s+)?(?:technical\\s+)?(?:spec|roadmap|strategy|plan)|create\\s+(?:a\\s+)?(?:technical\\s+)?(?:spec|roadmap|strategy|plan)|refactor(?:ing|ed)?|extract|rename|deduplicate|consolidate|modularize|reorganize|simplify|restructure|untangle|clean\\s+up|split\\s+(?:the\\s+)?module|move\\s+logic|correct|update|swap|remove|adjust|fix|add|change|trim|reword|tweak|bump|implement|build|generate|scaffold|prototype)\\b/i.exec(\n\t\t\tleading,\n\t\t);\n\treturn (\n\t\tleadingIntentMatch !== null && hasPostPositionedNegationCueV4(leading, leadingIntentMatch[0].length, windowChars)\n\t);\n}\n\nfunction classifyShortKoreanZeroScoreTaskV4(prompt: string, windowChars: number): TaskClassV4 | null {\n\tif (matchUnnegated(prompt, KO_SHORT_DEBUG_SIGNAL_PATTERN_V4, windowChars).matched) return \"debug\";\n\tif (matchUnnegated(prompt, KO_SHORT_SIMPLE_EDIT_SIGNAL_PATTERN_V4, windowChars).matched) return \"simple-edit\";\n\tif (matchUnnegated(prompt, KO_SHORT_REVIEW_SIGNAL_PATTERN_V4, windowChars).matched) return \"review\";\n\tif (matchUnnegated(prompt, KO_SHORT_PLAN_SIGNAL_PATTERN_V4, windowChars).matched) return \"plan\";\n\tif (matchUnnegated(prompt, KO_SHORT_REFACTOR_SIGNAL_PATTERN_V4, windowChars).matched) return \"refactor\";\n\tif (matchUnnegated(prompt, KO_SHORT_CODE_GEN_SIGNAL_PATTERN_V4, windowChars).matched) return \"code-gen\";\n\treturn null;\n}\n\n// ============================================================================\n// Feature extraction\n// ============================================================================\n\ninterface ContextualFeaturesV4 {\n\treadonly firstClause: string;\n\treadonly codeFence: boolean;\n\treadonly diffHunk: boolean;\n\treadonly localEdit: boolean;\n\treadonly diagnosticEvidence: boolean;\n\treadonly reviewScope: boolean;\n\treadonly planBrief: boolean;\n\treadonly operationalRunbook: boolean;\n\treadonly refactorCue: boolean;\n\treadonly implementationObject: boolean;\n\treadonly leadingIntent: TaskClassV4 | null;\n\treadonly secondClauseIntent: TaskClassV4 | null;\n\treadonly compoundIntent: boolean;\n\treadonly keywordFamilyMatch: Readonly<Record<TaskClassV4, boolean>>;\n\treadonly addKeywordMatch: boolean;\n}\n\ninterface LeadingIntentInputV4 {\n\treadonly firstClause: string;\n\treadonly localEdit: boolean;\n\treadonly diagnosticEvidence: boolean;\n}\n\n/** Identical decision order to v3's `leadingIntent`. */\nfunction leadingIntentV4(input: LeadingIntentInputV4): TaskClassV4 | null {\n\tif (\n\t\tinput.diagnosticEvidence &&\n\t\t(hasLeadingDebugAction(input.firstClause) || hasLeadingSimpleEditIntent(input.firstClause))\n\t)\n\t\treturn \"debug\";\n\tif (hasLeadingReviewIntent(input.firstClause)) return \"review\";\n\tif (hasLeadingPlanIntent(input.firstClause)) return \"plan\";\n\tif (hasLeadingRefactorIntent(input.firstClause)) return \"refactor\";\n\tif (input.localEdit && hasLeadingSimpleEditIntent(input.firstClause)) return \"simple-edit\";\n\tif (!input.localEdit && hasLeadingCodeGenIntent(input.firstClause)) return \"code-gen\";\n\treturn null;\n}\n\n/** Identical decision structure to v3's `hasDiagnosticEvidence`, parameterized over precomputed negation-aware matches. */\nfunction hasDiagnosticEvidenceV4(\n\tleading: string,\n\thardDiagnosticMatched: boolean,\n\tbugObjectMatched: boolean,\n\tgenericDiagnosticMatched: boolean,\n\tnonDiagnosticContext: boolean,\n): boolean {\n\tif (hasLeadingDebugAction(leading) || hardDiagnosticMatched) return true;\n\tconst hasContextualDiagnostic = bugObjectMatched || (genericDiagnosticMatched && !nonDiagnosticContext);\n\tif (!hasContextualDiagnostic) return false;\n\treturn !hasLeadingReviewIntent(leading) && !hasLeadingPlanIntent(leading) && !hasLeadingRefactorIntent(leading);\n}\n\n/**\n * Extracts every v4 feature from one prompt, negation-gating every\n * whole-prompt scan exactly once and recording a `negation:<channel>` id in\n * `suppressed` whenever a match existed but every occurrence was negated.\n */\nfunction extractFeaturesV4(prompt: string, weights: RouterWeightsV4, suppressed: string[]): ContextualFeaturesV4 {\n\tconst leading = firstClause(prompt);\n\tconst window = weights.negationWindowChars;\n\n\tconst implementationObjectResult = matchUnnegated(prompt, IMPLEMENTATION_OBJECT_PATTERN, window);\n\tif (implementationObjectResult.suppressed) suppressed.push(\"negation:implementation-object\");\n\tconst implementationObject = implementationObjectResult.matched;\n\n\tconst lowRiskEditAction = LOW_RISK_EDIT_ACTION_PATTERN.test(leading);\n\tconst localEditObjectResult = matchUnnegated(prompt, LOCAL_EDIT_OBJECT_PATTERN, window);\n\tif (localEditObjectResult.suppressed) suppressed.push(\"negation:local-edit-object\");\n\tconst localEditActionResult = matchUnnegated(prompt, LOCAL_EDIT_ACTION_OBJECT_PATTERN, window);\n\tif (localEditActionResult.suppressed) suppressed.push(\"negation:local-edit-action-object\");\n\tconst localEdit =\n\t\t!implementationObject && (lowRiskEditAction || localEditObjectResult.matched || localEditActionResult.matched);\n\n\tconst hardDiagnosticResult = matchUnnegated(prompt, HARD_DIAGNOSTIC_PATTERN, window);\n\tif (hardDiagnosticResult.suppressed) suppressed.push(\"negation:hard-diagnostic\");\n\tconst bugObjectResult = matchUnnegated(prompt, BUG_OBJECT_PATTERN, window);\n\tif (bugObjectResult.suppressed) suppressed.push(\"negation:bug-object\");\n\tconst genericDiagnosticResult = matchUnnegated(prompt, GENERIC_DIAGNOSTIC_PATTERN, window);\n\tconst nonDiagnosticContext = NON_DIAGNOSTIC_DEBUG_CONTEXT_PATTERN.test(prompt);\n\tif (genericDiagnosticResult.suppressed && !nonDiagnosticContext) suppressed.push(\"negation:generic-diagnostic\");\n\tconst diagnosticEvidence = hasDiagnosticEvidenceV4(\n\t\tleading,\n\t\thardDiagnosticResult.matched,\n\t\tbugObjectResult.matched,\n\t\tgenericDiagnosticResult.matched,\n\t\tnonDiagnosticContext,\n\t);\n\n\tconst reviewScopeResult = matchUnnegated(prompt, REVIEW_SCOPE_PATTERN, window);\n\tif (reviewScopeResult.suppressed) suppressed.push(\"negation:review-scope\");\n\tconst reviewScope = hasLeadingReviewIntent(leading) && reviewScopeResult.matched;\n\n\tconst planBriefResult = matchUnnegated(prompt, PLAN_BRIEF_PATTERN, window);\n\tif (planBriefResult.suppressed) suppressed.push(\"negation:plan-brief\");\n\tconst planBrief =\n\t\thasLeadingPlanIntent(leading) && (planBriefResult.matched || prompt.length >= LONG_BRIEF_MIN_CHARS_V4);\n\n\tconst operationalRunbookResult = matchOperationalRunbookV4(prompt, window);\n\tif (operationalRunbookResult.suppressed) suppressed.push(\"negation:operational-runbook\");\n\tconst operationalRunbook = operationalRunbookResult.matched;\n\n\tconst refactorCueResult = matchUnnegated(prompt, REFACTOR_CUE_PATTERN, window);\n\tif (refactorCueResult.suppressed) suppressed.push(\"negation:refactor-cue\");\n\tconst refactorCue = refactorCueResult.matched;\n\n\tconst rawPrimaryIntent = leadingIntentV4({ firstClause: leading, localEdit, diagnosticEvidence });\n\tconst primaryIntent =\n\t\trawPrimaryIntent !== null && leadingIntentIsPostNegatedV4(leading, window) ? null : rawPrimaryIntent;\n\n\tconst secondClauseText = splitCompoundClauseV4(prompt);\n\tconst secondClauseIntent = secondClauseText === null ? null : secondClauseLeadingIntentV4(secondClauseText);\n\tconst compoundIntent = secondClauseIntent !== null && secondClauseIntent !== primaryIntent;\n\n\tconst debugKeywordResult = diagnosticEvidence\n\t\t? matchUnnegated(prompt, DEBUG_KEYWORD_FAMILY_PATTERN, window)\n\t\t: NO_MATCH_RESULT_V4;\n\tif (debugKeywordResult.suppressed) suppressed.push(\"negation:keyword-debug\");\n\n\tconst reviewKeywordResult = matchUnnegated(prompt, REVIEW_KEYWORD_FAMILY_PATTERN, window);\n\tif (reviewKeywordResult.suppressed) suppressed.push(\"negation:keyword-review\");\n\n\tconst planKeywordResult = matchUnnegated(prompt, PLAN_KEYWORD_FAMILY_PATTERN, window);\n\tif (planKeywordResult.suppressed) suppressed.push(\"negation:keyword-plan\");\n\n\tconst codeGenKeywordResult = matchUnnegated(prompt, CODE_GEN_KEYWORD_FAMILY_PATTERN, window);\n\tif (codeGenKeywordResult.suppressed) suppressed.push(\"negation:keyword-code-gen\");\n\n\tconst addKeywordResult = localEdit ? NO_MATCH_RESULT_V4 : matchUnnegated(prompt, ADD_KEYWORD_PATTERN, window);\n\tif (addKeywordResult.suppressed) suppressed.push(\"negation:add-keyword\");\n\n\treturn {\n\t\tfirstClause: leading,\n\t\tcodeFence: hasCodeFence(prompt),\n\t\tdiffHunk: hasDiffMarkers(prompt),\n\t\tlocalEdit,\n\t\tdiagnosticEvidence,\n\t\treviewScope,\n\t\tplanBrief,\n\t\toperationalRunbook,\n\t\trefactorCue,\n\t\timplementationObject,\n\t\tleadingIntent: primaryIntent,\n\t\tsecondClauseIntent,\n\t\tcompoundIntent,\n\t\tkeywordFamilyMatch: {\n\t\t\ttrivial: false,\n\t\t\tdebug: debugKeywordResult.matched,\n\t\t\trefactor: refactorCue,\n\t\t\treview: reviewKeywordResult.matched,\n\t\t\tplan: planKeywordResult.matched,\n\t\t\t\"simple-edit\": localEditObjectResult.matched,\n\t\t\t\"code-gen\": codeGenKeywordResult.matched,\n\t\t},\n\t\taddKeywordMatch: addKeywordResult.matched,\n\t};\n}\n\n// ============================================================================\n// Scoring\n// ============================================================================\n\nfunction emptyScoresV4(): Record<TaskClassV4, number> {\n\treturn {\n\t\ttrivial: 0,\n\t\t\"simple-edit\": 0,\n\t\t\"code-gen\": 0,\n\t\tdebug: 0,\n\t\trefactor: 0,\n\t\treview: 0,\n\t\tplan: 0,\n\t};\n}\n\nfunction computeScoresV4(features: ContextualFeaturesV4, weights: RouterWeightsV4): Record<TaskClassV4, number> {\n\tconst scores = emptyScoresV4();\n\tif (features.codeFence || features.diffHunk) scores[\"code-gen\"] += weights.codeFenceOrDiff;\n\tif (features.leadingIntent !== null) scores[features.leadingIntent] += weights.leadingIntent;\n\tif (features.secondClauseIntent !== null) scores[features.secondClauseIntent] += weights.secondClauseIntent;\n\tif (features.localEdit) scores[\"simple-edit\"] += weights.localEdit;\n\tif (features.diagnosticEvidence) scores.debug += weights.diagnosticEvidence;\n\tif (features.reviewScope) scores.review += weights.reviewScope;\n\tif (features.planBrief) scores.plan += weights.planBrief;\n\tif (features.operationalRunbook) scores.plan += weights.operationalRunbook;\n\tif (features.refactorCue) scores.refactor += weights.refactorCue;\n\tif (features.implementationObject) scores[\"code-gen\"] += weights.implementationObject;\n\n\tfor (const taskClass of TASK_CLASSES_V4) {\n\t\tif (features.keywordFamilyMatch[taskClass]) scores[taskClass] += weights.keywordFamily[taskClass];\n\t}\n\tif (features.addKeywordMatch) scores[\"code-gen\"] += weights.addKeyword;\n\n\treturn scores;\n}\n\n/**\n * Extension signals (history / context-pressure / judge vote). Every\n * coefficient is 0 under DEFAULT_WEIGHTS_V4 (inert until calibrated by a\n * future governance-backed lane); the mechanism exists so\n * `TaskClassifierInputV4`'s optional fields are meaningfully wired rather than\n * silently accepted-and-ignored (as they are in v3's identical-shaped input).\n */\nfunction applyExtensionSignalsV4(\n\tscores: Record<TaskClassV4, number>,\n\tinput: TaskClassifierInputV4,\n\tweights: RouterWeightsV4,\n): void {\n\tconst priorClass = input.history !== undefined && input.history.length > 0 ? input.history[0] : null;\n\tif (priorClass !== null && weights.multiTurnPrior !== 0) scores[priorClass] += weights.multiTurnPrior;\n\n\tconst judgeVote = input.judgeVote ?? null;\n\tif (judgeVote !== null && weights.judgeVote !== 0) scores[judgeVote] += weights.judgeVote;\n\n\tconst pressureBucket = input.pressureBucket ?? 0;\n\tif (pressureBucket > 0 && weights.pressureBucket !== 0) {\n\t\tconst bump = pressureBucket * weights.pressureBucket;\n\t\tscores.debug += bump;\n\t\tscores.review += bump;\n\t\tscores.plan += bump;\n\t}\n}\n\n// ============================================================================\n// Public types\n// ============================================================================\n\n/**\n * Caller-supplied turn input. Identical shape to `TaskClassifierInputV3` /\n * `TaskClassifierInputV2` (prompt + optional lane/history/pressure/judge\n * slots), so existing callers can switch versions without reshaping data.\n */\nexport interface TaskClassifierInputV4 {\n\tprompt: string;\n\tlaneType?: ReasoningLaneTypeV4;\n\thistory?: readonly TaskClassV4[];\n\tpressureBucket?: number;\n\tjudgeVote?: TaskClassV4 | null;\n}\n\n/** Confidence band derived from `margin / topScore`, bucketed by `weights.lowConfidenceThreshold`/`highConfidenceThreshold`. */\nexport type ConfidenceBandV4 = \"low\" | \"medium\" | \"high\";\n\n/** Which branch of the zero-score fallback cascade decided `taskClass`; null when a real weighted signal decided it. */\nexport type FallbackReasonV4 =\n\t| \"code-fence-or-diff\"\n\t| \"trivial-length\"\n\t| \"ko-short-task-signal\"\n\t| \"long-prose\"\n\t| \"lane-fallback\"\n\t| \"default\";\n\n/**\n * The full, privacy-safe classification verdict (spec 008 Req 2). Every field\n * is a bounded enum, number, boolean, or a closed set of short diagnostic-id\n * strings (`suppressedFeatureIds`) — never raw prompt text, so this value is\n * safe to pass to evaluation, learning, and Adaptorch-advisory code.\n */\nexport interface ClassifierVerdictV4 {\n\t/** The decided task class (post zero-score-cascade if one fired). */\n\treadonly taskClass: TaskClassV4;\n\t/** Raw per-class score map from the weighted signals (pre-cascade; audit-only). */\n\treadonly scores: Readonly<Record<TaskClassV4, number>>;\n\t/** Second-highest-scoring class by the argmax, or null if every other class tied at the bottom. */\n\treadonly runnerUp: TaskClassV4 | null;\n\t/** `scores[argmaxClass] - scores[runnerUp]` (pre-cascade); 0 or negative-margin-free by construction (weights are non-negative). */\n\treadonly margin: number;\n\t/** Normalized confidence in [0, 1]: `margin / topScore`, or 0 when `topScore <= 0` (fallback cascade fired). */\n\treadonly confidence: number;\n\t/** Confidence bucketed by `weights.lowConfidenceThreshold` / `highConfidenceThreshold`. */\n\treadonly confidenceBand: ConfidenceBandV4;\n\t/** True when the argmax class won only via precedence-order tie-break over an exact score tie with the runner-up. */\n\treadonly tieBreak: boolean;\n\t/** Non-null exactly when the zero-score cascade (not a real weighted signal) decided `taskClass`. */\n\treadonly fallbackReason: FallbackReasonV4 | null;\n\t/** Audit trail of `negation:<channel>` ids for every whole-prompt signal that had a match but was fully negated. */\n\treadonly suppressedFeatureIds: readonly string[];\n\t/** True when a distinct second-clause leading intent was detected (bounded compound-intent detection). */\n\treadonly compoundIntent: boolean;\n\t/** The second clause's own leading intent, or null when no compound structure (or no distinct intent) was found. */\n\treadonly secondClauseIntent: TaskClassV4 | null;\n}\n\n// ============================================================================\n// Classifier\n// ============================================================================\n\n/**\n * Deterministic confidence-bearing classifier (spec 008 Req 2).\n *\n * Pipeline: extract negation/compound-aware contextual features -> score every\n * class from `weights` -> argmax with `TASK_CLASSES_V4` precedence tie-break,\n * tracking the runner-up -> compute margin/confidence/confidenceBand/tieBreak\n * from the RAW scores -> if `topScore <= 0`, replace `taskClass` (and record\n * `fallbackReason`) via the same zero-score cascade v1/v2/v3 use (fence/diff\n * -> code-gen; length < 40 -> trivial; length >= 2400 -> plan; lane fallback;\n * default code-gen) — the raw scores/margin/runnerUp/tieBreak fields still\n * reflect the pre-cascade computation, for audit purposes.\n *\n * Pure: same (input, weights) -> same ClassifierVerdictV4.\n */\nexport function classifyTaskV4(\n\tinput: TaskClassifierInputV4,\n\tweights: RouterWeightsV4 = DEFAULT_WEIGHTS_V4,\n): ClassifierVerdictV4 {\n\tconst prompt = input.prompt.trim();\n\tconst suppressed: string[] = [];\n\tconst features = extractFeaturesV4(prompt, weights, suppressed);\n\tconst scores = computeScoresV4(features, weights);\n\tapplyExtensionSignalsV4(scores, input, weights);\n\n\tlet top: TaskClassV4 = TASK_CLASSES_V4[0];\n\tfor (const c of TASK_CLASSES_V4) {\n\t\tif (scores[c] > scores[top]) top = c;\n\t}\n\tlet runnerUp: TaskClassV4 | null = null;\n\tfor (const c of TASK_CLASSES_V4) {\n\t\tif (c === top) continue;\n\t\tif (runnerUp === null || scores[c] > scores[runnerUp]) runnerUp = c;\n\t}\n\n\tconst topScore = scores[top];\n\tconst runnerUpScore = runnerUp !== null ? scores[runnerUp] : 0;\n\tconst margin = topScore - runnerUpScore;\n\tconst tieBreak = runnerUp !== null && topScore === runnerUpScore;\n\tconst confidence = topScore <= 0 ? 0 : Math.max(0, Math.min(1, margin / topScore));\n\tconst confidenceBand: ConfidenceBandV4 =\n\t\tconfidence <= weights.lowConfidenceThreshold\n\t\t\t? \"low\"\n\t\t\t: confidence >= weights.highConfidenceThreshold\n\t\t\t\t? \"high\"\n\t\t\t\t: \"medium\";\n\n\tlet taskClass: TaskClassV4 = top;\n\tlet fallbackReason: FallbackReasonV4 | null = null;\n\tif (topScore <= 0) {\n\t\tif (features.codeFence || features.diffHunk) {\n\t\t\ttaskClass = \"code-gen\";\n\t\t\tfallbackReason = \"code-fence-or-diff\";\n\t\t} else if (prompt.length < TRIVIAL_MAX_CHARS_V4) {\n\t\t\tconst shortKoreanTaskClass = classifyShortKoreanZeroScoreTaskV4(prompt, weights.negationWindowChars);\n\t\t\tif (shortKoreanTaskClass !== null) {\n\t\t\t\ttaskClass = shortKoreanTaskClass;\n\t\t\t\tfallbackReason = \"ko-short-task-signal\";\n\t\t\t} else {\n\t\t\t\ttaskClass = \"trivial\";\n\t\t\t\tfallbackReason = \"trivial-length\";\n\t\t\t}\n\t\t} else if (prompt.length >= COMPLEX_PROSE_MIN_CHARS_V4) {\n\t\t\ttaskClass = \"plan\";\n\t\t\tfallbackReason = \"long-prose\";\n\t\t} else if (input.laneType !== undefined) {\n\t\t\ttaskClass = LANE_FALLBACK_CLASS_V4[input.laneType];\n\t\t\tfallbackReason = \"lane-fallback\";\n\t\t} else {\n\t\t\ttaskClass = \"code-gen\";\n\t\t\tfallbackReason = \"default\";\n\t\t}\n\t}\n\n\treturn {\n\t\ttaskClass,\n\t\tscores,\n\t\trunnerUp,\n\t\tmargin,\n\t\tconfidence,\n\t\tconfidenceBand,\n\t\ttieBreak,\n\t\tfallbackReason,\n\t\tsuppressedFeatureIds: suppressed,\n\t\tcompoundIntent: features.compoundIntent,\n\t\tsecondClauseIntent: features.secondClauseIntent,\n\t};\n}\n\n// ============================================================================\n// Resolver\n// ============================================================================\n\n/** Reasoning ladder used for targets and clamping. Intentionally excludes \"off\". Identical to v1/v2/v3. */\nconst REASONING_LADDER_V4: readonly ThinkingLevel[] = [\"minimal\", \"low\", \"medium\", \"high\", \"xhigh\", \"max\"];\n\n/** Static rule table: task class -> recommended ThinkingLevel. Identical to v1/v2/v3. */\nexport const TASK_CLASS_THINKING_LEVELS_V4: Readonly<Record<TaskClassV4, ThinkingLevel>> = {\n\ttrivial: \"minimal\",\n\t\"simple-edit\": \"low\",\n\t\"code-gen\": \"medium\",\n\tdebug: \"high\",\n\trefactor: \"high\",\n\treview: \"high\",\n\tplan: \"xhigh\",\n};\n\n/** Lane ladder adjustment. Identical to v1/v2/v3: planner/security escalate one step, explorer de-escalates. */\nconst LANE_STEP_V4: Readonly<Partial<Record<ReasoningLaneTypeV4, 1 | -1>>> = {\n\tplanner: 1,\n\tsecurity: 1,\n\texplorer: -1,\n};\n\n/** Maximum magnitude of the learning/consult bias (ladder steps). Identical bound to v2/v3. */\nconst BIAS_MAX_V4 = 2;\n/** Confidence floor at which an override hint is fused into the target. Identical to v2/v3. */\nconst HINT_CONFIDENCE_THRESHOLD_V4 = 0.7;\n/**\n * Bounded, strictly non-negative escalation applied when the verdict's own\n * confidence is low or a fallback (not a real signal) decided the class. This\n * is the ONLY place confidence touches the target index, and it can only add:\n * confidence can never lower effort (spec 008 Req 2 acceptance criterion).\n */\nconst LOW_CONFIDENCE_ESCALATION_STEPS_V4 = 1;\n\nfunction clampToLadderIndexV4(index: number): number {\n\treturn Math.max(0, Math.min(index, REASONING_LADDER_V4.length - 1));\n}\n\nfunction clampToAvailableV4(targetIndex: number, availableLevels: readonly ThinkingLevel[]): ThinkingLevel {\n\tconst availableOnLadder = REASONING_LADDER_V4.filter((level) => availableLevels.includes(level));\n\tif (availableOnLadder.length === 0) return availableLevels[0] ?? \"off\";\n\tfor (let i = targetIndex; i >= 0; i--) {\n\t\tconst candidate = REASONING_LADDER_V4[i];\n\t\tif (availableOnLadder.includes(candidate)) return candidate;\n\t}\n\treturn availableOnLadder[0];\n}\n\n/**\n * Thin auto-mode wrapper with no uncertainty adjustment: base rule table ->\n * lane step -> clamp to availableLevels. Equivalent to\n * `resolveThinkingLevelV3ForAuto` / `resolveThinkingLevelV2ForAuto` for the\n * same (taskClass, availableLevels, laneType) — i.e. v4's \"confident\" path.\n */\nexport function resolveThinkingLevelV4ForAuto(\n\ttaskClass: TaskClassV4,\n\tavailableLevels: readonly ThinkingLevel[],\n\tlaneType: ReasoningLaneTypeV4 | undefined,\n): ThinkingLevel {\n\tconst baseIndex = REASONING_LADDER_V4.indexOf(TASK_CLASS_THINKING_LEVELS_V4[taskClass]);\n\tconst laneStep = laneType ? (LANE_STEP_V4[laneType] ?? 0) : 0;\n\tconst targetIndex = clampToLadderIndexV4(baseIndex + laneStep);\n\treturn clampToAvailableV4(targetIndex, availableLevels);\n}\n\n/**\n * Uncertainty-aware resolver (spec 008 Req 2 / plan.md step 5).\n *\n * Pipeline: base rule table for `verdict.taskClass` -> lane step -> bounded\n * bias [-2,+2] -> optional hint fusion (±2 bounded, same as v2/v3) -> bounded\n * non-negative confidence escalation (+1 ladder step exactly when\n * `verdict.confidenceBand === \"low\"` or `verdict.fallbackReason !== null`) ->\n * clamp to `availableLevels`.\n *\n * The confidence-escalation term is strictly `>= 0`: low confidence can only\n * hold the base+lane+bias+hint target or push it one step higher, never lower\n * it. This guarantees a low-confidence verdict never resolves BELOW what\n * `resolveThinkingLevelV4ForAuto` would give the same class (with bias=0,\n * hint=null) — text alone cannot talk the resolver down.\n */\nexport function resolveThinkingLevelV4WithUncertainty(\n\tverdict: ClassifierVerdictV4,\n\tavailableLevels: readonly ThinkingLevel[],\n\tlaneType: ReasoningLaneTypeV4 | undefined,\n\tbias = 0,\n\thint: { level: ThinkingLevel; confidence: number } | null = null,\n): ThinkingLevel {\n\tconst baseIndex = REASONING_LADDER_V4.indexOf(TASK_CLASS_THINKING_LEVELS_V4[verdict.taskClass]);\n\tconst laneStep = laneType ? (LANE_STEP_V4[laneType] ?? 0) : 0;\n\tconst biasClamped = Math.max(-BIAS_MAX_V4, Math.min(BIAS_MAX_V4, bias));\n\tlet targetIndex = clampToLadderIndexV4(baseIndex + laneStep + biasClamped);\n\n\tif (hint !== null && hint.confidence >= HINT_CONFIDENCE_THRESHOLD_V4) {\n\t\tconst hintIndex = REASONING_LADDER_V4.indexOf(hint.level);\n\t\tif (hintIndex >= 0) {\n\t\t\tconst delta = hintIndex - targetIndex;\n\t\t\tconst step = Math.max(-BIAS_MAX_V4, Math.min(BIAS_MAX_V4, delta));\n\t\t\ttargetIndex = clampToLadderIndexV4(targetIndex + step);\n\t\t}\n\t}\n\n\tif (verdict.confidenceBand === \"low\" || verdict.fallbackReason !== null) {\n\t\ttargetIndex = clampToLadderIndexV4(targetIndex + LOW_CONFIDENCE_ESCALATION_STEPS_V4);\n\t}\n\n\treturn clampToAvailableV4(targetIndex, availableLevels);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"reasoning-router-v4.d.ts","sourceRoot":"","sources":["../../src/core/reasoning-router-v4.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,EAEN,KAAK,mBAAmB,EACxB,KAAK,eAAe,EAEpB,KAAK,WAAW,EAChB,MAAM,kCAAkC,CAAC;AAE1C,YAAY,EAAE,mBAAmB,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AAC1G,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,kCAAkC,CAAC;AA2gBvF;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,mBAAmB,CAAC;IAC/B,OAAO,CAAC,EAAE,SAAS,WAAW,EAAE,CAAC;IACjC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;CAC/B;AAED,gIAAgI;AAChI,MAAM,MAAM,gBAAgB,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;AAEzD,wHAAwH;AACxH,MAAM,MAAM,gBAAgB,GACzB,oBAAoB,GACpB,gBAAgB,GAChB,sBAAsB,GACtB,YAAY,GACZ,eAAe,GACf,SAAS,CAAC;AAEb;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IACnC,qEAAqE;IACrE,QAAQ,CAAC,SAAS,EAAE,WAAW,CAAC;IAChC,mFAAmF;IACnF,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC;IACvD,mGAAmG;IACnG,QAAQ,CAAC,QAAQ,EAAE,WAAW,GAAG,IAAI,CAAC;IACtC,oIAAoI;IACpI,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,gHAAgH;IAChH,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,2FAA2F;IAC3F,QAAQ,CAAC,cAAc,EAAE,gBAAgB,CAAC;IAC1C,qHAAqH;IACrH,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,qGAAqG;IACrG,QAAQ,CAAC,cAAc,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACjD,oHAAoH;IACpH,QAAQ,CAAC,oBAAoB,EAAE,SAAS,MAAM,EAAE,CAAC;IACjD,0GAA0G;IAC1G,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC,oHAAoH;IACpH,QAAQ,CAAC,kBAAkB,EAAE,WAAW,GAAG,IAAI,CAAC;CAChD;AAMD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAC7B,KAAK,EAAE,qBAAqB,EAC5B,OAAO,GAAE,eAAoC,GAC3C,mBAAmB,CAqErB;AAMD;;;;GAIG;AACH,eAAO,MAAM,6BAA6B,8FAA6B,CAAC;AAUxE;;;;GAIG;AACH,wBAAgB,6BAA6B,CAC5C,SAAS,EAAE,WAAW,EACtB,eAAe,EAAE,SAAS,aAAa,EAAE,EACzC,QAAQ,EAAE,mBAAmB,GAAG,SAAS,GACvC,aAAa,CAEf;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,qCAAqC,CACpD,OAAO,EAAE,mBAAmB,EAC5B,eAAe,EAAE,SAAS,aAAa,EAAE,EACzC,QAAQ,EAAE,mBAAmB,GAAG,SAAS,EACzC,IAAI,SAAI,EACR,IAAI,GAAE;IAAE,KAAK,EAAE,aAAa,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAAG,IAAW,GAC9D,aAAa,CAIf","sourcesContent":["/**\n * Reasoning-router v4 — the sole `/think auto` classifier/resolver.\n *\n * Deterministic by construction: same (input, weights) -> same\n * ClassifierVerdictV4; same (verdict, availableLevels, laneType, bias, hint) ->\n * same ThinkingLevel. No clock, randomness, I/O, model calls, network access,\n * or state mutation anywhere in this file (ThinkingLevel is a type-only import\n * and is erased).\n *\n * `classifyTaskV4` returns a confidence-bearing verdict: task class, per-class\n * scores, runner-up, margin, confidence band, tie-break flag, fallback reason,\n * bounded-negation audit ids, and compound-intent metadata. None of this carries\n * prompt text — every field is a bounded enum, number, boolean, or a closed set\n * of short diagnostic-id strings.\n *\n * `resolveThinkingLevelV4WithUncertainty` starts from the canonical task-class\n * rule table, applies lane/bias/hint adjustments, and only adds non-negative\n * confidence escalation. A prompt cannot lower its own effort by asking the\n * router to \"think less\"; low confidence or fallback routing can only hold or\n * raise the resolved level.\n */\n\nimport type { ThinkingLevel } from \"omk-agent-core\";\nimport {\n\tDEFAULT_WEIGHTS_V4,\n\ttype ReasoningLaneTypeV4,\n\ttype RouterWeightsV4,\n\tTASK_CLASSES_V4,\n\ttype TaskClassV4,\n} from \"./reasoning-router-v4-weights.ts\";\n\nexport type { ReasoningLaneTypeV4, RouterWeightsV4, TaskClassV4 } from \"./reasoning-router-v4-weights.ts\";\nexport { DEFAULT_WEIGHTS_V4, TASK_CLASSES_V4 } from \"./reasoning-router-v4-weights.ts\";\n\nimport { resolveThinkingLevelCore, TASK_CLASS_THINKING_LEVELS } from \"./reasoning-router-resolver.ts\";\n\n/** Prompts shorter than this (trimmed) fall back to trivial when no real signal scores > 0. */\nconst TRIVIAL_MAX_CHARS_V4 = 40;\n/** Prompts at/above this length with no real signal fall back to plan (long prose brief). */\nconst COMPLEX_PROSE_MIN_CHARS_V4 = 2400;\n/** Plan-brief prompts at/above this length count as a \"long brief\" even without PLAN_BRIEF_PATTERN. */\nconst LONG_BRIEF_MIN_CHARS_V4 = 512;\n/** Compound-clause split only applies within this many leading characters (short direct commands, not long prose). */\nconst COMPOUND_SPLIT_MAX_INDEX_V4 = 300;\n/** Minimum trimmed length of a candidate second clause to be worth checking for a leading intent. */\nconst COMPOUND_SECOND_CLAUSE_MIN_CHARS_V4 = 3;\n/** Bounded look-ahead cap when checking a second clause's leading intent (defense in depth; independent of regex cost). */\nconst COMPOUND_SECOND_CLAUSE_SCAN_CHARS_V4 = 200;\n\n/** Lane fallback class, used only when every scored class is <= 0 (zero-score cascade). */\nconst LANE_FALLBACK_CLASS_V4: Readonly<Record<ReasoningLaneTypeV4, TaskClassV4>> = {\n\tplanner: \"plan\",\n\tsecurity: \"review\",\n\texplorer: \"review\",\n\tcoder: \"code-gen\",\n\treviewer: \"review\",\n\ttester: \"code-gen\",\n};\n\n// ============================================================================\n// Whole-prompt / leading-clause patterns used by the v4 scorer.\n// ============================================================================\n\nconst LOCAL_EDIT_OBJECT_PATTERN =\n\t/\\b(spelling|grammar|capitalization|date\\s+format|author\\s+e-?mail|copyright\\s+year|headline|title|tooltip|placeholder|punctuation|comma|period|semicolon|closing\\s+html\\s+tag|closing\\s+tag|double\\s+space|whitespace|indentation|table\\s+alignment|typos?|one-?liner?|single\\s+line|sentence|stray|trailing)\\b|오타|맞춤법|띄어쓰기|문구|제목/i;\nconst LOCAL_EDIT_ACTION_OBJECT_PATTERN =\n\t/\\b(update|change|swap|remove|correct|fix|add|adjust|trim)\\s+(?:the\\s+|a\\s+|an\\s+)?(?:missing\\s+|stray\\s+|author\\s+|two\\s+)?(?:e-?mail|comma|period|semicolon|tag|word|words|headline|title|copyright|spelling|grammar)\\b/i;\nconst IMPLEMENTATION_OBJECT_PATTERN =\n\t/\\b(error\\s+handling|input\\s+validation|validation|auth(?:entication)?|retry|endpoint|migration|component|function|utility|helper|module|service|database|schema|column|middleware|rate\\s+limiter|cache|caching|oauth|jwt|webhook)\\b|에러\\s*처리|오류\\s*처리|입력\\s*검증|인증|재시도|엔드포인트|마이그레이션|컴포넌트|함수|유틸|헬퍼|모듈|서비스|데이터베이스|스키마|미들웨어|레이트\\s*리미터|캐시|웹훅|기능/i;\nconst HARD_DIAGNOSTIC_PATTERN =\n\t/\\b(stack\\s*trace|traceback|panic|segfault|crash(?:es|ing|ed)?|throws?|fails?|failing|flaky|hangs?|timeout|500|EADDRINUSE|silently\\s+fails?)\\b|\\b(?:TypeError|ReferenceError|RangeError|Error):|크래시|스택\\s*트레이스|세그폴트|먹통|멈춰|타임아웃/i;\nconst BUG_OBJECT_PATTERN =\n\t/\\b(null\\s+pointer|null\\s+deref|race(?:\\s+condition)?|heap(?:\\s+overflow)?|use-after-free|memory\\s+leak|leaks?|deadlock|data\\s+corruption|stale\\s+data|off-by-one|encoding\\s+bug|regression\\s+(?:was\\s+)?introduced|exceptions?|assertion\\s+error|bugs?)\\b|버그|예외|메모리\\s*누수|데드락|무한\\s*루프/i;\nconst NON_DIAGNOSTIC_DEBUG_CONTEXT_PATTERN = /\\berror\\s+(?:handling|messages?|budgets?)\\b|에러\\s*처리|오류\\s*처리/i;\nconst GENERIC_DIAGNOSTIC_PATTERN = /\\b(errors?|broken|wrong\\s+results|rolls?\\s+back|rollback)\\b/i;\nconst PLAN_BRIEF_PATTERN =\n\t/\\b(context\\s+and\\s+constraints|starting\\s+state|target\\s+state|cross-cutting|deliver:|deliverables?|roadmap|migration\\s+wave|top\\s+(?:ten\\s+)?risks|component\\s+diagram|data\\s+model|architecture|go\\/no-go|phased\\s+(?:delivery|rollout)|bounded\\s+contexts?|strangler\\s+fig|event-driven|quarter-by-quarter|milestone)\\b/i;\nconst OPERATIONAL_RUNBOOK_SIGNAL_PATTERNS: readonly { pattern: RegExp; critical: boolean }[] = [\n\t{ pattern: /\\b(?:commit|commits|committed|committing)\\b|커밋/i, critical: false },\n\t{ pattern: /\\b(?:push|pushed|pushing)\\b|푸시|푸쉬/i, critical: true },\n\t{\n\t\tpattern: /\\b(?:release|releases|tag|version\\s+bump|bump\\s+(?:the\\s+)?version)\\b|릴리즈|태그|버전/i,\n\t\tcritical: true,\n\t},\n\t{ pattern: /\\bnpm\\s+publish\\b|\\bpublish(?:ing|ed)?\\b|퍼블리시|배포/i, critical: true },\n\t{ pattern: /\\b(?:CHANGELOG\\.md|changelog|release\\s+notes?)\\b|채널로그|체인지로그/i, critical: false },\n\t{ pattern: /\\bREADME\\.md\\b|\\breadme\\b/i, critical: false },\n\t{ pattern: /\\bci\\s*\\/\\s*cd\\b|\\bgithub\\s+actions?\\b|\\bworkflow\\b|깃허브\\s*액션|깃헙\\s*액션/i, critical: false },\n];\nconst EXPLICIT_RELEASE_RUNBOOK_PATTERN =\n\t/\\bnpm\\s+publish\\b|\\brelease\\b[^\\n]{0,80}\\bv?\\d+\\.\\d+\\.\\d+\\b|릴리즈[^\\n]{0,80}\\d+\\.\\d+\\.\\d+/i;\nconst REVIEW_SCOPE_PATTERN =\n\t/\\b(pr|pull\\s+request|diff|codebase|strategy|plan|spec|schema|design|api|coverage|security\\s+posture|licensing|risks?|edge\\s+cases|clarity|consistency|dependencies|third-party|ci\\s+pipeline|threat\\s+model|retry\\s+logic|error\\s+handling\\s+strategy|error\\s+messages?)\\b/i;\nconst REFACTOR_CUE_PATTERN =\n\t/\\b(refactor(?:ing|ed)?|extract|rename|deduplicate|consolidate|modularize|reorganize|simplify|clean\\s*up|tidy\\s+up|restructure|split\\s+module|move\\s+logic|untangle|merge\\s+duplicate)\\b|리팩토링(?!하지\\s*마|하지\\s*말)|리팩터링(?!하지\\s*마|하지\\s*말)|구조\\s*개선/i;\nconst ADD_KEYWORD_PATTERN = /\\badd\\b/i;\nconst LOW_RISK_EDIT_ACTION_PATTERN = /^(?:correct|update|swap|remove|reword|tweak|bump|trim)\\b/i;\n\n/** Whole-prompt keyword-family patterns; refactor/simple-edit reuse the patterns above. */\nconst DEBUG_KEYWORD_FAMILY_PATTERN =\n\t/^(?:debug|investigate\\s+why|reproduce|trace\\b)|\\bfix\\s+this\\s+(?:traceback|panic|error)\\b|\\btrack\\s+down\\b|디버깅|디버그|재현|원인\\s*분석/i;\nconst REVIEW_KEYWORD_FAMILY_PATTERN =\n\t/\\b(review|critique|assess|inspect|approve|lgtm|double-?check|sanity[-\\s]?check|once-?over|eyeball|tell\\s+me\\s+(?:if|whether)[^.!?;\\n]{0,80}\\bcorrect|audit(?!\\s+log\\b))\\b|리뷰|검토|점검/i;\nconst PLAN_KEYWORD_FAMILY_PATTERN =\n\t/\\b(plan|design|architect|architecture|roadmap|spec(?:ification)?|strategy|decompose|milestones?|map\\s+out|phases|write\\s+a\\s+(?:technical\\s+)?spec|create\\s+a\\s+(?:roadmap|strategy|plan))\\b|설계|로드맵|아키텍처|계획.{0,6}세워|기획/i;\nconst CODE_GEN_KEYWORD_FAMILY_PATTERN =\n\t/\\b(implement|write|create|build|generate|scaffold|prototype)\\b|구현|만들어\\s*줘|생성|작성|추가해/i;\nconst KO_SHORT_DEBUG_SIGNAL_PATTERN_V4 =\n\t/오류|에러|실패|안\\s*돼|안\\s*됨|깨졌|깨짐|고장|디버깅|디버그|재현|원인\\s*(?:분석|파악)?/i;\nconst KO_SHORT_SIMPLE_EDIT_SIGNAL_PATTERN_V4 = /오타|맞춤법|띄어쓰기|문구|제목/i;\nconst KO_SHORT_REVIEW_SIGNAL_PATTERN_V4 = /리뷰|검토|점검/i;\nconst KO_SHORT_PLAN_SIGNAL_PATTERN_V4 = /설계|로드맵|아키텍처|계획.{0,6}세워|기획/i;\nconst KO_SHORT_REFACTOR_SIGNAL_PATTERN_V4 = /리팩토링|리팩터링|구조\\s*개선/i;\nconst KO_SHORT_CODE_GEN_SIGNAL_PATTERN_V4 =\n\t/구현|만들|생성|작성|추가|테스트|삭제|지워|지우|제거|고쳐|고치|수정|변경|바꿔|바꾸|옮겨|옮기|넣어/i;\n\n/** Short-range compound-clause conjunction boundary. Only bare/compound \"then\"/\"and also\"/\";\" — never bare \"and\". */\nconst COMPOUND_SPLIT_PATTERN = /\\band\\s+then\\b|\\bthen\\b|\\band\\s+also\\b|;/i;\n\n/**\n * Bounded negation-cue vocabulary (spec 008 plan.md \"bounded negation window\").\n * Deliberately excludes bare \"not\" (too broad; verified against the current\n * GOLD_SET that every listed cue here is either absent or occurs AFTER, not\n * before, a tracked keyword — see this lane's evidence file for the audit).\n */\nconst NEGATION_CUE_PATTERN =\n\t/\\b(?:don't|do\\s+not|doesn't|does\\s+not|didn't|did\\s+not|won't|will\\s+not|shouldn't|should\\s+not|wouldn't|would\\s+not|can't|cannot|isn't|is\\s+not|aren't|are\\s+not|never|avoid|skip|without|instead\\s+of|rather\\s+than|no\\s+need\\s+(?:to|for)|no\\s+longer\\s+need(?:\\s+(?:to|for))?)\\b/i;\nconst DIRECT_NOT_OBJECT_CUE_PATTERN_V4 = /\\b(?:not|no)\\s+(?:a\\s+|an\\s+|the\\s+)?$/i;\nconst DOUBLE_NEGATION_RESCUE_PATTERN_V4 =\n\t/\\b(?:don't|do\\s+not|doesn't|does\\s+not|didn't|did\\s+not|won't|will\\s+not|shouldn't|should\\s+not|wouldn't|would\\s+not|can't|cannot)\\s+(?:skip|avoid)\\s+(?:the\\s+|a\\s+|an\\s+)?$/i;\nconst DELIBERATIVE_QUESTION_RESCUE_PATTERN_V4 =\n\t/\\b(?:shouldn't|should\\s+not|wouldn't|would\\s+not|can't|cannot)\\s+(?:we|i|you)\\s*$/i;\nconst POSTPOSITIONED_NEGATION_CUE_PATTERN_V4 =\n\t/^\\s*(?:-\\s*free\\b|(?:is|are|was|were)?\\s*(?:not\\s+(?:needed|required|desired)|unnecessary|not\\s+necessary)|(?:[은는이가을를도만]\\s*)?(?:하지\\s*(?:마(?:라|세요|십시오)?|말(?:고|아|라)?|않(?:아|는|고|을|게)?)|말고|금지))/i;\n\n/** A closed set of clause-boundary characters; a negation cue never reaches across one of these into a prior clause. */\nconst CLAUSE_BOUNDARY_CHARS = [\".\", \"!\", \"?\", \";\", \",\"] as const;\n\nfunction hasCodeFence(text: string): boolean {\n\treturn text.includes(\"```\");\n}\n\n/** Explicit hunk headers or `diff --git` count alone; bare +/- only count together. */\nfunction hasDiffMarkers(text: string): boolean {\n\tif (/^@@[^\\n]*@@/m.test(text) || /^diff --git /m.test(text)) return true;\n\treturn /^\\+(?!\\+)/m.test(text) && /^-(?!-)/m.test(text);\n}\n\n/** First line (after stripping one polite prefix), capped at 180 chars. */\nfunction firstClause(prompt: string): string {\n\tconst firstLine =\n\t\tprompt.replace(/^(?:please|pls|can you|could you|would you|help me|i need you to)\\s+/i, \"\").split(\"\\n\")[0] ?? \"\";\n\treturn firstLine.slice(0, 180);\n}\n\n// --- Leading-clause intent tests. All are `^`-anchored against the LEADING\n// clause text only, so a negation cue at the start (\"don't refactor...\") can\n// never match one of these: none of the alternations include \"don't\"/\"never\"/\n// etc, so they are negation-immune by construction.\n\nfunction hasLeadingReviewIntent(text: string): boolean {\n\treturn /^(?:review|audit(?!\\s+log\\b)|critique|inspect|assess|approve|double-?check|lgtm)\\b/i.test(text);\n}\n\nfunction hasLeadingPlanIntent(text: string): boolean {\n\treturn /^(?:plan|design|architect|decompose)\\b|^(?:write|create)\\s+(?:a\\s+)?(?:technical\\s+)?(?:spec|roadmap|strategy|plan)\\b/i.test(\n\t\ttext,\n\t);\n}\n\nfunction hasLeadingRefactorIntent(text: string): boolean {\n\treturn /^(?:refactor(?:ing|ed)?|extract|rename|deduplicate|consolidate|modularize|reorganize|simplify|restructure|untangle)\\b|^clean\\s+up\\b|^split\\s+(?:the\\s+)?module\\b|^move\\s+logic\\b/i.test(\n\t\ttext,\n\t);\n}\n\nfunction hasLeadingDebugAction(text: string): boolean {\n\treturn /^(?:debug|investigate\\s+why|reproduce|trace\\b)|^fix\\s+this\\s+(?:traceback|panic|error)\\b/i.test(text);\n}\n\nfunction hasLeadingCodeGenIntent(text: string): boolean {\n\treturn /^(?:implement|write|create|build|generate|scaffold|prototype|add)\\b/i.test(text);\n}\n\nfunction hasLeadingSimpleEditIntent(text: string): boolean {\n\treturn /^(?:correct|update|swap|remove|adjust|fix|add|change|trim|reword|tweak|bump)\\b/i.test(text);\n}\n\n// ============================================================================\n// Bounded negation gating\n// ============================================================================\n\ninterface UnnegatedMatchResultV4 {\n\t/** True when the pattern matched at least once and at least one occurrence was NOT preceded by a negation cue. */\n\treadonly matched: boolean;\n\t/** True when the pattern matched at least once but EVERY occurrence was negated (useful for suppressedFeatureIds). */\n\treadonly suppressed: boolean;\n}\n\nconst NO_MATCH_RESULT_V4: UnnegatedMatchResultV4 = { matched: false, suppressed: false };\n\nfunction matchOperationalRunbookV4(prompt: string, windowChars: number): UnnegatedMatchResultV4 {\n\tlet distinctSignals = 0;\n\tlet criticalSignals = 0;\n\tlet sawSuppressed = false;\n\tfor (const { pattern, critical } of OPERATIONAL_RUNBOOK_SIGNAL_PATTERNS) {\n\t\tconst result = matchUnnegated(prompt, pattern, windowChars);\n\t\tif (result.matched) {\n\t\t\tdistinctSignals += 1;\n\t\t\tif (critical) criticalSignals += 1;\n\t\t}\n\t\tif (result.suppressed) sawSuppressed = true;\n\t}\n\tconst explicitRelease = matchUnnegated(prompt, EXPLICIT_RELEASE_RUNBOOK_PATTERN, windowChars).matched;\n\tconst matched = explicitRelease || distinctSignals >= 3 || (criticalSignals >= 2 && distinctSignals >= 2);\n\treturn { matched, suppressed: !matched && sawSuppressed };\n}\n\n/**\n * Negation-aware whole-prompt pattern scan. Finds every occurrence of `pattern`\n * in `prompt`; for each, looks back up to\n * `windowChars` characters (never crossing a `.,;!?` boundary into a prior\n * clause) for a `NEGATION_CUE_PATTERN` hit, and looks forward inside the same\n * bounded window for Hangul-only post-positioned negation cues such as\n * \"하지 말고\". `matched` is true iff at least one occurrence survives\n * un-negated. Deterministic, single pass per pattern, no shared regex state (a\n * fresh global-flag RegExp is constructed per call).\n */\nfunction hasPrePositionedNegationCueV4(scoped: string): boolean {\n\tif (!NEGATION_CUE_PATTERN.test(scoped) && !DIRECT_NOT_OBJECT_CUE_PATTERN_V4.test(scoped)) return false;\n\treturn !DOUBLE_NEGATION_RESCUE_PATTERN_V4.test(scoped) && !DELIBERATIVE_QUESTION_RESCUE_PATTERN_V4.test(scoped);\n}\n\nfunction hasPostPositionedNegationCueV4(prompt: string, matchEnd: number, windowChars: number): boolean {\n\tconst scoped = prompt.slice(matchEnd, Math.min(prompt.length, matchEnd + windowChars));\n\treturn POSTPOSITIONED_NEGATION_CUE_PATTERN_V4.test(scoped);\n}\n\nfunction matchUnnegated(prompt: string, pattern: RegExp, windowChars: number): UnnegatedMatchResultV4 {\n\tconst flags = pattern.flags.includes(\"g\") ? pattern.flags : `${pattern.flags}g`;\n\tconst globalPattern = new RegExp(pattern.source, flags);\n\tlet sawMatch = false;\n\tlet sawUnnegated = false;\n\tlet match = globalPattern.exec(prompt);\n\twhile (match !== null) {\n\t\tsawMatch = true;\n\t\tconst matchStart = match.index;\n\t\tconst windowStart = Math.max(0, matchStart - windowChars);\n\t\tconst windowText = prompt.slice(windowStart, matchStart);\n\t\tlet boundary = -1;\n\t\tfor (const boundaryChar of CLAUSE_BOUNDARY_CHARS) {\n\t\t\tconst idx = windowText.lastIndexOf(boundaryChar);\n\t\t\tif (idx > boundary) boundary = idx;\n\t\t}\n\t\tconst scoped = boundary >= 0 ? windowText.slice(boundary + 1) : windowText;\n\t\tconst preNegated = hasPrePositionedNegationCueV4(scoped);\n\t\tconst postNegated =\n\t\t\t!preNegated && hasPostPositionedNegationCueV4(prompt, matchStart + match[0].length, windowChars);\n\t\tif (!preNegated && !postNegated) sawUnnegated = true;\n\t\tif (match[0].length === 0) globalPattern.lastIndex += 1;\n\t\tmatch = globalPattern.exec(prompt);\n\t}\n\treturn { matched: sawUnnegated, suppressed: sawMatch && !sawUnnegated };\n}\n\n// ============================================================================\n// Bounded compound-intent detection\n// ============================================================================\n\n/**\n * Splits off a bounded second clause after a short-range conjunction\n * (\"then\"/\"and then\"/\"and also\"/\";\"), only when the split point is within the\n * first `COMPOUND_SPLIT_MAX_INDEX_V4` characters (short direct commands, not\n * long prose briefs — verified this never fires inside the GOLD_SET's\n * >=2400-char plan entries). Returns null when no qualifying split exists.\n */\nfunction splitCompoundClauseV4(prompt: string): string | null {\n\tconst match = COMPOUND_SPLIT_PATTERN.exec(prompt);\n\tif (match === null || match.index > COMPOUND_SPLIT_MAX_INDEX_V4) return null;\n\tconst second = prompt.slice(match.index + match[0].length).trim();\n\tif (second.length < COMPOUND_SECOND_CLAUSE_MIN_CHARS_V4) return null;\n\treturn second;\n}\n\n/**\n * Leading-intent test for a compound prompt's SECOND clause. Reuses the same\n * `^`-anchored leading-intent tests as the primary clause (so it is equally\n * negation-immune), without the primary clause's diagnosticEvidence/localEdit\n * gating (a short second-clause fragment carries no such context of its own).\n */\nfunction secondClauseLeadingIntentV4(clause: string): TaskClassV4 | null {\n\tconst bounded = clause.slice(0, COMPOUND_SECOND_CLAUSE_SCAN_CHARS_V4);\n\treturn leadingIntentFromLeadingClauseV4(bounded);\n}\n\nfunction leadingIntentFromLeadingClauseV4(leading: string): TaskClassV4 | null {\n\tif (hasLeadingDebugAction(leading)) return \"debug\";\n\tif (hasLeadingReviewIntent(leading)) return \"review\";\n\tif (hasLeadingPlanIntent(leading)) return \"plan\";\n\tif (hasLeadingRefactorIntent(leading)) return \"refactor\";\n\tif (hasLeadingSimpleEditIntent(leading)) return \"simple-edit\";\n\tif (hasLeadingCodeGenIntent(leading)) return \"code-gen\";\n\treturn null;\n}\n\nfunction leadingIntentIsPostNegatedV4(leading: string, windowChars: number): boolean {\n\tconst leadingIntentMatch =\n\t\t/^(?:debug|investigate\\s+why|reproduce|trace\\b|fix\\s+this\\s+(?:traceback|panic|error)|review|audit(?!\\s+log\\b)|critique|inspect|assess|approve|double-?check|lgtm|plan|design|architect|decompose|write\\s+(?:a\\s+)?(?:technical\\s+)?(?:spec|roadmap|strategy|plan)|create\\s+(?:a\\s+)?(?:technical\\s+)?(?:spec|roadmap|strategy|plan)|refactor(?:ing|ed)?|extract|rename|deduplicate|consolidate|modularize|reorganize|simplify|restructure|untangle|clean\\s+up|split\\s+(?:the\\s+)?module|move\\s+logic|correct|update|swap|remove|adjust|fix|add|change|trim|reword|tweak|bump|implement|build|generate|scaffold|prototype)\\b/i.exec(\n\t\t\tleading,\n\t\t);\n\treturn (\n\t\tleadingIntentMatch !== null && hasPostPositionedNegationCueV4(leading, leadingIntentMatch[0].length, windowChars)\n\t);\n}\n\nfunction classifyShortKoreanZeroScoreTaskV4(prompt: string, windowChars: number): TaskClassV4 | null {\n\tif (matchUnnegated(prompt, KO_SHORT_DEBUG_SIGNAL_PATTERN_V4, windowChars).matched) return \"debug\";\n\tif (matchUnnegated(prompt, KO_SHORT_SIMPLE_EDIT_SIGNAL_PATTERN_V4, windowChars).matched) return \"simple-edit\";\n\tif (matchUnnegated(prompt, KO_SHORT_REVIEW_SIGNAL_PATTERN_V4, windowChars).matched) return \"review\";\n\tif (matchUnnegated(prompt, KO_SHORT_PLAN_SIGNAL_PATTERN_V4, windowChars).matched) return \"plan\";\n\tif (matchUnnegated(prompt, KO_SHORT_REFACTOR_SIGNAL_PATTERN_V4, windowChars).matched) return \"refactor\";\n\tif (matchUnnegated(prompt, KO_SHORT_CODE_GEN_SIGNAL_PATTERN_V4, windowChars).matched) return \"code-gen\";\n\treturn null;\n}\n\n// ============================================================================\n// Feature extraction\n// ============================================================================\n\ninterface ContextualFeaturesV4 {\n\treadonly firstClause: string;\n\treadonly codeFence: boolean;\n\treadonly diffHunk: boolean;\n\treadonly localEdit: boolean;\n\treadonly diagnosticEvidence: boolean;\n\treadonly reviewScope: boolean;\n\treadonly planBrief: boolean;\n\treadonly operationalRunbook: boolean;\n\treadonly refactorCue: boolean;\n\treadonly implementationObject: boolean;\n\treadonly leadingIntent: TaskClassV4 | null;\n\treadonly secondClauseIntent: TaskClassV4 | null;\n\treadonly compoundIntent: boolean;\n\treadonly keywordFamilyMatch: Readonly<Record<TaskClassV4, boolean>>;\n\treadonly addKeywordMatch: boolean;\n}\n\ninterface LeadingIntentInputV4 {\n\treadonly firstClause: string;\n\treadonly localEdit: boolean;\n\treadonly diagnosticEvidence: boolean;\n}\n\n/** Decision order for the leading intent classifier. */\nfunction leadingIntentV4(input: LeadingIntentInputV4): TaskClassV4 | null {\n\tif (\n\t\tinput.diagnosticEvidence &&\n\t\t(hasLeadingDebugAction(input.firstClause) || hasLeadingSimpleEditIntent(input.firstClause))\n\t)\n\t\treturn \"debug\";\n\tif (hasLeadingReviewIntent(input.firstClause)) return \"review\";\n\tif (hasLeadingPlanIntent(input.firstClause)) return \"plan\";\n\tif (hasLeadingRefactorIntent(input.firstClause)) return \"refactor\";\n\tif (input.localEdit && hasLeadingSimpleEditIntent(input.firstClause)) return \"simple-edit\";\n\tif (!input.localEdit && hasLeadingCodeGenIntent(input.firstClause)) return \"code-gen\";\n\treturn null;\n}\n\n/** Diagnostic-evidence decision structure, parameterized over precomputed negation-aware matches. */\nfunction hasDiagnosticEvidenceV4(\n\tleading: string,\n\thardDiagnosticMatched: boolean,\n\tbugObjectMatched: boolean,\n\tgenericDiagnosticMatched: boolean,\n\tnonDiagnosticContext: boolean,\n): boolean {\n\tif (hasLeadingDebugAction(leading) || hardDiagnosticMatched) return true;\n\tconst hasContextualDiagnostic = bugObjectMatched || (genericDiagnosticMatched && !nonDiagnosticContext);\n\tif (!hasContextualDiagnostic) return false;\n\treturn !hasLeadingReviewIntent(leading) && !hasLeadingPlanIntent(leading) && !hasLeadingRefactorIntent(leading);\n}\n\n/**\n * Extracts every v4 feature from one prompt, negation-gating every\n * whole-prompt scan exactly once and recording a `negation:<channel>` id in\n * `suppressed` whenever a match existed but every occurrence was negated.\n */\nfunction extractFeaturesV4(prompt: string, weights: RouterWeightsV4, suppressed: string[]): ContextualFeaturesV4 {\n\tconst leading = firstClause(prompt);\n\tconst window = weights.negationWindowChars;\n\n\tconst implementationObjectResult = matchUnnegated(prompt, IMPLEMENTATION_OBJECT_PATTERN, window);\n\tif (implementationObjectResult.suppressed) suppressed.push(\"negation:implementation-object\");\n\tconst implementationObject = implementationObjectResult.matched;\n\n\tconst lowRiskEditAction = LOW_RISK_EDIT_ACTION_PATTERN.test(leading);\n\tconst localEditObjectResult = matchUnnegated(prompt, LOCAL_EDIT_OBJECT_PATTERN, window);\n\tif (localEditObjectResult.suppressed) suppressed.push(\"negation:local-edit-object\");\n\tconst localEditActionResult = matchUnnegated(prompt, LOCAL_EDIT_ACTION_OBJECT_PATTERN, window);\n\tif (localEditActionResult.suppressed) suppressed.push(\"negation:local-edit-action-object\");\n\tconst localEdit =\n\t\t!implementationObject && (lowRiskEditAction || localEditObjectResult.matched || localEditActionResult.matched);\n\n\tconst hardDiagnosticResult = matchUnnegated(prompt, HARD_DIAGNOSTIC_PATTERN, window);\n\tif (hardDiagnosticResult.suppressed) suppressed.push(\"negation:hard-diagnostic\");\n\tconst bugObjectResult = matchUnnegated(prompt, BUG_OBJECT_PATTERN, window);\n\tif (bugObjectResult.suppressed) suppressed.push(\"negation:bug-object\");\n\tconst genericDiagnosticResult = matchUnnegated(prompt, GENERIC_DIAGNOSTIC_PATTERN, window);\n\tconst nonDiagnosticContext = NON_DIAGNOSTIC_DEBUG_CONTEXT_PATTERN.test(prompt);\n\tif (genericDiagnosticResult.suppressed && !nonDiagnosticContext) suppressed.push(\"negation:generic-diagnostic\");\n\tconst diagnosticEvidence = hasDiagnosticEvidenceV4(\n\t\tleading,\n\t\thardDiagnosticResult.matched,\n\t\tbugObjectResult.matched,\n\t\tgenericDiagnosticResult.matched,\n\t\tnonDiagnosticContext,\n\t);\n\n\tconst reviewScopeResult = matchUnnegated(prompt, REVIEW_SCOPE_PATTERN, window);\n\tif (reviewScopeResult.suppressed) suppressed.push(\"negation:review-scope\");\n\tconst reviewScope = hasLeadingReviewIntent(leading) && reviewScopeResult.matched;\n\n\tconst planBriefResult = matchUnnegated(prompt, PLAN_BRIEF_PATTERN, window);\n\tif (planBriefResult.suppressed) suppressed.push(\"negation:plan-brief\");\n\tconst planBrief =\n\t\thasLeadingPlanIntent(leading) && (planBriefResult.matched || prompt.length >= LONG_BRIEF_MIN_CHARS_V4);\n\n\tconst operationalRunbookResult = matchOperationalRunbookV4(prompt, window);\n\tif (operationalRunbookResult.suppressed) suppressed.push(\"negation:operational-runbook\");\n\tconst operationalRunbook = operationalRunbookResult.matched;\n\n\tconst refactorCueResult = matchUnnegated(prompt, REFACTOR_CUE_PATTERN, window);\n\tif (refactorCueResult.suppressed) suppressed.push(\"negation:refactor-cue\");\n\tconst refactorCue = refactorCueResult.matched;\n\n\tconst rawPrimaryIntent = leadingIntentV4({ firstClause: leading, localEdit, diagnosticEvidence });\n\tconst primaryIntent =\n\t\trawPrimaryIntent !== null && leadingIntentIsPostNegatedV4(leading, window) ? null : rawPrimaryIntent;\n\n\tconst secondClauseText = splitCompoundClauseV4(prompt);\n\tconst secondClauseIntent = secondClauseText === null ? null : secondClauseLeadingIntentV4(secondClauseText);\n\tconst compoundIntent = secondClauseIntent !== null && secondClauseIntent !== primaryIntent;\n\n\tconst debugKeywordResult = diagnosticEvidence\n\t\t? matchUnnegated(prompt, DEBUG_KEYWORD_FAMILY_PATTERN, window)\n\t\t: NO_MATCH_RESULT_V4;\n\tif (debugKeywordResult.suppressed) suppressed.push(\"negation:keyword-debug\");\n\n\tconst reviewKeywordResult = matchUnnegated(prompt, REVIEW_KEYWORD_FAMILY_PATTERN, window);\n\tif (reviewKeywordResult.suppressed) suppressed.push(\"negation:keyword-review\");\n\n\tconst planKeywordResult = matchUnnegated(prompt, PLAN_KEYWORD_FAMILY_PATTERN, window);\n\tif (planKeywordResult.suppressed) suppressed.push(\"negation:keyword-plan\");\n\n\tconst codeGenKeywordResult = matchUnnegated(prompt, CODE_GEN_KEYWORD_FAMILY_PATTERN, window);\n\tif (codeGenKeywordResult.suppressed) suppressed.push(\"negation:keyword-code-gen\");\n\n\tconst addKeywordResult = localEdit ? NO_MATCH_RESULT_V4 : matchUnnegated(prompt, ADD_KEYWORD_PATTERN, window);\n\tif (addKeywordResult.suppressed) suppressed.push(\"negation:add-keyword\");\n\n\treturn {\n\t\tfirstClause: leading,\n\t\tcodeFence: hasCodeFence(prompt),\n\t\tdiffHunk: hasDiffMarkers(prompt),\n\t\tlocalEdit,\n\t\tdiagnosticEvidence,\n\t\treviewScope,\n\t\tplanBrief,\n\t\toperationalRunbook,\n\t\trefactorCue,\n\t\timplementationObject,\n\t\tleadingIntent: primaryIntent,\n\t\tsecondClauseIntent,\n\t\tcompoundIntent,\n\t\tkeywordFamilyMatch: {\n\t\t\ttrivial: false,\n\t\t\tdebug: debugKeywordResult.matched,\n\t\t\trefactor: refactorCue,\n\t\t\treview: reviewKeywordResult.matched,\n\t\t\tplan: planKeywordResult.matched,\n\t\t\t\"simple-edit\": localEditObjectResult.matched,\n\t\t\t\"code-gen\": codeGenKeywordResult.matched,\n\t\t},\n\t\taddKeywordMatch: addKeywordResult.matched,\n\t};\n}\n\n// ============================================================================\n// Scoring\n// ============================================================================\n\nfunction emptyScoresV4(): Record<TaskClassV4, number> {\n\treturn {\n\t\ttrivial: 0,\n\t\t\"simple-edit\": 0,\n\t\t\"code-gen\": 0,\n\t\tdebug: 0,\n\t\trefactor: 0,\n\t\treview: 0,\n\t\tplan: 0,\n\t};\n}\n\nfunction computeScoresV4(features: ContextualFeaturesV4, weights: RouterWeightsV4): Record<TaskClassV4, number> {\n\tconst scores = emptyScoresV4();\n\tif (features.codeFence || features.diffHunk) scores[\"code-gen\"] += weights.codeFenceOrDiff;\n\tif (features.leadingIntent !== null) scores[features.leadingIntent] += weights.leadingIntent;\n\tif (features.secondClauseIntent !== null) scores[features.secondClauseIntent] += weights.secondClauseIntent;\n\tif (features.localEdit) scores[\"simple-edit\"] += weights.localEdit;\n\tif (features.diagnosticEvidence) scores.debug += weights.diagnosticEvidence;\n\tif (features.reviewScope) scores.review += weights.reviewScope;\n\tif (features.planBrief) scores.plan += weights.planBrief;\n\tif (features.operationalRunbook) scores.plan += weights.operationalRunbook;\n\tif (features.refactorCue) scores.refactor += weights.refactorCue;\n\tif (features.implementationObject) scores[\"code-gen\"] += weights.implementationObject;\n\n\tfor (const taskClass of TASK_CLASSES_V4) {\n\t\tif (features.keywordFamilyMatch[taskClass]) scores[taskClass] += weights.keywordFamily[taskClass];\n\t}\n\tif (features.addKeywordMatch) scores[\"code-gen\"] += weights.addKeyword;\n\n\treturn scores;\n}\n\n/**\n * Extension signals (history / context-pressure / judge vote). Every\n * coefficient is 0 under DEFAULT_WEIGHTS_V4 (inert until calibrated by a\n * future governance-backed lane); the mechanism exists so\n * `TaskClassifierInputV4`'s optional fields are meaningfully wired rather than\n * silently accepted-and-ignored.\n */\nfunction applyExtensionSignalsV4(\n\tscores: Record<TaskClassV4, number>,\n\tinput: TaskClassifierInputV4,\n\tweights: RouterWeightsV4,\n): void {\n\tconst priorClass = input.history !== undefined && input.history.length > 0 ? input.history[0] : null;\n\tif (priorClass !== null && weights.multiTurnPrior !== 0) scores[priorClass] += weights.multiTurnPrior;\n\n\tconst judgeVote = input.judgeVote ?? null;\n\tif (judgeVote !== null && weights.judgeVote !== 0) scores[judgeVote] += weights.judgeVote;\n\n\tconst pressureBucket = input.pressureBucket ?? 0;\n\tif (pressureBucket > 0 && weights.pressureBucket !== 0) {\n\t\tconst bump = pressureBucket * weights.pressureBucket;\n\t\tscores.debug += bump;\n\t\tscores.review += bump;\n\t\tscores.plan += bump;\n\t}\n}\n\n// ============================================================================\n// Public types\n// ============================================================================\n\n/**\n * Caller-supplied turn input. Identical shape to `TaskClassifierInputV3` /\n * `TaskClassifierInputV2` (prompt + optional lane/history/pressure/judge\n * slots), so existing callers can switch versions without reshaping data.\n */\nexport interface TaskClassifierInputV4 {\n\tprompt: string;\n\tlaneType?: ReasoningLaneTypeV4;\n\thistory?: readonly TaskClassV4[];\n\tpressureBucket?: number;\n\tjudgeVote?: TaskClassV4 | null;\n}\n\n/** Confidence band derived from `margin / topScore`, bucketed by `weights.lowConfidenceThreshold`/`highConfidenceThreshold`. */\nexport type ConfidenceBandV4 = \"low\" | \"medium\" | \"high\";\n\n/** Which branch of the zero-score fallback cascade decided `taskClass`; null when a real weighted signal decided it. */\nexport type FallbackReasonV4 =\n\t| \"code-fence-or-diff\"\n\t| \"trivial-length\"\n\t| \"ko-short-task-signal\"\n\t| \"long-prose\"\n\t| \"lane-fallback\"\n\t| \"default\";\n\n/**\n * The full, privacy-safe classification verdict (spec 008 Req 2). Every field\n * is a bounded enum, number, boolean, or a closed set of short diagnostic-id\n * strings (`suppressedFeatureIds`) — never raw prompt text, so this value is\n * safe to pass to evaluation, learning, and Adaptorch-advisory code.\n */\nexport interface ClassifierVerdictV4 {\n\t/** The decided task class (post zero-score-cascade if one fired). */\n\treadonly taskClass: TaskClassV4;\n\t/** Raw per-class score map from the weighted signals (pre-cascade; audit-only). */\n\treadonly scores: Readonly<Record<TaskClassV4, number>>;\n\t/** Second-highest-scoring class by the argmax, or null if every other class tied at the bottom. */\n\treadonly runnerUp: TaskClassV4 | null;\n\t/** `scores[argmaxClass] - scores[runnerUp]` (pre-cascade); 0 or negative-margin-free by construction (weights are non-negative). */\n\treadonly margin: number;\n\t/** Normalized confidence in [0, 1]: `margin / topScore`, or 0 when `topScore <= 0` (fallback cascade fired). */\n\treadonly confidence: number;\n\t/** Confidence bucketed by `weights.lowConfidenceThreshold` / `highConfidenceThreshold`. */\n\treadonly confidenceBand: ConfidenceBandV4;\n\t/** True when the argmax class won only via precedence-order tie-break over an exact score tie with the runner-up. */\n\treadonly tieBreak: boolean;\n\t/** Non-null exactly when the zero-score cascade (not a real weighted signal) decided `taskClass`. */\n\treadonly fallbackReason: FallbackReasonV4 | null;\n\t/** Audit trail of `negation:<channel>` ids for every whole-prompt signal that had a match but was fully negated. */\n\treadonly suppressedFeatureIds: readonly string[];\n\t/** True when a distinct second-clause leading intent was detected (bounded compound-intent detection). */\n\treadonly compoundIntent: boolean;\n\t/** The second clause's own leading intent, or null when no compound structure (or no distinct intent) was found. */\n\treadonly secondClauseIntent: TaskClassV4 | null;\n}\n\n// ============================================================================\n// Classifier\n// ============================================================================\n\n/**\n * Deterministic confidence-bearing classifier (spec 008 Req 2).\n *\n * Pipeline: extract negation/compound-aware contextual features -> score every\n * class from `weights` -> argmax with `TASK_CLASSES_V4` precedence tie-break,\n * tracking the runner-up -> compute margin/confidence/confidenceBand/tieBreak\n * from the RAW scores -> if `topScore <= 0`, replace `taskClass` (and record\n * `fallbackReason`) via the zero-score cascade (fence/diff -> code-gen;\n * length < 40 -> trivial; length >= 2400 -> plan; lane fallback; default\n * code-gen) — the raw scores/margin/runnerUp/tieBreak fields still reflect the\n * pre-cascade computation, for audit purposes.\n *\n * Pure: same (input, weights) -> same ClassifierVerdictV4.\n */\nexport function classifyTaskV4(\n\tinput: TaskClassifierInputV4,\n\tweights: RouterWeightsV4 = DEFAULT_WEIGHTS_V4,\n): ClassifierVerdictV4 {\n\tconst prompt = input.prompt.trim();\n\tconst suppressed: string[] = [];\n\tconst features = extractFeaturesV4(prompt, weights, suppressed);\n\tconst scores = computeScoresV4(features, weights);\n\tapplyExtensionSignalsV4(scores, input, weights);\n\n\tlet top: TaskClassV4 = TASK_CLASSES_V4[0];\n\tfor (const c of TASK_CLASSES_V4) {\n\t\tif (scores[c] > scores[top]) top = c;\n\t}\n\tlet runnerUp: TaskClassV4 | null = null;\n\tfor (const c of TASK_CLASSES_V4) {\n\t\tif (c === top) continue;\n\t\tif (runnerUp === null || scores[c] > scores[runnerUp]) runnerUp = c;\n\t}\n\n\tconst topScore = scores[top];\n\tconst runnerUpScore = runnerUp !== null ? scores[runnerUp] : 0;\n\tconst margin = topScore - runnerUpScore;\n\tconst tieBreak = runnerUp !== null && topScore === runnerUpScore;\n\tconst confidence = topScore <= 0 ? 0 : Math.max(0, Math.min(1, margin / topScore));\n\tconst confidenceBand: ConfidenceBandV4 =\n\t\tconfidence <= weights.lowConfidenceThreshold\n\t\t\t? \"low\"\n\t\t\t: confidence >= weights.highConfidenceThreshold\n\t\t\t\t? \"high\"\n\t\t\t\t: \"medium\";\n\n\tlet taskClass: TaskClassV4 = top;\n\tlet fallbackReason: FallbackReasonV4 | null = null;\n\tif (topScore <= 0) {\n\t\tif (features.codeFence || features.diffHunk) {\n\t\t\ttaskClass = \"code-gen\";\n\t\t\tfallbackReason = \"code-fence-or-diff\";\n\t\t} else if (prompt.length < TRIVIAL_MAX_CHARS_V4) {\n\t\t\tconst shortKoreanTaskClass = classifyShortKoreanZeroScoreTaskV4(prompt, weights.negationWindowChars);\n\t\t\tif (shortKoreanTaskClass !== null) {\n\t\t\t\ttaskClass = shortKoreanTaskClass;\n\t\t\t\tfallbackReason = \"ko-short-task-signal\";\n\t\t\t} else {\n\t\t\t\ttaskClass = \"trivial\";\n\t\t\t\tfallbackReason = \"trivial-length\";\n\t\t\t}\n\t\t} else if (prompt.length >= COMPLEX_PROSE_MIN_CHARS_V4) {\n\t\t\ttaskClass = \"plan\";\n\t\t\tfallbackReason = \"long-prose\";\n\t\t} else if (input.laneType !== undefined) {\n\t\t\ttaskClass = LANE_FALLBACK_CLASS_V4[input.laneType];\n\t\t\tfallbackReason = \"lane-fallback\";\n\t\t} else {\n\t\t\ttaskClass = \"code-gen\";\n\t\t\tfallbackReason = \"default\";\n\t\t}\n\t}\n\n\treturn {\n\t\ttaskClass,\n\t\tscores,\n\t\trunnerUp,\n\t\tmargin,\n\t\tconfidence,\n\t\tconfidenceBand,\n\t\ttieBreak,\n\t\tfallbackReason,\n\t\tsuppressedFeatureIds: suppressed,\n\t\tcompoundIntent: features.compoundIntent,\n\t\tsecondClauseIntent: features.secondClauseIntent,\n\t};\n}\n\n// ============================================================================\n// Resolver\n// ============================================================================\n\n/**\n * Static rule table: task class -> recommended ThinkingLevel. Re-exported\n * from the shared resolver core (byte-identical values; previously a\n * duplicated literal here — see reasoning-router-resolver.ts).\n */\nexport const TASK_CLASS_THINKING_LEVELS_V4 = TASK_CLASS_THINKING_LEVELS;\n\n/**\n * Bounded, strictly non-negative escalation applied when the verdict's own\n * confidence is low or a fallback (not a real signal) decided the class. This\n * is the ONLY place confidence touches the target index, and it can only add:\n * confidence can never lower effort (spec 008 Req 2 acceptance criterion).\n */\nconst LOW_CONFIDENCE_ESCALATION_STEPS_V4 = 1;\n\n/**\n * Thin auto-mode wrapper with no uncertainty adjustment: base rule table ->\n * lane step -> clamp to availableLevels. This is v4's \"confident\" path and\n * delegates to the shared resolver core with bias=0, hint=null, escalationSteps=0.\n */\nexport function resolveThinkingLevelV4ForAuto(\n\ttaskClass: TaskClassV4,\n\tavailableLevels: readonly ThinkingLevel[],\n\tlaneType: ReasoningLaneTypeV4 | undefined,\n): ThinkingLevel {\n\treturn resolveThinkingLevelCore(taskClass, availableLevels, laneType, 0, null, 0);\n}\n\n/**\n * Uncertainty-aware resolver (spec 008 Req 2 / plan.md step 5).\n *\n * Pipeline: base rule table for `verdict.taskClass` -> lane step -> bounded\n * bias [-2,+2] -> optional hint fusion (±2 bounded) -> bounded\n * non-negative confidence escalation (+1 ladder step exactly when\n * `verdict.confidenceBand === \"low\"` or `verdict.fallbackReason !== null`) ->\n * clamp to `availableLevels`.\n *\n * The confidence-escalation term is strictly `>= 0`: low confidence can only\n * hold the base+lane+bias+hint target or push it one step higher, never lower\n * it. This guarantees a low-confidence verdict never resolves BELOW what\n * `resolveThinkingLevelV4ForAuto` would give the same class (with bias=0,\n * hint=null) — text alone cannot talk the resolver down.\n */\nexport function resolveThinkingLevelV4WithUncertainty(\n\tverdict: ClassifierVerdictV4,\n\tavailableLevels: readonly ThinkingLevel[],\n\tlaneType: ReasoningLaneTypeV4 | undefined,\n\tbias = 0,\n\thint: { level: ThinkingLevel; confidence: number } | null = null,\n): ThinkingLevel {\n\tconst escalationSteps =\n\t\tverdict.confidenceBand === \"low\" || verdict.fallbackReason !== null ? LOW_CONFIDENCE_ESCALATION_STEPS_V4 : 0;\n\treturn resolveThinkingLevelCore(verdict.taskClass, availableLevels, laneType, bias, hint, escalationSteps);\n}\n"]}
|