@paro.io/expert-shared-components 1.14.55 → 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."),
@@ -25,49 +25,46 @@ function positionStrengthLabel(positionStrength) {
25
25
  return { label: positionStrength, color: "#9498B8", dots: 2 };
26
26
  }
27
27
  /**
28
- * Split engagement_recommendation into two parts:
29
- * - savingsSection: sentences/paragraphs that contain dollar figures or formula references
30
- * → shown as "HOW SAVINGS BREAK DOWN"
31
- * - cpaSection: the remaining implementation/CPA-action paragraphs
32
- * → shown as "CPA ENGAGEMENT NOTES"
28
+ * Split engagement_recommendation at the implementation/action boundary.
33
29
  *
34
- * Strategy: split on double-newlines or sentence boundaries. Sentences with "$" or
35
- * formulas go to savings; implementation/deadline/CPA sentences go to cpa.
30
+ * The LLM writes engagement_recommendation as a single paragraph covering both
31
+ * the savings math AND the CPA implementation steps. We split at the first
32
+ * implementation keyword so the savings math shows under "HOW SAVINGS BREAK DOWN"
33
+ * and the CPA action steps show under "CPA ENGAGEMENT NOTES".
34
+ *
35
+ * Heuristic: split at "Implementation:", "CPA should", "CPA must", or numbered
36
+ * action lists "(1)". If no split point found, entire text goes to savings section.
36
37
  */
