@paro.io/expert-shared-components 1.14.67 → 1.14.68

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.
@@ -70,12 +70,6 @@ const PERIOD_OPTIONS = ["Full Year", "YTD"];
70
70
  const YEAR_OPTIONS = ["2024", "2025", "2026"];
71
71
  function ClientParametersSection({ userContext = "expert", }) {
72
72
  const { control, formState: { errors }, } = (0, react_hook_form_1.useFormContext)();
73
- // TECH DEBT (post-BDO): Analysis Period dropdown is local useState only —
74
- // value does not persist to form state because ClientProfile has no `period`
75
- // field. computeAllStrategies does not read this value. Spec defect.
76
- // Resolve post-BDO by either adding `period` to ClientProfile or removing
77
- // the field entirely.
78
- const [analysisPeriod, setAnalysisPeriod] = (0, react_1.useState)("Full Year");
79
73
  const [expanded, setExpanded] = (0, react_1.useState)(true);
80
74
  return (react_1.default.createElement(TaxAxisCard_1.TaxAxisCard, null,
81
75
  react_1.default.createElement("div", { onClick: () => setExpanded((prev) => !prev), className: "flex items-center justify-between cursor-pointer", style: { marginBottom: expanded ? 14 : 0 } },
@@ -106,9 +100,12 @@ function ClientParametersSection({ userContext = "expert", }) {
106
100
  errors.industry && react_1.default.createElement("div", { className: errCls }, errors.industry.message)),
107
101
  react_1.default.createElement("div", null,
108
102
  react_1.default.createElement("label", { className: labelCls }, "Analysis Period"),
109
- react_1.default.createElement("div", { className: "relative" },
110
- react_1.default.createElement("select", { value: analysisPeriod, onChange: e => setAnalysisPeriod(e.target.value), className: selectCls }, PERIOD_OPTIONS.map(o => (react_1.default.createElement("option", { key: o, value: o, className: "bg-tax-axis-surface-2" }, o)))),
111
- react_1.default.createElement(Chevron, null))),
103
+ react_1.default.createElement(react_hook_form_1.Controller, { name: "period", control: control, render: ({ field }) => {
104
+ var _a;
105
+ return (react_1.default.createElement("div", { className: "relative" },
106
+ react_1.default.createElement("select", Object.assign({}, field, { value: (_a = field.value) !== null && _a !== void 0 ? _a : "Full Year", className: selectCls }), PERIOD_OPTIONS.map(o => (react_1.default.createElement("option", { key: o, value: o, className: "bg-tax-axis-surface-2" }, o)))),
107
+ react_1.default.createElement(Chevron, null)));
108
+ } })),
112
109
  react_1.default.createElement("div", null,
113
110
  react_1.default.createElement("label", { className: labelCls }, "Annual Revenue"),
114
111
  react_1.default.createElement(react_hook_form_1.Controller, { name: "revenue", control: control, render: ({ field }) => (react_1.default.createElement("div", { className: "relative" },
@@ -106,13 +106,34 @@ function StrategyRadar({ profile }) {
106
106
  const eligibleStrategies = (0, compute_1.filterEligibleStrategies)(safeProfile);
107
107
  const eligibleIds = new Set(eligibleStrategies.map(s => String(s.rank)));
108
108
  const stratCount = eligibleStrategies.length;
109
- const computed = (0, react_1.useMemo)(() => (0, compute_1.computeAllStrategies)(safeProfile), [profile]);
110
- const loSum = eligibleStrategies.reduce((a, s) => { var _a, _b; return a + ((_b = (_a = computed.get(s.rank)) === null || _a === void 0 ? void 0 : _a.lo) !== null && _b !== void 0 ? _b : s.lo); }, 0);
111
- const hiSum = eligibleStrategies.reduce((a, s) => { var _a, _b; return a + ((_b = (_a = computed.get(s.rank)) === null || _a === void 0 ? void 0 : _a.hi) !== null && _b !== void 0 ? _b : s.hi); }, 0);
109
+ // Serialize to primitive — watch() may return a reference-stable proxy,
110
+ // so [profile] as a dep would never re-fire the memo on field changes
111
+ const profileKey = JSON.stringify(profile);
112
+ const computed = (0, react_1.useMemo)(() => (0, compute_1.computeAllStrategies)(safeProfile), [profileKey]);
113
+ const fedRate = parseFloat((profile.federalRate || "24").replace("%", "")) / 100 || 0.24;
114
+ const stateRate = parseFloat(profile.stateRate || "4.95") / 100;
115
+ const netIncome = parseInt((profile.netIncome || "0").replace(/,/g, "")) || Math.round(rev * 0.3);
116
+ let loSum = eligibleStrategies.reduce((a, s) => { var _a, _b; return a + ((_b = (_a = computed.get(s.rank)) === null || _a === void 0 ? void 0 : _a.lo) !== null && _b !== void 0 ? _b : s.lo); }, 0);
117
+ let hiSum = eligibleStrategies.reduce((a, s) => { var _a, _b; return a + ((_b = (_a = computed.get(s.rank)) === null || _a === void 0 ? void 0 : _a.hi) !== null && _b !== void 0 ? _b : s.hi); }, 0);
118
+ // Tax-capacity cap: savings cannot exceed approximate total tax liability
119
+ const approxTaxLiability = Math.round(netIncome * (fedRate + stateRate));
120
+ if (approxTaxLiability > 0) {
121
+ hiSum = Math.min(hiSum, approxTaxLiability);
122
+ loSum = Math.min(loSum, hiSum);
123
+ // If cap squashed lo close to hi, re-spread to maintain a meaningful range
124
+ if (hiSum > 0 && loSum >= hiSum * 0.9) {
125
+ loSum = Math.round(hiSum / 3);
126
+ }
127
+ }
128
+ // Aggregate spread constraint: max 3× ratio between hi and lo
129
+ if (hiSum > 0 && loSum > 0 && hiSum / loSum > 3.0) {
130
+ loSum = Math.round(hiSum / 3.0);
131
+ }
112
132
  const highImpact = eligibleStrategies.filter(s => s.priority === "HIGH").length;
113
133
  const quickWins = eligibleStrategies.filter(s => s.priority === "QUICK WIN").length;
114
134
  const obbbaNew = eligibleStrategies.filter(s => s.cat === "obbba_new").length;
115
- const hasPTE = states.some(s => states_1.PTE_STATES.has(s));
135
+ // PTE elections are only available for pass-through entities; C-Corps are ineligible
136
+ const hasPTE = entity !== "C-Corporation" && states.some(s => states_1.PTE_STATES.has(s));
116
137
  const hasNoTax = states.some(s => states_1.NO_INCOME_TAX.has(s));
117
138
  const total = RADAR_NODES.length;
118
139
  // G4 — Sizing: compact vs expanded
@@ -31,6 +31,7 @@ export declare const intakeSchema: Yup.ObjectSchema<import("yup/lib/object").Ass
31
31
  hsaContributions: Yup.StringSchema<string, import("yup/lib/types").AnyObject, string>;
32
32
  wotcHires: Yup.StringSchema<string, import("yup/lib/types").AnyObject, string>;
33
33
  familyEmployed: Yup.StringSchema<string, import("yup/lib/types").AnyObject, string>;
34
+ period: Yup.StringSchema<string, import("yup/lib/types").AnyObject, string>;
34
35
  }>, import("yup/lib/object").AnyObject, import("yup/lib/object").TypeOfShape<import("yup/lib/object").Assign<import("yup/lib/object").ObjectShape, {
35
36
  bizName: Yup.StringSchema<string, import("yup/lib/types").AnyObject, string>;
36
37
  cpaName: Yup.StringSchema<string, import("yup/lib/types").AnyObject, string>;
@@ -62,6 +63,7 @@ export declare const intakeSchema: Yup.ObjectSchema<import("yup/lib/object").Ass
62
63
  hsaContributions: Yup.StringSchema<string, import("yup/lib/types").AnyObject, string>;
63
64
  wotcHires: Yup.StringSchema<string, import("yup/lib/types").AnyObject, string>;
64
65
  familyEmployed: Yup.StringSchema<string, import("yup/lib/types").AnyObject, string>;
66
+ period: Yup.StringSchema<string, import("yup/lib/types").AnyObject, string>;
65
67
  }>>, import("yup/lib/object").AssertsShape<import("yup/lib/object").Assign<import("yup/lib/object").ObjectShape, {
66
68
  bizName: Yup.StringSchema<string, import("yup/lib/types").AnyObject, string>;
67
69
  cpaName: Yup.StringSchema<string, import("yup/lib/types").AnyObject, string>;
@@ -93,5 +95,6 @@ export declare const intakeSchema: Yup.ObjectSchema<import("yup/lib/object").Ass
93
95
  hsaContributions: Yup.StringSchema<string, import("yup/lib/types").AnyObject, string>;
94
96
  wotcHires: Yup.StringSchema<string, import("yup/lib/types").AnyObject, string>;
95
97
  familyEmployed: Yup.StringSchema<string, import("yup/lib/types").AnyObject, string>;
98
+ period: Yup.StringSchema<string, import("yup/lib/types").AnyObject, string>;
96
99
  }>>>;
97
100
  export declare const intakeDefaultValues: Partial<ClientProfile>;
@@ -56,6 +56,7 @@ exports.intakeSchema = Yup.object().shape({
56
56
  hsaContributions: Yup.string().ensure(),
57
57
  wotcHires: Yup.string().ensure(),
58
58
  familyEmployed: Yup.string().ensure(),
59
+ period: Yup.string().ensure(),
59
60
  });
60
61
  exports.intakeDefaultValues = {
61
62
  bizName: "",
@@ -88,4 +89,5 @@ exports.intakeDefaultValues = {
88
89
  hsaContributions: "0",
89
90
  wotcHires: "No",
90
91
  familyEmployed: "No",
92
+ period: "Full Year",
91
93
  };
@@ -1,151 +1,65 @@
1
1
  import type { Strategy, ComputedMap } from "../types";
2
- export interface SourceDocumentRef {
3
- field_label: string;
4
- value_display: string;
5
- source_hint: string;
6
- }
7
- export interface EngineCalculationTrace {
8
- strategy_id: string;
9
- branch_executed: string | null;
10
- formula: string;
11
- inputs: Record<string, unknown>;
12
- result_gross: number | null;
13
- confidence_interval: {
14
- low: number;
15
- high: number;
16
- basis: string;
17
- } | null;
18
- position_strength: string;
19
- irs_cite: string;
20
- data_quality_flag: string | null;
21
- forms_required: string[];
22
- deadline_flag: string | null;
23
- amt_flag: boolean;
24
- source_documents?: SourceDocumentRef[];
25
- specialist_note?: string | null;
26
- }
27
2
  export interface EngineStrategyAnalysis {
28
- strategy_id: string;
29
- strategy_name: string;
30
- priority_tier: "HIGH" | "MEDIUM" | "LOW" | "NOT_RECOMMENDED";
31
- weighted_score: number;
32
- scores: {
33
- financial_impact: number;
34
- applicability: number;
35
- complexity: number;
36
- risk: number;
37
- cash_flow: number;
3
+ strategy_id?: string;
4
+ strategy_name?: string;
5
+ lo?: number;
6
+ hi?: number;
7
+ estimatedSavings?: {
8
+ min?: number;
9
+ max?: number;
38
10
  };
39
- quick_win: boolean;
40
- calculation_trace: EngineCalculationTrace;
41
- engagement_recommendation: string;
42
- risk_disclosure: string | null;
43
- }
44
- export interface EngineClientSummaryStrategy {
45
- slot: number;
46
- strategy_name: string;
47
- savings_range: string;
48
- why_it_applies: string;
49
- next_step: string;
50
- quick_win_badge: boolean;
51
- deadline_flag?: string | null;
11
+ priority?: string;
12
+ score?: number;
13
+ weighted_score?: number;
14
+ quick_win?: boolean;
15
+ timeline_bucket?: string;
16
+ calculation_trace?: {
17
+ irs_cite?: string;
18
+ forms_required?: string[];
19
+ source_documents?: Array<{
20
+ field_label: string;
21
+ value_display: string;
22
+ source_hint: string;
23
+ }>;
24
+ position_strength?: string;
25
+ specialist_note?: string;
26
+ };
27
+ why_it_applies?: string;
28
+ next_step?: string;
29
+ risk_disclosure?: string;
52
30
  }
53
31
  export interface EngineCpaWorkflow {
54
- workpaper_codes: string[] | null;
55
- section_6694_notices: Array<{
56
- strategy_id: string;
57
- position_strength: string;
58
- notice: string;
59
- }>;
60
- prior_year_flags: string[];
61
- conflict_of_interest_checklist: Array<{
62
- strategy_id: string;
63
- conflict_prompt: string;
64
- confirmed_no_conflict: boolean;
65
- }>;
66
- engagement_recommendations: string[];
32
+ engagement_recommendations?: string[];
67
33
  }
68
34
  export interface EngineOutput {
69
- engagement_id: string;
70
- generated_at: string;
71
- algorithm_version: string;
72
- business_profile: {
73
- entity_type: string;
74
- industry: string;
75
- industry_vertical: string;
76
- states: string[];
77
- data_years: number;
78
- confidence_tier: string;
79
- effective_tax_rate: number | null;
80
- data_quality_flags: string[];
81
- };
82
- eligibility_screening: {
83
- excluded_strategies: Array<{
84
- strategy_id: string;
85
- status: "EXCLUDED";
86
- reason: string;
87
- irs_cite: string;
88
- }>;
89
- };
90
- strategy_analysis: EngineStrategyAnalysis[];
91
- strategy_interaction_warnings: Array<{
92
- strategy_pair: [string, string];
93
- warning_text: string;
94
- }>;
95
- implementation_roadmap: {
96
- quick_wins: Array<{
97
- strategy_id: string;
98
- action: string;
99
- deadline?: string | null;
100
- }>;
101
- immediate: Array<{
102
- strategy_id: string;
103
- action: string;
104
- deadline?: string | null;
105
- }>;
106
- thirty_day: Array<{
107
- strategy_id: string;
108
- action: string;
109
- deadline?: string | null;
110
- }>;
111
- ninety_day: Array<{
112
- strategy_id: string;
113
- action: string;
114
- deadline?: string | null;
115
- }>;
116
- annual: Array<{
117
- strategy_id: string;
118
- action: string;
119
- deadline?: string | null;
35
+ strategy_analysis?: EngineStrategyAnalysis[];
36
+ cpa_workflow?: EngineCpaWorkflow;
37
+ client_summary?: {
38
+ opening?: string;
39
+ closing?: string;
40
+ data_quality_note?: string;
41
+ interaction_warning?: string;
42
+ strategies?: Array<{
43
+ strategy_name?: string;
44
+ why_it_applies?: string;
45
+ next_step?: string;
120
46
  }>;
121
47
  };
122
- cpa_workflow: EngineCpaWorkflow;
123
- client_summary: {
124
- opening: string;
125
- strategies: EngineClientSummaryStrategy[];
126
- data_quality_note: string | null;
127
- interaction_warning: string | null;
128
- closing: string;
129
- };
130
- risk_disclosures: string[];
131
- client_risk_disclosures: {
132
- accuracy_penalty_warning: string | null;
133
- applies_to_strategies: string[];
134
- };
135
- amt_flags: Array<{
136
- trigger: string;
137
- strategy_id: string;
138
- flag_text: string;
139
- }>;
140
- nexus_flags: Array<{
48
+ nexus_flags?: Array<{
141
49
  state: string;
142
50
  nexus_type: string;
143
51
  flag_text: string;
144
52
  }>;
53
+ eligibility_screening?: {
54
+ excluded_strategies?: Array<{
55
+ strategy_id: string;
56
+ reason: string;
57
+ irs_cite: string;
58
+ }>;
59
+ };
145
60
  }
146
61
  export interface EngineOutputAdapterResult {
147
62
  strategies: Strategy[];
148
63
  computedMap: ComputedMap;
149
- engineOutput: EngineOutput;
150
64
  }
151
65
  export declare function useEngineOutput(engineOutput: EngineOutput | null | undefined): EngineOutputAdapterResult | null;
@@ -1,161 +1,56 @@
1
1
  "use strict";
2
- // ═══════════════════════════════════════════════════════════════════
3
- // useEngineOutput — adapts live EngineOutput from paro-graphql-api
4
- // into the Strategy[] + ComputedMap shapes the FE components consume.
5
- //
6
- // Strategy: use the static STRATEGIES catalog as a structural template
7
- // (rank, cat, entities, authority, etc.) and override savings/priority/score
8
- // from the live engine data. Falls back to static data when a strategy_id
9
- // has no match in the catalog (future-proofing for new strategies).
10
- // ═══════════════════════════════════════════════════════════════════
11
2
  Object.defineProperty(exports, "__esModule", { value: true });
12
3
  exports.useEngineOutput = useEngineOutput;
13
4
  const react_1 = require("react");
14
- const data_1 = require("../data");
15
- // ── Strategy ID → rank mapping (S1..S26 → rank 1..25 catalog) ───────
16
- // Maps engine strategy_ids like "S2", "S5" etc. to catalog ranks.
17
- // Built once from the static STRATEGIES array by matching code/name heuristics.
18
- const ENGINE_ID_TO_RANK = {
19
- S1: 1, // Business Entity Structure (S-Corp election)
20
- S2: 2, // Section 179
21
- S3: 3, // Bonus Depreciation §168(k)
22
- S4: 4, // QBI §199A
23
- S5: 5, // R&D Credit §41
24
- S6: 6, // WOTC §51
25
- S7: 7, // Retirement Plan
26
- S8: 8, // Cost Segregation
27
- S9: 9, // HSA
28
- S10: 10, // Business Meals
29
- S11: 11, // Professional Services / Legal
30
- S12: 12, // Home Office
31
- S13: 13, // Health Care Credit §45R
32
- S14: 14, // Charitable / DAF
33
- S15: 15, // SALT PTE
34
- S16: 16, // §163(j) Interest
35
- S17: 17, // Income Deferral
36
- S18: 18, // Opportunity Zone §1400Z
37
- S19: 19, // §179D Energy
38
- S20: 20, // Accounting Method
39
- S21: 21, // OBBBA Overtime
40
- S22: 22, // OBBBA Tips / Overtime
41
- S23: 23, // §174A R&E catch-up
42
- S24: 24, // Childcare §45F
43
- S25: 25, // OBBBA Trump Accounts
44
- };
45
- const CATALOG_BY_RANK = new Map(data_1.STRATEGIES.map((s) => [s.rank, s]));
46
- // ── Priority tier mapping ─────────────────────────────────────────────
47
- function mapPriority(tier, quickWin) {
48
- if (quickWin)
49
- return "QUICK WIN";
50
- if (tier === "HIGH")
51
- return "HIGH";
52
- if (tier === "MEDIUM")
53
- return "MEDIUM";
54
- if (tier === "LOW")
55
- return "LOW";
56
- return "NOT RECOMMENDED";
5
+ function humanize(id) {
6
+ return id
7
+ .replace(/[_-]/g, " ")
8
+ .replace(/\b\w/g, (c) => c.toUpperCase());
57
9
  }
58
- // ── Timeline bucket from implementation_roadmap ───────────────────────
59
- function resolveTimelineBucket(strategyId, roadmap) {
60
- if (roadmap.quick_wins.some((r) => r.strategy_id === strategyId))
61
- return "now";
62
- if (roadmap.immediate.some((r) => r.strategy_id === strategyId))
63
- return "now";
64
- if (roadmap.thirty_day.some((r) => r.strategy_id === strategyId))
65
- return "30d";
66
- if (roadmap.ninety_day.some((r) => r.strategy_id === strategyId))
67
- return "90d";
68
- if (roadmap.annual.some((r) => r.strategy_id === strategyId))
69
- return "filing";
70
- return "filing";
71
- }
72
- // ── CalculationTrace → StrategyTraceStep[] ───────────────────────────
73
- function traceFromEngine(trace) {
74
- var _a, _b;
75
- const steps = [];
76
- // Emit formula as step 1
77
- if (trace.formula) {
78
- steps.push({
79
- n: 1,
80
- step: "Formula",
81
- formula: trace.formula,
82
- result: trace.result_gross != null ? `$${trace.result_gross.toLocaleString()}` : "See CI",
83
- src: trace.irs_cite || "",
84
- });
85
- }
86
- // Emit confidence interval as step 2 when present
87
- if (((_a = trace.confidence_interval) === null || _a === void 0 ? void 0 : _a.low) != null && ((_b = trace.confidence_interval) === null || _b === void 0 ? void 0 : _b.high) != null) {
88
- steps.push({
89
- n: 2,
90
- step: "Confidence range",
91
- formula: `Low × ${trace.confidence_interval.basis}`,
92
- result: `$${trace.confidence_interval.low.toLocaleString()} – $${trace.confidence_interval.high.toLocaleString()}`,
93
- src: trace.irs_cite || "",
94
- });
95
- }
96
- return steps;
97
- }
98
- function adaptEngineOutput(engineOutput) {
10
+ function mapSingleStrategy(sa, idx) {
99
11
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t;
100
- const strategies = [];
101
- const computedMap = new Map();
102
- for (const analysis of engineOutput.strategy_analysis) {
103
- if (analysis.priority_tier === "NOT_RECOMMENDED")
104
- continue;
105
- const rank = ENGINE_ID_TO_RANK[analysis.strategy_id];
106
- const template = rank != null ? CATALOG_BY_RANK.get(rank) : undefined;
107
- const trace = analysis.calculation_trace;
108
- const ci = trace === null || trace === void 0 ? void 0 : trace.confidence_interval;
109
- const lo = (_b = (_a = ci === null || ci === void 0 ? void 0 : ci.low) !== null && _a !== void 0 ? _a : template === null || template === void 0 ? void 0 : template.lo) !== null && _b !== void 0 ? _b : 0;
110
- const hi = (_d = (_c = ci === null || ci === void 0 ? void 0 : ci.high) !== null && _c !== void 0 ? _c : template === null || template === void 0 ? void 0 : template.hi) !== null && _d !== void 0 ? _d : 0;
111
- const timelineBucket = resolveTimelineBucket(analysis.strategy_id, engineOutput.implementation_roadmap);
112
- const clientSummaryEntry = engineOutput.client_summary.strategies.find((cs) => cs.strategy_name === analysis.strategy_name);
113
- const strategy = {
114
- rank: rank !== null && rank !== void 0 ? rank : 99,
115
- code: (_e = template === null || template === void 0 ? void 0 : template.code) !== null && _e !== void 0 ? _e : analysis.strategy_id,
116
- name: analysis.strategy_name,
117
- cat: (_f = template === null || template === void 0 ? void 0 : template.cat) !== null && _f !== void 0 ? _f : "deductions",
118
- priority: mapPriority(analysis.priority_tier, analysis.quick_win),
119
- score: Math.round(analysis.weighted_score),
120
- entities: (_g = template === null || template === void 0 ? void 0 : template.entities) !== null && _g !== void 0 ? _g : ["S-Corporation", "C-Corporation", "Partnership/LLC", "Sole Proprietorship"],
121
- lo,
122
- hi,
123
- timeline: (_h = template === null || template === void 0 ? void 0 : template.timeline) !== null && _h !== void 0 ? _h : timelineBucket,
124
- timelineBucket,
125
- authority: (_j = trace === null || trace === void 0 ? void 0 : trace.irs_cite) !== null && _j !== void 0 ? _j : template === null || template === void 0 ? void 0 : template.authority,
126
- forms: (_l = (_k = trace === null || trace === void 0 ? void 0 : trace.forms_required) === null || _k === void 0 ? void 0 : _k.join(", ")) !== null && _l !== void 0 ? _l : template === null || template === void 0 ? void 0 : template.forms,
127
- warning: (_o = (_m = analysis.risk_disclosure) !== null && _m !== void 0 ? _m : template === null || template === void 0 ? void 0 : template.warning) !== null && _o !== void 0 ? _o : null,
128
- abstract: analysis.engagement_recommendation,
129
- trace: traceFromEngine(trace),
130
- needsPTE: template === null || template === void 0 ? void 0 : template.needsPTE,
131
- onlyIndustry: template === null || template === void 0 ? void 0 : template.onlyIndustry,
132
- excludeIndustry: template === null || template === void 0 ? void 0 : template.excludeIndustry,
133
- preferIndustry: template === null || template === void 0 ? void 0 : template.preferIndustry,
134
- minEmployees: template === null || template === void 0 ? void 0 : template.minEmployees,
135
- needsOwnerComp: template === null || template === void 0 ? void 0 : template.needsOwnerComp,
136
- clientBrief: (_p = clientSummaryEntry === null || clientSummaryEntry === void 0 ? void 0 : clientSummaryEntry.why_it_applies) !== null && _p !== void 0 ? _p : template === null || template === void 0 ? void 0 : template.clientBrief,
137
- action: (_q = clientSummaryEntry === null || clientSummaryEntry === void 0 ? void 0 : clientSummaryEntry.next_step) !== null && _q !== void 0 ? _q : template === null || template === void 0 ? void 0 : template.action,
138
- sourceDocuments: (_r = trace === null || trace === void 0 ? void 0 : trace.source_documents) !== null && _r !== void 0 ? _r : [],
139
- quickWin: analysis.quick_win,
140
- positionStrength: (_s = trace === null || trace === void 0 ? void 0 : trace.position_strength) !== null && _s !== void 0 ? _s : undefined,
141
- specialistNote: (_t = trace === null || trace === void 0 ? void 0 : trace.specialist_note) !== null && _t !== void 0 ? _t : undefined,
142
- };
143
- strategies.push(strategy);
144
- computedMap.set(strategy.rank, {
145
- lo,
146
- hi,
147
- trace: strategy.trace,
148
- });
149
- }
150
- // Sort by weighted_score descending (same ordering the FE uses)
151
- strategies.sort((a, b) => b.score - a.score);
152
- return { strategies, computedMap, engineOutput };
12
+ const ct = (_a = sa.calculation_trace) !== null && _a !== void 0 ? _a : {};
13
+ const lo = (_d = (_b = sa.lo) !== null && _b !== void 0 ? _b : (_c = sa.estimatedSavings) === null || _c === void 0 ? void 0 : _c.min) !== null && _d !== void 0 ? _d : 0;
14
+ const hi = (_g = (_e = sa.hi) !== null && _e !== void 0 ? _e : (_f = sa.estimatedSavings) === null || _f === void 0 ? void 0 : _f.max) !== null && _g !== void 0 ? _g : 0;
15
+ const weightedScore = (_j = (_h = sa.weighted_score) !== null && _h !== void 0 ? _h : sa.score) !== null && _j !== void 0 ? _j : (sa.priority === "HIGH" ? 85 : sa.priority === "MEDIUM" ? 70 : 55);
16
+ const timelineBucket = (_k = sa.timeline_bucket) !== null && _k !== void 0 ? _k : (sa.priority === "HIGH" ? "now" : "90d");
17
+ return {
18
+ rank: idx + 1,
19
+ code: (_l = sa.strategy_id) !== null && _l !== void 0 ? _l : `S${idx + 1}`,
20
+ name: (_m = sa.strategy_name) !== null && _m !== void 0 ? _m : humanize((_o = sa.strategy_id) !== null && _o !== void 0 ? _o : `Strategy ${idx + 1}`),
21
+ cat: "income",
22
+ priority: (((_p = sa.priority) !== null && _p !== void 0 ? _p : "MEDIUM").toUpperCase()),
23
+ score: Math.round(weightedScore),
24
+ entities: [],
25
+ lo,
26
+ hi,
27
+ timeline: timelineBucket === "now" ? "Act now" : timelineBucket === "30d" ? "Within 30 days" : "Within 90 days",
28
+ timelineBucket,
29
+ clientBrief: (_q = sa.why_it_applies) !== null && _q !== void 0 ? _q : "Your preparer will review this strategy and include the analysis in your engagement report.",
30
+ action: (_r = sa.next_step) !== null && _r !== void 0 ? _r : "Work with your CPA to implement this strategy.",
31
+ forms: Array.isArray(ct.forms_required) ? ct.forms_required.join(", ") : undefined,
32
+ abstract: sa.why_it_applies,
33
+ warning: (_s = sa.risk_disclosure) !== null && _s !== void 0 ? _s : null,
34
+ sources: [],
35
+ trace: [],
36
+ cost: undefined,
37
+ sourceDocuments: Array.isArray(ct.source_documents) ? ct.source_documents : undefined,
38
+ positionStrength: ct.position_strength,
39
+ specialistNote: ct.specialist_note,
40
+ authority: ct.irs_cite,
41
+ quickWin: (_t = sa.quick_win) !== null && _t !== void 0 ? _t : false,
42
+ };
153
43
  }
154
- // ── React hook ────────────────────────────────────────────────────────
155
44
  function useEngineOutput(engineOutput) {
156
45
  return (0, react_1.useMemo)(() => {
157
- if (!engineOutput)
46
+ const analysis = engineOutput === null || engineOutput === void 0 ? void 0 : engineOutput.strategy_analysis;
47
+ if (!Array.isArray(analysis) || analysis.length === 0)
158
48
  return null;
159
- return adaptEngineOutput(engineOutput);
49
+ const strategies = analysis.map((sa, idx) => mapSingleStrategy(sa, idx));
50
+ const computedMap = new Map();
51
+ for (const s of strategies) {
52
+ computedMap.set(s.rank, { lo: s.lo, hi: s.hi });
53
+ }
54
+ return { strategies, computedMap };
160
55
  }, [engineOutput]);
161
56
  }
@@ -159,5 +159,11 @@ function computeAllStrategies(profile) {
159
159
  results.set(s.rank, { lo: Math.round(s.lo * scale), hi: Math.round(s.hi * scale), sources: s.sources, trace: s.trace });
160
160
  }
161
161
  });
162
+ // Enforce max 3× spread per strategy — tighten lo toward hi if ratio is too wide
163
+ for (const [rank, v] of results) {
164
+ if (v.hi > 0 && v.lo > 0 && v.hi / v.lo > 3.0) {
165
+ results.set(rank, Object.assign(Object.assign({}, v), { lo: Math.round(v.hi / 3.0) }));
166
+ }
167
+ }
162
168
  return results;
163
169
  }