@f5xc-salesdemos/xcsh 18.68.0 → 18.69.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5xc-salesdemos/xcsh",
4
- "version": "18.68.0",
4
+ "version": "18.69.0",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/f5xc-salesdemos/xcsh",
7
7
  "author": "Can Boluk",
@@ -48,12 +48,12 @@
48
48
  "dependencies": {
49
49
  "@agentclientprotocol/sdk": "0.16.1",
50
50
  "@mozilla/readability": "^0.6",
51
- "@f5xc-salesdemos/xcsh-stats": "18.68.0",
52
- "@f5xc-salesdemos/pi-agent-core": "18.68.0",
53
- "@f5xc-salesdemos/pi-ai": "18.68.0",
54
- "@f5xc-salesdemos/pi-natives": "18.68.0",
55
- "@f5xc-salesdemos/pi-tui": "18.68.0",
56
- "@f5xc-salesdemos/pi-utils": "18.68.0",
51
+ "@f5xc-salesdemos/xcsh-stats": "18.69.0",
52
+ "@f5xc-salesdemos/pi-agent-core": "18.69.0",
53
+ "@f5xc-salesdemos/pi-ai": "18.69.0",
54
+ "@f5xc-salesdemos/pi-natives": "18.69.0",
55
+ "@f5xc-salesdemos/pi-tui": "18.69.0",
56
+ "@f5xc-salesdemos/pi-utils": "18.69.0",
57
57
  "@sinclair/typebox": "^0.34",
58
58
  "@xterm/headless": "^6.0",
59
59
  "ajv": "^8.18",
@@ -17,17 +17,17 @@ export interface BuildInfo {
17
17
  }
18
18
 
19
19
  export const BUILD_INFO: BuildInfo = {
20
- "version": "18.68.0",
21
- "commit": "27ddc863792ce7bba1ab75066abeca95cafca4d4",
22
- "shortCommit": "27ddc86",
20
+ "version": "18.69.0",
21
+ "commit": "309ca5b1b28a5530f01b83381e69f8a6c94f83c8",
22
+ "shortCommit": "309ca5b",
23
23
  "branch": "main",
24
- "tag": "v18.68.0",
25
- "commitDate": "2026-05-18T17:51:04Z",
26
- "buildDate": "2026-05-18T18:22:44.227Z",
24
+ "tag": "v18.69.0",
25
+ "commitDate": "2026-05-18T23:00:56Z",
26
+ "buildDate": "2026-05-18T23:27:04.429Z",
27
27
  "dirty": false,
28
28
  "prNumber": "",
29
29
  "repoUrl": "https://github.com/f5xc-salesdemos/xcsh",
30
30
  "repoSlug": "f5xc-salesdemos/xcsh",
31
- "commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/27ddc863792ce7bba1ab75066abeca95cafca4d4",
32
- "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.68.0"
31
+ "commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/309ca5b1b28a5530f01b83381e69f8a6c94f83c8",
32
+ "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.69.0"
33
33
  };
@@ -0,0 +1,405 @@
1
+ /**
2
+ * pipeline-report/benchmark.ts
3
+ *
4
+ * Autoresearch benchmark harness for the pipeline report.
5
+ * Generates a real pipeline report against the live Salesforce org
6
+ * and scores it deterministically across 6 quality dimensions.
7
+ *
8
+ * Primary metric: report_quality_score (0–100, higher is better)
9
+ *
10
+ * OFF LIMITS to autoresearch modifications — this is the ground-truth evaluator.
11
+ */
12
+
13
+ import { loadSalesforceContext } from "../internal-urls/salesforce-context";
14
+ import { loadProfile } from "../internal-urls/user-profile";
15
+ import { generatePipelineReport } from "./generator";
16
+ import { renderPipelineReport } from "./renderer";
17
+ import type { PipelineReportData } from "./types";
18
+
19
+ // ─── Fiscal quarter helpers ───────────────────────────────────────────
20
+ // F5 fiscal year starts November 1. Quarters:
21
+ // Q1: Nov–Jan Q2: Feb–Apr Q3: May–Jul Q4: Aug–Oct
22
+
23
+ function fiscalQuarterDates(): { start: string; end: string } {
24
+ const now = new Date();
25
+ const m = now.getMonth(); // 0-indexed
26
+ const y = now.getFullYear();
27
+
28
+ let start: Date;
29
+ let end: Date;
30
+
31
+ if (m >= 10) {
32
+ // Nov–Dec → Q1 starts this year Nov, ends Jan next year
33
+ start = new Date(y, 10, 1);
34
+ end = new Date(y + 1, 1, 0);
35
+ } else if (m === 0) {
36
+ // Jan → still Q1 (Nov prev year – Jan this year)
37
+ start = new Date(y - 1, 10, 1);
38
+ end = new Date(y, 1, 0);
39
+ } else if (m <= 3) {
40
+ // Feb–Apr → Q2
41
+ start = new Date(y, 1, 1);
42
+ end = new Date(y, 4, 0);
43
+ } else if (m <= 6) {
44
+ // May–Jul → Q3
45
+ start = new Date(y, 4, 1);
46
+ end = new Date(y, 7, 0);
47
+ } else {
48
+ // Aug–Oct → Q4
49
+ start = new Date(y, 7, 1);
50
+ end = new Date(y, 10, 0);
51
+ }
52
+
53
+ const fmt = (d: Date) => d.toISOString().split("T")[0]!;
54
+ return { start: fmt(start), end: fmt(end) };
55
+ }
56
+
57
+ // ─── Scoring helpers ──────────────────────────────────────────────────
58
+
59
+ interface DimScore {
60
+ score: number;
61
+ max: number;
62
+ notes: string[];
63
+ }
64
+
65
+ function scoreSectionPresence(report: string): DimScore {
66
+ const checks: Array<{ re: RegExp; label: string; pts: number }> = [
67
+ { re: /^# F5 Distributed Cloud Pipeline Report/m, label: "report header", pts: 2 },
68
+ { re: /\*\*Generated:\*\*/m, label: "generated timestamp", pts: 2 },
69
+ { re: /\*\*Quarter:\*\*/m, label: "quarter range", pts: 2 },
70
+ { re: /\*\*Summary:\*\*/m, label: "executive summary", pts: 3 },
71
+ { re: /Closed.*Booked/m, label: "booked section", pts: 3 },
72
+ { re: /Open Pipeline.*Net New/m, label: "net new section", pts: 3 },
73
+ { re: /Open Pipeline.*Renewals/m, label: "renewals section", pts: 3 },
74
+ { re: /Forecast Summary/m, label: "forecast summary", pts: 3 },
75
+ { re: /\*\*Line items:\*\*/m, label: "line item count", pts: 1 },
76
+ { re: /\*\*SKUs:\*\*/m, label: "SKU count", pts: 1 },
77
+ { re: /\*\*Model:\*\*/m, label: "value model description", pts: 1 },
78
+ ];
79
+ let score = 0;
80
+ let max = 0;
81
+ const notes: string[] = [];
82
+ for (const c of checks) {
83
+ max += c.pts;
84
+ if (c.re.test(report)) {
85
+ score += c.pts;
86
+ } else {
87
+ notes.push(`Missing: ${c.label}`);
88
+ }
89
+ }
90
+ return { score, max, notes };
91
+ }
92
+
93
+ function scoreDollarFormatting(report: string): DimScore {
94
+ const notes: string[] = [];
95
+ let score = 0;
96
+ const max = 20;
97
+
98
+ // Compact dollar amounts in summaries ($1.2M, $45K)
99
+ if (/\$[\d,.]+[MK]\b/.test(report)) {
100
+ score += 5;
101
+ } else {
102
+ notes.push("No compact dollar amounts ($XM/$XK) in summary lines");
103
+ }
104
+
105
+ // Precise dollar amounts in tables (1,234.56)
106
+ if (/[\d,]+\.\d{2}/.test(report)) {
107
+ score += 5;
108
+ } else {
109
+ notes.push("No precise dollar amounts (X,XXX.XX) in tables");
110
+ }
111
+
112
+ // Em-dash (—) for zero values instead of literal $0
113
+ if (/\|\s*—\s*\|/.test(report)) {
114
+ score += 3;
115
+ }
116
+
117
+ // No unformatted large numbers in table context
118
+ const rawLarge = [...(report.matchAll(/(?<!\$)(?<!\d[,.])\b\d{5,}\b(?!\.\d{2})(?![MK%])/g) ?? [])].filter(m => {
119
+ const ctx = report.slice(Math.max(0, m.index! - 20), m.index! + m[0].length + 20);
120
+ return ctx.includes("|");
121
+ });
122
+ if (rawLarge.length === 0) {
123
+ score += 4;
124
+ } else {
125
+ notes.push(`${rawLarge.length} unformatted large number(s) in table context`);
126
+ }
127
+
128
+ // No undefined/null/NaN in output
129
+ const badValues = (report.match(/\bundefined\b|\bnull\b|\bNaN\b/g) ?? []).length;
130
+ if (badValues === 0) {
131
+ score += 3;
132
+ } else {
133
+ notes.push(`${badValues} occurrence(s) of undefined/null/NaN in output`);
134
+ }
135
+
136
+ return { score, max, notes };
137
+ }
138
+
139
+ function scoreDateFormatting(report: string): DimScore {
140
+ const notes: string[] = [];
141
+ let score = 0;
142
+ const max = 10;
143
+
144
+ // Generated timestamp: human-readable, not raw ISO
145
+ const generatedMatch = report.match(/\*\*Generated:\*\*\s*(.+)/);
146
+ if (generatedMatch) {
147
+ const d = generatedMatch[1]!.trim();
148
+ if (/^\d{4}-\d{2}-\d{2}T/.test(d)) {
149
+ notes.push("Generated date uses raw ISO 8601 (should be locale string)");
150
+ } else {
151
+ score += 3;
152
+ }
153
+ }
154
+
155
+ // Quarter line present and reasonably readable
156
+ const quarterMatch = report.match(/\*\*Quarter:\*\*\s*(.+)/);
157
+ if (quarterMatch) {
158
+ score += quarterMatch[1]!.includes("-") ? 3 : 4;
159
+ }
160
+
161
+ // No raw ISO timestamps (2026-05-18T...) in the report body
162
+ const rawTs = (report.match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/g) ?? []).length;
163
+ if (rawTs === 0) {
164
+ score += 3;
165
+ } else {
166
+ notes.push(`${rawTs} raw ISO timestamp(s) in report body`);
167
+ }
168
+
169
+ return { score, max, notes };
170
+ }
171
+
172
+ function scoreTableQuality(report: string): DimScore {
173
+ const notes: string[] = [];
174
+ let score = 0;
175
+ const max = 15;
176
+
177
+ const tables = report.match(/\|.+\|[\n\r]+\|[-:|]+\|/g) ?? [];
178
+ if (tables.length === 0) {
179
+ notes.push("No properly formatted markdown tables found");
180
+ return { score: 0, max, notes };
181
+ }
182
+ score += 3;
183
+
184
+ // All tables have separator rows (already implied by regex, this checks alignment chars)
185
+ const hasRightAlign = /---:\|/.test(report);
186
+ if (hasRightAlign) {
187
+ score += 3;
188
+ } else {
189
+ notes.push("No right-aligned columns (amounts should be right-aligned with ---:|)");
190
+ }
191
+
192
+ // Tables have left-aligned label columns
193
+ if (/:---\|/.test(report)) {
194
+ score += 3;
195
+ }
196
+
197
+ // Territory grouping separators (bold em-dash rows)
198
+ if (/\*\*.*—.*\*\*/.test(report)) {
199
+ score += 3;
200
+ }
201
+
202
+ // Total rows (bold)
203
+ if (/\|\s*\*\*Total\*\*\s*\|/.test(report)) {
204
+ score += 3;
205
+ } else {
206
+ notes.push("No bold Total rows in tables");
207
+ }
208
+
209
+ return { score, max, notes };
210
+ }
211
+
212
+ function scoreDataDensity(data: PipelineReportData, report: string): DimScore {
213
+ const notes: string[] = [];
214
+ let score = 0;
215
+ const max = 20;
216
+
217
+ if (data.lineItemCount > 0) {
218
+ score += 4;
219
+ } else {
220
+ notes.push("Zero line items processed");
221
+ }
222
+
223
+ const totalAccounts = data.netNew.accounts.length + data.booked.accounts.length + data.renewals.accounts.length;
224
+ if (totalAccounts > 0) {
225
+ score += 4;
226
+ } else {
227
+ notes.push("No accounts in any section");
228
+ }
229
+
230
+ const fc = data.forecast;
231
+ const fcCount = [fc.commit, fc.bestCase, fc.pipeline].filter(v => v > 0).length;
232
+ score += Math.min(5, fcCount * 2);
233
+ if (fcCount === 0) {
234
+ notes.push("No forecast category amounts populated");
235
+ }
236
+
237
+ if (data.skusFound.length > 0) {
238
+ score += 3;
239
+ } else {
240
+ notes.push("No SKUs discovered");
241
+ }
242
+
243
+ // Anomaly detection running (even 0 anomalies means the engine ran)
244
+ score += 2;
245
+ if (data.anomalies.length > 0 && /Data Quality/.test(report)) {
246
+ score += 2;
247
+ }
248
+
249
+ return { score, max, notes };
250
+ }
251
+
252
+ function scoreInformationValue(report: string): DimScore {
253
+ const notes: string[] = [];
254
+ let score = 0;
255
+ const max = 10;
256
+
257
+ // Quota total callouts (actionable for sales professionals)
258
+ if (/Quota Total/i.test(report)) {
259
+ score += 3;
260
+ } else {
261
+ notes.push("No quota total callouts");
262
+ }
263
+
264
+ // Forecast category breakdown visible (Commit, Best Case, Pipeline)
265
+ if (/Commit/m.test(report) && /Best Case/m.test(report)) {
266
+ score += 2;
267
+ }
268
+
269
+ // Platform vs Point product split (critical for overlay SE reporting)
270
+ if (/Platform.*Distributed Cloud/m.test(report) || /Point.*Shape/m.test(report)) {
271
+ score += 3;
272
+ } else {
273
+ notes.push("Platform/Point product split not visible");
274
+ }
275
+
276
+ // Territory context present
277
+ if (/Territories:/m.test(report) || /Territory/m.test(report)) {
278
+ score += 2;
279
+ }
280
+
281
+ return { score, max, notes };
282
+ }
283
+
284
+ // ─── Main ─────────────────────────────────────────────────────────────
285
+
286
+ const profile = await loadProfile();
287
+ const sfContext = await loadSalesforceContext();
288
+
289
+ if (!profile.identifiers?.salesforceId) {
290
+ console.error("ERROR: No salesforceId in user profile. Run: xcsh://salesforce?refresh=true");
291
+ process.exit(1);
292
+ }
293
+
294
+ if (!sfContext) {
295
+ console.error("ERROR: No cached Salesforce context. Run: xcsh://salesforce?refresh=true");
296
+ process.exit(1);
297
+ }
298
+
299
+ const userId = profile.identifiers.salesforceId;
300
+ const partnerId = profile.partner?.id;
301
+ const userIds = partnerId ? [userId, partnerId] : [userId];
302
+ const { start, end } = fiscalQuarterDates();
303
+
304
+ // Stale cutoff: 12 months ago to include non-stale pipeline beyond current quarter
305
+ const staleDate = new Date();
306
+ staleDate.setFullYear(staleDate.getFullYear() - 1);
307
+ const staleCutoff = staleDate.toISOString().split("T")[0]!;
308
+
309
+ const partnerName = profile.partner?.name;
310
+ const selfName = [profile.givenName, profile.familyName].filter(Boolean).join(" ").trim();
311
+ const teamMemberNames = partnerName && selfName ? [selfName, partnerName] : undefined;
312
+
313
+ const options = {
314
+ userIds,
315
+ orgAlias: sfContext.orgAlias,
316
+ quarterStart: start,
317
+ quarterEnd: end,
318
+ staleCutoff,
319
+ confirmedTerritories: profile.territories ?? sfContext.confirmedTerritories ?? sfContext.territories,
320
+ teamMemberNames,
321
+ };
322
+
323
+ // ─── Generate and time ────────────────────────────────────────────────
324
+
325
+ const genStart = performance.now();
326
+ const data = await generatePipelineReport(options);
327
+ const generateMs = Math.round(performance.now() - genStart);
328
+
329
+ const renderStart = performance.now();
330
+ const report = renderPipelineReport(data, sfContext.instanceUrl);
331
+ const renderMs = Math.round(performance.now() - renderStart);
332
+
333
+ const totalMs = generateMs + renderMs;
334
+
335
+ // ─── Score ────────────────────────────────────────────────────────────
336
+
337
+ const sections = scoreSectionPresence(report);
338
+ const dollars = scoreDollarFormatting(report);
339
+ const dates = scoreDateFormatting(report);
340
+ const tables = scoreTableQuality(report);
341
+ const density = scoreDataDensity(data, report);
342
+ const infoVal = scoreInformationValue(report);
343
+
344
+ const rawScore = sections.score + dollars.score + dates.score + tables.score + density.score + infoVal.score;
345
+ const rawMax = sections.max + dollars.max + dates.max + tables.max + density.max + infoVal.max;
346
+ const qualityScore = Math.round((rawScore / rawMax) * 100);
347
+
348
+ const dataCompletenessPct = Math.round(
349
+ ([
350
+ data.generated,
351
+ data.quarter.start,
352
+ data.quarter.end,
353
+ data.lineItemCount > 0,
354
+ data.skusFound.length > 0,
355
+ data.netNew.accounts.length > 0 || data.booked.accounts.length > 0,
356
+ data.forecast.commit > 0 || data.forecast.bestCase > 0 || data.forecast.pipeline > 0,
357
+ data.territories.length > 0,
358
+ ].filter(Boolean).length /
359
+ 8) *
360
+ 100,
361
+ );
362
+
363
+ // Baseline generator makes 3 queries: net new + booked (parallel) + renewals (sequential)
364
+ const queryCount = 3;
365
+
366
+ const totalAccounts = data.netNew.accounts.length + data.booked.accounts.length + data.renewals.accounts.length;
367
+
368
+ // ─── Output METRIC lines ──────────────────────────────────────────────
369
+
370
+ console.log(`METRIC report_quality_score=${qualityScore}`);
371
+ console.log(`METRIC query_count=${queryCount}`);
372
+ console.log(`METRIC total_time_ms=${totalMs}`);
373
+ console.log(`METRIC generate_time_ms=${generateMs}`);
374
+ console.log(`METRIC render_time_ms=${renderMs}`);
375
+ console.log(`METRIC data_completeness_pct=${dataCompletenessPct}`);
376
+ console.log(`METRIC section_score=${sections.score}`);
377
+ console.log(`METRIC dollar_format_score=${dollars.score}`);
378
+ console.log(`METRIC date_format_score=${dates.score}`);
379
+ console.log(`METRIC table_alignment_score=${tables.score}`);
380
+ console.log(`METRIC data_density_score=${density.score}`);
381
+ console.log(`METRIC info_value_score=${infoVal.score}`);
382
+ console.log(`METRIC line_item_count=${data.lineItemCount}`);
383
+ console.log(`METRIC account_count=${totalAccounts}`);
384
+ console.log(`METRIC report_length=${report.length}`);
385
+ console.log(
386
+ `ASI scoring_breakdown={"sections":${sections.score}/${sections.max},"dollars":${dollars.score}/${dollars.max},"dates":${dates.score}/${dates.max},"tables":${tables.score}/${tables.max},"density":${density.score}/${density.max},"infoValue":${infoVal.score}/${infoVal.max},"total":${rawScore}/${rawMax}}`,
387
+ );
388
+
389
+ const allNotes = [
390
+ ...sections.notes.map(n => `[sections] ${n}`),
391
+ ...dollars.notes.map(n => `[dollars] ${n}`),
392
+ ...dates.notes.map(n => `[dates] ${n}`),
393
+ ...tables.notes.map(n => `[tables] ${n}`),
394
+ ...density.notes.map(n => `[density] ${n}`),
395
+ ...infoVal.notes.map(n => `[info-value] ${n}`),
396
+ ];
397
+
398
+ if (allNotes.length > 0) {
399
+ console.log(`ASI deduction_notes=${JSON.stringify(allNotes)}`);
400
+ console.log("");
401
+ console.log("Deduction notes:");
402
+ for (const note of allNotes) {
403
+ console.log(` - ${note}`);
404
+ }
405
+ }