37
38
  function splitEngagementText(text) {
38
39
  if (!text)
39
40
  return { savingsSection: "", cpaSection: "" };
40
- // Split into paragraphs first, then sentences within paragraphs
41
- const rawParas = text.split(/\n{2,}/).map((p) => p.trim()).filter(Boolean);
42
- const savingsParas = [];
43
- const cpaParas = [];
44
- for (const para of rawParas) {
45
- const hasDollar = /\$[\d,]+/.test(para);
46
- const hasFormula = /SAVINGS\s*=|CREDIT\s*=|DEDUCTION\s*=|ADDITIONAL_CONTRIBUTION/i.test(para);
47
- const hasPercent = /\d+\.?\d*%/.test(para);
48
- const hasCpaKeyword = /\bCPA\b|actuar|implement|deadline|establish|file|form\s+\d|verify|engage|model|recommend/i.test(para);
49
- if ((hasDollar || hasFormula || hasPercent) && !hasCpaKeyword) {
50
- savingsParas.push(para);
51
- }
52
- else if (savingsParas.length === 0) {
53
- // First para with dollar amounts seeds savings section even if it has CPA keywords
54
- if (hasDollar || hasFormula)
55
- savingsParas.push(para);
56
- else
57
- cpaParas.push(para);
58
- }
59
- else {
60
- cpaParas.push(para);
61
- }
41
+ // Try splitting on double-newline first (well-structured LLM output)
42
+ const paraBreak = text.indexOf("\n\n");
43
+ if (paraBreak !== -1) {
44
+ return {
45
+ savingsSection: text.slice(0, paraBreak).trim(),
46
+ cpaSection: text.slice(paraBreak).trim(),
47
+ };
62
48
  }
63
- // If everything ended up in one bucket, put first para in savings, rest in cpa
64
- if (!savingsParas.length && cpaParas.length) {
65
- savingsParas.push(cpaParas.shift());
49
+ // Single-paragraph: find the first implementation boundary
50
+ const splitPatterns = [
51
+ /\s+Implementation:\s+/i,
52
+ /\s+CPA should\s+/i,
53
+ /\s+CPA must\s+/i,
54
+ /\s+To implement:\s+/i,
55
+ /\s+\(1\)\s+/, // numbered list start
56
+ ];
57
+ for (const pattern of splitPatterns) {
58
+ const match = text.search(pattern);
59
+ if (match > 80) { // only split if there's meaningful content before it
60
+ return {
61
+ savingsSection: text.slice(0, match).trim(),
62
+ cpaSection: text.slice(match).trim(),
63
+ };
64
+ }
66
65
  }
67
- return {
68
- savingsSection: savingsParas.join("\n\n"),
69
- cpaSection: cpaParas.join("\n\n"),
70
- };
66
+ // No clean split point — show everything as savings breakdown, nothing as CPA notes
67
+ return { savingsSection: text, cpaSection: "" };
71
68
  }
72
69
  function SourceDocRows({ docs, trace }) {
73
70
  if (docs && docs.length > 0) {
@@ -26,7 +26,9 @@ 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");
31
+ const useEngineOutput_1 = require("../../lib/adapters/useEngineOutput");
30
32
  const TaxAxisButton_1 = require("../shared/TaxAxisButton");
31
33
  const TaxAxisBadge_1 = require("../shared/TaxAxisBadge");
32
34
  const DashboardSummary_1 = require("./DashboardSummary");
@@ -96,18 +98,109 @@ function buildRoadmapBucketLookup(llm) {
96
98
  }
97
99
  return map;
98
100
  }
99
- 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) {
100
152
  const nameLookup = buildEngineNameLookup(llm);
101
153
  const bucketLookup = buildRoadmapBucketLookup(llm);
154
+ const clientSummaryLookup = buildClientSummaryLookup(llm);
102
155
  return llm.strategies
103
156
  .filter((s) => s.applicable)
104
157
  .map((s, idx) => {
105
- var _a, _b, _c, _d, _e;
158
+ var _a, _b, _c, _d, _e, _f, _g;
106
159
  const id = (_a = s.strategyId) !== null && _a !== void 0 ? _a : s.strategyType;
107
- // Prefer the real strategy_name from engineOutput, fall back to humanized strategyType
108
160
  const name = (_b = nameLookup.get(id)) !== null && _b !== void 0 ? _b : humanizeStrategyType(s.strategyType);
109
161
  const timelineBucket = (_c = bucketLookup.get(id)) !== null && _c !== void 0 ? _c : (s.priority === "HIGH" ? "now" : "90d");
110
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;
111
204
  return {
112
205
  rank: idx + 1,
113
206
  code: id,
@@ -120,13 +213,18 @@ function mapLlmToStrategies(llm) {
120
213
  hi: s.estimatedSavings.max,
121
214
  timeline: timelineBucket === "now" ? "Act now" : timelineBucket === "30d" ? "Within 30 days" : "Within 90 days",
122
215
  timelineBucket,
123
- clientBrief: s.summary,
124
- action: (_e = s.implementationSteps[0]) !== null && _e !== void 0 ? _e : s.summary,
125
- forms: s.requiredForms.join(", "),
216
+ clientBrief,
217
+ action,
218
+ forms: formsFromTrace !== null && formsFromTrace !== void 0 ? formsFromTrace : s.requiredForms.join(", "),
126
219
  abstract: s.summary,
127
220
  sources: [],
128
221
  trace: [],
129
222
  cost: undefined,
223
+ sourceDocuments,
224
+ specialistNote,
225
+ positionStrength,
226
+ authority,
227
+ quickWin: s.quickWin,
130
228
  };
131
229
  });
132
230
  }
@@ -163,8 +261,29 @@ function TaxAxisDashboard({ profile, llmResult, parsedDocuments, onDownloadClien
163
261
  // Extracted documents: live from session if provided, otherwise empty (no mock fallback)
164
262
  const extractionDocs = parsedDocuments !== null && parsedDocuments !== void 0 ? parsedDocuments : [];
165
263
  // ─── Derived data ──────────────────────────────────────────────
166
- const llmStrategies = (0, react_1.useMemo)(() => (hasLlm ? mapLlmToStrategies(llmResult) : []), [hasLlm, llmResult]);
167
- const llmComputed = (0, react_1.useMemo)(() => (hasLlm ? buildLlmComputed(llmStrategies) : new Map()), [hasLlm, llmStrategies]);
264
+ // Prefer useEngineOutput (full rich data: sourceDocuments, positionStrength,
265
+ // clientBrief from why_it_applies, etc.) over the thin mapLlmToStrategies fallback.
266
+ // engineOutput is at llmResult.engineOutput or llmResult.rawOutput (both present).
267
+ const engineOutputForAdapter = (0, react_1.useMemo)(() => { var _a, _b; return (_b = (_a = llmResult === null || llmResult === void 0 ? void 0 : llmResult.engineOutput) !== null && _a !== void 0 ? _a : llmResult === null || llmResult === void 0 ? void 0 : llmResult.rawOutput) !== null && _b !== void 0 ? _b : null; }, [llmResult]);
268
+ const adapted = (0, useEngineOutput_1.useEngineOutput)(engineOutputForAdapter);
269
+ const llmStrategies = (0, react_1.useMemo)(() => {
270
+ var _a;
271
+ if (!hasLlm)
272
+ return [];
273
+ // Use adapted strategies when available — they carry the full engine data.
274
+ if ((_a = adapted === null || adapted === void 0 ? void 0 : adapted.strategies) === null || _a === void 0 ? void 0 : _a.length)
275
+ return adapted.strategies;
276
+ // Fallback to thin mapping for old payload shapes without engineOutput.
277
+ return mapLlmToStrategies(llmResult, profile);
278
+ }, [hasLlm, adapted, llmResult, profile]);
279
+ const llmComputed = (0, react_1.useMemo)(() => {
280
+ var _a;
281
+ if (!hasLlm)
282
+ return new Map();
283
+ if ((_a = adapted === null || adapted === void 0 ? void 0 : adapted.computedMap) === null || _a === void 0 ? void 0 : _a.size)
284
+ return adapted.computedMap;
285
+ return buildLlmComputed(llmStrategies);
286
+ }, [hasLlm, adapted, llmStrategies]);
168
287
  const computed = (0, react_1.useMemo)(() => (hasLlm ? llmComputed : (0, compute_1.computeAllStrategies)(profile)), [hasLlm, llmComputed, profile]);
169
288
  const dashEligible = (0, react_1.useMemo)(() => (hasLlm ? llmStrategies : (0, compute_1.filterEligibleStrategies)(profile)), [hasLlm, llmStrategies, profile]);
170
289
  const maxSavings = Math.max(...dashEligible.map((s) => { var _a, _b; return (_b = (_a = computed.get(s.rank)) === null || _a === void 0 ? void 0 : _a.hi) !== null && _b !== void 0 ? _b : s.hi; }), 1);
@@ -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.55",
3
+ "version": "1.14.57",
4
4
  "description": "",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {