@paro.io/expert-shared-components 1.14.56 → 1.14.57

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.
@@ -10,13 +10,13 @@ const SectionOpener_1 = require("./SectionOpener");
10
10
  function Methodology({ profile, eligible, palette }) {
11
11
  return (react_1.default.createElement(react_1.default.Fragment, null,
12
12
  react_1.default.createElement(SectionOpener_1.SectionOpener, { number: "04", eyebrow: "METHODOLOGY", headline: "How TaxAxis arrives at these numbers.", bullets: [
13
- "25 federal strategies evaluated against your profile via deterministic, rule-based computation at temperature zero \u2014 every recommendation traces to a specific IRC section, revenue ruling, or OBBBA provision.",
13
+ "25 federal strategies evaluated against your profile through a two-stage AI and deterministic pipeline \u2014 every recommendation traces to a specific IRC section, revenue ruling, or OBBBA provision.",
14
14
  "Confidence bands reflect data quality: \u00B130% on single-year data, narrowing to \u00B115% with multi-year history.",
15
15
  "Aggregate savings on the cover chart are adjusted by an interaction-discount factor reflecting strategy stacking, marginal-rate exhaustion, and \u00A76694 reasonableness.",
16
16
  ], palette: palette }),
17
17
  react_1.default.createElement("div", { style: { marginBottom: 28 } },
18
18
  react_1.default.createElement("div", { style: { fontSize: 10, fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.12em", color: palette.gray400, fontFamily: palette.mono, marginBottom: 8 } }, "EVAL PIPELINE"),
19
- react_1.default.createElement("div", { style: { fontSize: 13, color: palette.gray700, lineHeight: 1.8, fontFamily: palette.body } }, "TaxAxis evaluates 25 federal tax strategies against each client profile. The computation is entirely deterministic and rule-based, operating at temperature zero \u2014 no generative model is involved at any stage. Each strategy passes through a three-phase pipeline: an eligibility filter removes strategies that do not apply to the entity type, industry, or fact pattern, then the computation engine calculates savings ranges against IRC-cited rules using the client's actual financial data. Every recommendation is reviewed by a qualified tax professional before the report is issued.")),
19
+ react_1.default.createElement("div", { style: { fontSize: 13, color: palette.gray700, lineHeight: 1.8, fontFamily: palette.body } }, "TaxAxis evaluates 25 federal tax strategies against each client profile through a two-stage pipeline. An AI model (Claude Sonnet 4.5 via AWS Bedrock) reads your financial documents, identifies applicable strategies, and drafts narrative content. A deterministic post-processing engine then recomputes savings ranges against IRC-cited rules, corrects confidence bands based on data quality, and validates every output through an 8-gate compliance check \u2014 covering schema integrity, strategy coverage, profile consistency, mathematical accuracy, business rules, \u00A76694 compliance, narrative quality, and entity eligibility. Every recommendation is reviewed by a qualified tax professional before the report is issued.")),
20
20
  react_1.default.createElement("div", { style: { marginBottom: 28 } },
21
21
  react_1.default.createElement("div", { style: { fontSize: 10, fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.12em", color: palette.gray400, fontFamily: palette.mono, marginBottom: 8 } }, "CONFIDENCE BANDS"),
22
22
  react_1.default.createElement("div", { style: { fontSize: 13, color: palette.gray700, lineHeight: 1.8, fontFamily: palette.body, marginBottom: 12 } }, "Savings ranges reflect data quality. Clients who provide multiple years of financial history receive tighter bands of \u00B115%, because multi-year data reveals trends and reduces estimation variance. Single-year data widens the bands to \u00B130%. Within each band, the low estimate uses conservative inputs at the minimum applicable rate, while the high estimate uses full applicable inputs at the client's marginal rate."),
@@ -26,6 +26,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
26
26
  exports.TaxAxisDashboard = TaxAxisDashboard;
27
27
  const react_1 = __importStar(require("react"));
28
28
  const data_1 = require("../../lib/data");
29
+ const strategyNarrative_1 = require("../../lib/data/strategyNarrative");
29
30
  const compute_1 = require("../../lib/compute");
30
31
  const useEngineOutput_1 = require("../../lib/adapters/useEngineOutput");
31
32
  const TaxAxisButton_1 = require("../shared/TaxAxisButton");
@@ -97,18 +98,109 @@ function buildRoadmapBucketLookup(llm) {
97
98
  }
98
99
  return map;
99
100
  }
100
- function mapLlmToStrategies(llm) {
101
+ // LLM sometimes emits these literal placeholder strings as why_it_applies / next_step
102
+ // content. When detected, we fall back to STRATEGY_NARRATIVE static content.
103
+ const PLACEHOLDER_WHY = "This strategy may apply based on your financial profile.";
104
+ const PLACEHOLDER_NEXT = "Discuss with your CPA to evaluate and implement this strategy.";
105
+ function isMissingOrPlaceholder(value, knownPlaceholder) {
106
+ if (!value)
107
+ return true;
108
+ const trimmed = value.trim();
109
+ if (trimmed === "")
110
+ return true;
111
+ if (trimmed === knownPlaceholder)
112
+ return true;
113
+ return false;
114
+ }
115
+ // Parse "S10" -> 10. Returns null if strategyId doesn't match the SN pattern.
116
+ function parseStrategyNumber(strategyId) {
117
+ if (!strategyId)
118
+ return null;
119
+ const match = strategyId.match(/^S(\d+)$/);
120
+ if (!match)
121
+ return null;
122
+ return parseInt(match[1], 10);
123
+ }
124
+ // Build a strategy_id -> client_summary entry lookup using strategy_analysis as the join table.
125
+ function buildClientSummaryLookup(llm) {
126
+ var _a, _b, _c, _d;
127
+ const lookup = new Map();
128
+ const engineOutput = (_a = llm.engineOutput) !== null && _a !== void 0 ? _a : llm.rawOutput;
129
+ if (!engineOutput)
130
+ return lookup;
131
+ const strategyAnalysis = (_b = engineOutput.strategy_analysis) !== null && _b !== void 0 ? _b : [];
132
+ const clientSummaryStrategies = (_d = (_c = engineOutput.client_summary) === null || _c === void 0 ? void 0 : _c.strategies) !== null && _d !== void 0 ? _d : [];
133
+ // strategy_name -> client_summary entry
134
+ const byName = new Map();
135
+ for (const cs of clientSummaryStrategies) {
136
+ if (cs.strategy_name) {
137
+ byName.set(cs.strategy_name, cs);
138
+ }
139
+ }
140
+ // strategy_id -> client_summary entry (via strategy_analysis name lookup)
141
+ for (const sa of strategyAnalysis) {
142
+ if (sa.strategy_id && sa.strategy_name) {
143
+ const cs = byName.get(sa.strategy_name);
144
+ if (cs) {
145
+ lookup.set(sa.strategy_id, cs);
146
+ }
147
+ }
148
+ }
149
+ return lookup;
150
+ }
151
+ function mapLlmToStrategies(llm, profile) {
101
152
  const nameLookup = buildEngineNameLookup(llm);
102
153
  const bucketLookup = buildRoadmapBucketLookup(llm);
154
+ const clientSummaryLookup = buildClientSummaryLookup(llm);
103
155
  return llm.strategies
104
156
  .filter((s) => s.applicable)
105
157
  .map((s, idx) => {
106
- var _a, _b, _c, _d, _e;
158
+ var _a, _b, _c, _d, _e, _f, _g;
107
159
  const id = (_a = s.strategyId) !== null && _a !== void 0 ? _a : s.strategyType;
108
- // Prefer the real strategy_name from engineOutput, fall back to humanized strategyType
109
160
  const name = (_b = nameLookup.get(id)) !== null && _b !== void 0 ? _b : humanizeStrategyType(s.strategyType);
110
161
  const timelineBucket = (_c = bucketLookup.get(id)) !== null && _c !== void 0 ? _c : (s.priority === "HIGH" ? "now" : "90d");
111
162
  const weightedScore = (_d = s.weightedScore) !== null && _d !== void 0 ? _d : (s.priority === "HIGH" ? 85 : s.priority === "MEDIUM" ? 70 : 55);
163
+ // Lookup the client_summary entry for this strategy (top-3 strategies only)
164
+ const csEntry = clientSummaryLookup.get(id);
165
+ const llmWhy = csEntry === null || csEntry === void 0 ? void 0 : csEntry.why_it_applies;
166
+ const llmNext = csEntry === null || csEntry === void 0 ? void 0 : csEntry.next_step;
167
+ // Strategy number for STRATEGY_NARRATIVE static lookup (e.g. "S10" -> 10)
168
+ const strategyNumber = parseStrategyNumber(id);
169
+ const staticNarrative = strategyNumber !== null ? strategyNarrative_1.STRATEGY_NARRATIVE[strategyNumber] : undefined;
170
+ // clientBrief: prefer LLM why_it_applies, fall back to static, then neutral default
171
+ let clientBrief;
172
+ if (!isMissingOrPlaceholder(llmWhy, PLACEHOLDER_WHY)) {
173
+ clientBrief = llmWhy;
174
+ }
175
+ else if (staticNarrative === null || staticNarrative === void 0 ? void 0 : staticNarrative.whyMatters) {
176
+ clientBrief = staticNarrative.whyMatters(profile);
177
+ }
178
+ else {
179
+ clientBrief = "Your preparer will review this strategy and include the analysis in your engagement report.";
180
+ }
181
+ // action: prefer LLM next_step, fall back to static nextSteps, then implementationSteps[0]
182
+ let action;
183
+ if (!isMissingOrPlaceholder(llmNext, PLACEHOLDER_NEXT)) {
184
+ action = llmNext;
185
+ }
186
+ else if (staticNarrative === null || staticNarrative === void 0 ? void 0 : staticNarrative.nextSteps) {
187
+ action = staticNarrative.nextSteps;
188
+ }
189
+ else if ((_e = s.implementationSteps) === null || _e === void 0 ? void 0 : _e[0]) {
190
+ action = s.implementationSteps[0];
191
+ }
192
+ else {
193
+ action = "Work with your CPA to implement this strategy.";
194
+ }
195
+ // Pull calculation_trace fields. Defensive about shape.
196
+ const ct = ((_f = s.calculationTrace) !== null && _f !== void 0 ? _f : {});
197
+ const sourceDocuments = Array.isArray(ct.source_documents) ? ct.source_documents : undefined;
198
+ const specialistNote = typeof ct.specialist_note === "string" && ct.specialist_note.trim() !== ""
199
+ ? ct.specialist_note
200
+ : ((_g = staticNarrative === null || staticNarrative === void 0 ? void 0 : staticNarrative.whySpecialist) !== null && _g !== void 0 ? _g : undefined);
201
+ const positionStrength = typeof ct.position_strength === "string" ? ct.position_strength : undefined;
202
+ const authority = typeof ct.irs_cite === "string" ? ct.irs_cite : undefined;
203
+ const formsFromTrace = Array.isArray(ct.forms_required) ? ct.forms_required.join(", ") : undefined;
112
204
  return {
113
205
  rank: idx + 1,
114
206
  code: id,
@@ -121,13 +213,18 @@ function mapLlmToStrategies(llm) {
121
213
  hi: s.estimatedSavings.max,
122
214
  timeline: timelineBucket === "now" ? "Act now" : timelineBucket === "30d" ? "Within 30 days" : "Within 90 days",
123
215
  timelineBucket,
124
- clientBrief: s.summary,
125
- action: (_e = s.implementationSteps[0]) !== null && _e !== void 0 ? _e : s.summary,
126
- forms: s.requiredForms.join(", "),
216
+ clientBrief,
217
+ action,
218
+ forms: formsFromTrace !== null && formsFromTrace !== void 0 ? formsFromTrace : s.requiredForms.join(", "),
127
219
  abstract: s.summary,
128
220
  sources: [],
129
221
  trace: [],
130
222
  cost: undefined,
223
+ sourceDocuments,
224
+ specialistNote,
225
+ positionStrength,
226
+ authority,
227
+ quickWin: s.quickWin,
131
228
  };
132
229
  });
133
230
  }
@@ -177,8 +274,8 @@ function TaxAxisDashboard({ profile, llmResult, parsedDocuments, onDownloadClien
177
274
  if ((_a = adapted === null || adapted === void 0 ? void 0 : adapted.strategies) === null || _a === void 0 ? void 0 : _a.length)
178
275
  return adapted.strategies;
179
276
  // Fallback to thin mapping for old payload shapes without engineOutput.
180
- return mapLlmToStrategies(llmResult);
181
- }, [hasLlm, adapted, llmResult]);
277
+ return mapLlmToStrategies(llmResult, profile);
278
+ }, [hasLlm, adapted, llmResult, profile]);
182
279
  const llmComputed = (0, react_1.useMemo)(() => {
183
280
  var _a;
184
281
  if (!hasLlm)
@@ -26,10 +26,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
26
26
  exports.TaxAxisProcessing = TaxAxisProcessing;
27
27
  const react_1 = __importStar(require("react"));
28
28
  const ProcessingStages_1 = require("./ProcessingStages");
29
- const CRAWL_CEILING = 80; // hold here until reportReady
30
- const CRAWL_TICK_MS = 180; // interval between crawl ticks
31
- const CRAWL_STEP = 0.35; // % added each tick (reaches 80% in ~41s)
32
- const FINISH_MS = 600; // ms to animate from current% → 100% once ready
29
+ const CRAWL_CEILING = 99; // pause here until reportReady
30
+ const CRAWL_TICK_MS = 300; // interval between crawl ticks
31
+ const CRAWL_STEP = 0.27; // % added each tick 0.27 / 0.3s = 0.9%/s
32
+ const FINISH_MS = 400; // ms to animate from 99% → 100% once ready
33
33
  const STAGE_ADVANCE_MS = 3200; // how often the displayed stage advances
34
34
  function buildStages(profile) {
35
35
  var _a;
@@ -69,6 +69,30 @@ function TaxAxisProcessing({ onComplete, profile, reportReady = false, userConte
69
69
  // Track current progress in a ref so the finish animation can read it synchronously
70
70
  // without relying on the async functional-updater pattern.
71
71
  const progressRef = (0, react_1.useRef)(0);
72
+ // ── Stall detection: show patience message after 35s of no progress change ──
73
+ const [showPatience, setShowPatience] = (0, react_1.useState)(false);
74
+ const lastProgressRef = (0, react_1.useRef)(0);
75
+ const stallTimerRef = (0, react_1.useRef)(null);
76
+ (0, react_1.useEffect)(() => {
77
+ if (finishingRef.current) {
78
+ setShowPatience(false);
79
+ return;
80
+ }
81
+ if (progress !== lastProgressRef.current) {
82
+ lastProgressRef.current = progress;
83
+ setShowPatience(false);
84
+ if (stallTimerRef.current)
85
+ clearTimeout(stallTimerRef.current);
86
+ stallTimerRef.current = setTimeout(() => {
87
+ if (!finishingRef.current)
88
+ setShowPatience(true);
89
+ }, 8000);
90
+ }
91
+ return () => {
92
+ if (stallTimerRef.current)
93
+ clearTimeout(stallTimerRef.current);
94
+ };
95
+ }, [progress]);
72
96
  const setProgressSync = (v) => {
73
97
  progressRef.current = v;
74
98
  setProgress(v);
@@ -147,5 +171,6 @@ function TaxAxisProcessing({ onComplete, profile, reportReady = false, userConte
147
171
  transition: isFinishing ? "none" : "width 0.18s linear",
148
172
  } })),
149
173
  react_1.default.createElement(ProcessingStages_1.ProcessingStages, { stages: stages, progress: stageIdx + 1 }),
150
- pct >= CRAWL_CEILING && !isFinishing && (react_1.default.createElement("p", { className: "text-[11px] text-tax-axis-text-4 font-tax-axis-mono mt-4 animate-pulse" }, "Finalizing analysis\u2026"))));
174
+ pct >= CRAWL_CEILING && !isFinishing && (react_1.default.createElement("p", { className: "text-[11px] text-tax-axis-text-4 font-tax-axis-mono mt-4 animate-pulse" }, "Finalizing analysis\u2026")),
175
+ showPatience && !isFinishing && (react_1.default.createElement("p", { className: "text-[13px] text-tax-axis-text-3 font-tax-axis-body mt-4 text-center" }, "Still running \u2014 document analysis typically takes 2\u20133 minutes. Do not navigate away."))));
151
176
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paro.io/expert-shared-components",
3
- "version": "1.14.56",
3
+ "version": "1.14.57",
4
4
  "description": "",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {