@agent-native/core 0.98.2 → 0.98.4

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.
Files changed (78) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +23 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/production-agent.ts +2 -2
  5. package/corpus/core/src/client/analytics.ts +12 -0
  6. package/corpus/core/src/client/index.ts +2 -0
  7. package/corpus/core/src/client/session-replay.ts +612 -26
  8. package/corpus/core/src/client/settings/DemoModeSection.tsx +6 -6
  9. package/corpus/core/src/client/settings/SettingsPanel.tsx +31 -211
  10. package/corpus/core/src/client/settings/agent-settings-search.ts +219 -0
  11. package/corpus/core/src/client/settings/index.ts +4 -0
  12. package/corpus/core/src/demo/actions/toggle-demo-mode.ts +2 -2
  13. package/corpus/core/src/demo/config.ts +6 -6
  14. package/corpus/core/src/demo/fetch-interceptor.ts +25 -2
  15. package/corpus/core/src/demo/redact.ts +57 -66
  16. package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +65 -6
  17. package/corpus/templates/analytics/AGENTS.md +6 -6
  18. package/corpus/templates/analytics/app/components/layout/CommandPalette.tsx +429 -244
  19. package/corpus/templates/analytics/app/components/layout/command-palette-search.ts +93 -0
  20. package/corpus/templates/analytics/app/global.css +4 -62
  21. package/corpus/templates/analytics/app/hooks/use-replay-storage-status.ts +2 -1
  22. package/corpus/templates/analytics/app/lib/demo-chart-trend.ts +46 -33
  23. package/corpus/templates/analytics/app/pages/Settings.tsx +8 -45
  24. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +413 -105
  25. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +32 -16
  26. package/corpus/templates/analytics/app/pages/settings/settings-search.ts +126 -0
  27. package/corpus/templates/analytics/changelog/2026-07-12-command-menu-loading-placeholders-now-stay-below-available-r.md +6 -0
  28. package/corpus/templates/analytics/changelog/2026-07-12-command-menu-search-now-prioritizes-the-best-match.md +6 -0
  29. package/corpus/templates/analytics/changelog/2026-07-12-demo-charts-preserve-source-movement.md +6 -0
  30. package/corpus/templates/analytics/changelog/2026-07-12-fixed-session-replays-that-appeared-ultra-wide-hid-recorded-.md +6 -0
  31. package/corpus/templates/analytics/changelog/2026-07-12-replay-storage-settings-stay-readable-at-narrow-widths.md +6 -0
  32. package/corpus/templates/analytics/changelog/2026-07-12-session-replays-load-faster-in-demo-mode-while-visitor-email.md +6 -0
  33. package/corpus/templates/analytics/changelog/2026-07-12-session-replays-preserve-recorded-styling-and-keep-malformed.md +6 -0
  34. package/corpus/templates/calendar/app/components/calendar/DeleteEventDialog.tsx +11 -2
  35. package/corpus/templates/calendar/app/components/calendar/EventAttendeesSection.tsx +14 -2
  36. package/corpus/templates/calendar/changelog/2026-07-12-calendar-responses-can-now-be-saved-with-command-enter-or-ct.md +6 -0
  37. package/dist/agent/production-agent.js +2 -2
  38. package/dist/agent/production-agent.js.map +1 -1
  39. package/dist/client/analytics.d.ts.map +1 -1
  40. package/dist/client/analytics.js +11 -0
  41. package/dist/client/analytics.js.map +1 -1
  42. package/dist/client/index.d.ts +1 -1
  43. package/dist/client/index.d.ts.map +1 -1
  44. package/dist/client/index.js +1 -1
  45. package/dist/client/index.js.map +1 -1
  46. package/dist/client/session-replay.d.ts +12 -3
  47. package/dist/client/session-replay.d.ts.map +1 -1
  48. package/dist/client/session-replay.js +488 -23
  49. package/dist/client/session-replay.js.map +1 -1
  50. package/dist/client/settings/DemoModeSection.d.ts +3 -3
  51. package/dist/client/settings/DemoModeSection.js +4 -4
  52. package/dist/client/settings/DemoModeSection.js.map +1 -1
  53. package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
  54. package/dist/client/settings/SettingsPanel.js +41 -191
  55. package/dist/client/settings/SettingsPanel.js.map +1 -1
  56. package/dist/client/settings/agent-settings-search.d.ts +16 -0
  57. package/dist/client/settings/agent-settings-search.d.ts.map +1 -0
  58. package/dist/client/settings/agent-settings-search.js +178 -0
  59. package/dist/client/settings/agent-settings-search.js.map +1 -0
  60. package/dist/client/settings/index.d.ts +1 -0
  61. package/dist/client/settings/index.d.ts.map +1 -1
  62. package/dist/client/settings/index.js +1 -0
  63. package/dist/client/settings/index.js.map +1 -1
  64. package/dist/collab/routes.d.ts +1 -1
  65. package/dist/demo/actions/toggle-demo-mode.js +2 -2
  66. package/dist/demo/actions/toggle-demo-mode.js.map +1 -1
  67. package/dist/demo/config.js +6 -6
  68. package/dist/demo/config.js.map +1 -1
  69. package/dist/demo/fetch-interceptor.d.ts +1 -0
  70. package/dist/demo/fetch-interceptor.d.ts.map +1 -1
  71. package/dist/demo/fetch-interceptor.js +19 -2
  72. package/dist/demo/fetch-interceptor.js.map +1 -1
  73. package/dist/demo/redact.d.ts +14 -5
  74. package/dist/demo/redact.d.ts.map +1 -1
  75. package/dist/demo/redact.js +30 -56
  76. package/dist/demo/redact.js.map +1 -1
  77. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  78. package/package.json +2 -2
@@ -1,17 +1,26 @@
1
1
  /**
2
2
  * Pure, dependency-free, deterministic demo-mode redactor.
3
3
  *
4
- * Replaces sensitive values (names, emails, and free numbers) with stable fake
4
+ * Replaces sensitive values (emails and free numbers) with stable fake
5
5
  * substitutes so a demo looks coherent — the same input always maps to the
6
6
  * same fake. Crucially, it NEVER rewrites identifiers, structural tokens, or
7
- * timestamps: under-redaction is safe, corrupting an ID is not. The string
8
- * redactor uses a protect-first strategy (mask IDs with opaque placeholders
9
- * before any transform runs, restore them byte-identical afterwards), and the
10
- * structure-aware walker additionally protects leaf values by key name.
7
+ * timestamps. Names and other free text are deliberately left alone because
8
+ * guessing whether arbitrary text is a person's name is too inaccurate. The
9
+ * string redactor uses a protect-first strategy (mask IDs with opaque
10
+ * placeholders before any transform runs, restore them byte-identical
11
+ * afterwards), and the structure-aware walker additionally protects leaf
12
+ * values by key name.
11
13
  */
12
14
 
13
15
  export interface RedactOptions {
14
16
  salt?: string;
17
+ /** Redact numeric values. Defaults to true for backward compatibility. */
18
+ redactNumbers?: boolean;
19
+ /**
20
+ * Redact emails stored in otherwise protected structural fields such as
21
+ * `userId` and `userKey`. Intended for display-only frontend responses.
22
+ */
23
+ redactProtectedEmails?: boolean;
15
24
  }
16
25
 
17
26
  /* ------------------------------------------------------------------ *
@@ -66,7 +75,7 @@ function pick<T>(rng: () => number, pool: readonly T[]): T {
66
75
  * round-trips back through redaction (e.g. you edit a draft that's
67
76
  * already showing fake text, it autosaves, then refetches) it is passed
68
77
  * through UNCHANGED instead of being re-faked into something new. This
69
- * is what stops names/emails drifting on every edit.
78
+ * is what stops emails drifting on every edit.
70
79
  *
71
80
  * Leak-free by construction: no timers, a hard size cap, and lazy TTL purge
72
81
  * on write. Works the same per-tab in the browser and per-process on the
@@ -242,15 +251,6 @@ const LAST_NAMES: readonly string[] = [
242
251
  * Fake value generators (deterministic in value + salt)
243
252
  * ------------------------------------------------------------------ */
244
253
 
245
- function fakeName(original: string, salt: string): string {
246
- return memoFake("name", original, salt, () => {
247
- const rng = seededRng(`name:${original}`, salt);
248
- const first = pick(rng, FIRST_NAMES);
249
- const last = pick(rng, LAST_NAMES);
250
- return `${first} ${last}`;
251
- });
252
- }
253
-
254
254
  // Realistic-looking but safe stand-in domains: real consumer providers (the
255
255
  // local part is fake, so no real mailbox is implied) plus the canonical
256
256
  // reserved fictional-company domains (Contoso/Fabrikam/Northwind), which read
@@ -484,10 +484,6 @@ function unprotect(text: string, restore: Map<string, string>): string {
484
484
 
485
485
  const EMAIL_RE = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g;
486
486
 
487
- // 2+ capitalized words (each starts uppercase, allows internal lowercase /
488
- // apostrophes / hyphens). Single-letter middle initials allowed: `Sarah J Connor`.
489
- const FULL_NAME_RE = /\b[A-Z][a-zA-Z'’-]*(?:\s+[A-Z](?:[a-zA-Z'’-]*)?){1,3}\b/g;
490
-
491
487
  // A standalone numeric token: optional currency + sign, digits with optional
492
488
  // comma grouping and a single optional decimal part. Bounded so it is NOT
493
489
  // adjacent to a letter (placeholders already removed letter-mixed tokens, but
@@ -499,10 +495,6 @@ function transformEmails(text: string, salt: string): string {
499
495
  return text.replace(EMAIL_RE, (match) => fakeEmail(match, salt));
500
496
  }
501
497
 
502
- function transformNames(text: string, salt: string): string {
503
- return text.replace(FULL_NAME_RE, (match) => fakeName(match, salt));
504
- }
505
-
506
498
  function isProbablyYear(digits: string): boolean {
507
499
  if (digits.length !== 4) return false;
508
500
  const n = Number(digits);
@@ -550,20 +542,26 @@ function transformNumbers(text: string, salt: string): string {
550
542
  function redactDemoStringInternal(
551
543
  text: string,
552
544
  salt: string,
553
- redactNames: boolean,
545
+ redactNumbers: boolean,
554
546
  ): string {
555
547
  if (typeof text !== "string" || text.length === 0) return text;
548
+ if (!text.includes("@") && (!redactNumbers || !/[\d$€£¥]/.test(text))) {
549
+ return text;
550
+ }
556
551
 
557
552
  const { text: masked, restore } = protect(text);
558
553
  let out = masked;
559
554
  out = transformEmails(out, salt);
560
- if (redactNames) out = transformNames(out, salt);
561
- out = transformNumbers(out, salt);
555
+ if (redactNumbers) out = transformNumbers(out, salt);
562
556
  return unprotect(out, restore);
563
557
  }
564
558
 
565
559
  export function redactDemoString(text: string, opts?: RedactOptions): string {
566
- return redactDemoStringInternal(text, opts?.salt ?? "", true);
560
+ return redactDemoStringInternal(
561
+ text,
562
+ opts?.salt ?? "",
563
+ opts?.redactNumbers !== false,
564
+ );
567
565
  }
568
566
 
569
567
  /* ------------------------------------------------------------------ *
@@ -573,35 +571,14 @@ export function redactDemoString(text: string, opts?: RedactOptions): string {
573
571
  // Keys whose leaf values must NEVER be transformed (still recurse into nested
574
572
  // objects/arrays under them). Besides ids/urls/timestamps this also covers
575
573
  // code/query-bearing keys (`sql`, `query`, `expression`, `formula`, `code`):
576
- // prose-redacting a SQL string injects fake emails/numbers into it and rewrites
577
- // its leading tokens, producing `syntax error at or near "Henry"` /
578
- // `queries must start with SELECT or WITH`. The query must run untouched; its
579
- // RESULTS are what get redacted so the chart still shows fake values.
574
+ // redacting a SQL string mutates literals and can make the query semantically
575
+ // wrong or invalid. The query must run untouched; its RESULTS are what get
576
+ // redacted so the chart still shows fake values.
580
577
  const PROTECTED_KEY_RE =
581
578
  /^id$|(^|_)id$|Id$|Ids$|uuid|guid|slug|token|secret|password|passwd|apikey|api_key|hash|sha\d*|etag|cursor|nonce|sessionid|messageid|threadid|nodeid|(^|_)key$|keyid|(^|_)ref$|url$|uri$|href$|src$|path$|filename$|mimetype|mime|^sql$|sql$|query|expression|formula|^code$|createdat|updatedat|deletedat|expiresat|timestamp|.+at$|.+_at$/i;
582
579
 
583
- // UI labels and chart/dashboard titles often look like person names ("Maya
584
- // Davis (First-party)", "Clicks by Henry Moore") but are structural labels. Keep
585
- // them stable while still redacting free-text and contact-style person fields.
586
- const LABEL_TEXT_KEY_RE = /^(name|title|label)$/i;
587
-
588
580
  const MAX_DEPTH = 64;
589
581
 
590
- function isStandalonePersonNameLabel(text: string): boolean {
591
- const trimmed = text.trim();
592
- if (!trimmed) return false;
593
- return /^[A-Z][a-zA-Z'’-]+(?:\s+[A-Z]\.?)?\s+[A-Z][a-zA-Z'’-]+$/.test(
594
- trimmed,
595
- );
596
- }
597
-
598
- function shouldRedactNamesForChild(key: string, entry: unknown): boolean {
599
- if (!LABEL_TEXT_KEY_RE.test(key)) return true;
600
- // Preserve structural labels/titles, but do not let common contact-shaped
601
- // fields like `{ name: "Olivia Parker" }` leak real person names.
602
- return typeof entry === "string" ? isStandalonePersonNameLabel(entry) : true;
603
- }
604
-
605
582
  function isPlainObject(value: unknown): value is Record<string, unknown> {
606
583
  if (value === null || typeof value !== "object") return false;
607
584
  if (Array.isArray(value)) return false;
@@ -610,14 +587,6 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
610
587
  return proto === Object.prototype || proto === null;
611
588
  }
612
589
 
613
- function redactDemoStringWithSalt(
614
- text: string,
615
- salt: string,
616
- redactNames: boolean,
617
- ): string {
618
- return redactDemoStringInternal(text, salt, redactNames);
619
- }
620
-
621
590
  function redactNumberLeaf(value: number, salt: string): number {
622
591
  if (!Number.isFinite(value)) return value;
623
592
  const repr = String(value);
@@ -635,7 +604,8 @@ function walk(
635
604
  salt: string,
636
605
  depth: number,
637
606
  seen: WeakSet<object>,
638
- redactNames: boolean,
607
+ redactNumbers: boolean,
608
+ redactProtectedEmails: boolean,
639
609
  ): unknown {
640
610
  if (depth > MAX_DEPTH) return value;
641
611
 
@@ -644,11 +614,11 @@ function walk(
644
614
  const t = typeof value;
645
615
 
646
616
  if (t === "string") {
647
- return redactDemoStringWithSalt(value as string, salt, redactNames);
617
+ return redactDemoStringInternal(value as string, salt, redactNumbers);
648
618
  }
649
619
 
650
620
  if (t === "number") {
651
- return redactNumberLeaf(value as number, salt);
621
+ return redactNumbers ? redactNumberLeaf(value as number, salt) : value;
652
622
  }
653
623
 
654
624
  if (t === "boolean" || t === "bigint" || t === "function" || t === "symbol") {
@@ -661,7 +631,7 @@ function walk(
661
631
  if (seen.has(value)) return value;
662
632
  seen.add(value);
663
633
  const out = value.map((item) =>
664
- walk(item, salt, depth + 1, seen, redactNames),
634
+ walk(item, salt, depth + 1, seen, redactNumbers, redactProtectedEmails),
665
635
  );
666
636
  seen.delete(value);
667
637
  return out;
@@ -681,7 +651,20 @@ function walk(
681
651
  ) {
682
652
  // Still recurse into nested structures, but the protected key does
683
653
  // not transform its own leaf value.
684
- out[key] = walk(entry, salt, depth + 1, seen, redactNames);
654
+ out[key] = walk(
655
+ entry,
656
+ salt,
657
+ depth + 1,
658
+ seen,
659
+ redactNumbers,
660
+ redactProtectedEmails,
661
+ );
662
+ } else if (
663
+ redactProtectedEmails &&
664
+ typeof entry === "string" &&
665
+ entry.includes("@")
666
+ ) {
667
+ out[key] = redactDemoStringInternal(entry, salt, false);
685
668
  } else {
686
669
  // Leaf under a protected key: pass through completely untouched.
687
670
  out[key] = entry;
@@ -693,7 +676,8 @@ function walk(
693
676
  salt,
694
677
  depth + 1,
695
678
  seen,
696
- redactNames && shouldRedactNamesForChild(key, entry),
679
+ redactNumbers,
680
+ redactProtectedEmails,
697
681
  );
698
682
  }
699
683
  seen.delete(value);
@@ -706,7 +690,14 @@ function walk(
706
690
 
707
691
  export function redactDemoData<T>(value: T, opts?: RedactOptions): T {
708
692
  const salt = opts?.salt ?? "";
709
- return walk(value, salt, 0, new WeakSet<object>(), true) as T;
693
+ return walk(
694
+ value,
695
+ salt,
696
+ 0,
697
+ new WeakSet<object>(),
698
+ opts?.redactNumbers !== false,
699
+ opts?.redactProtectedEmails === true,
700
+ ) as T;
710
701
  }
711
702
 
712
703
  /**
@@ -102,12 +102,60 @@ agent answers about browser recordings in the Analytics template.
102
102
  - Wait for all replay chunks (`isComplete`) before constructing the rrweb
103
103
  `Replayer`. Progressive chunk publishes should only update the loading bar;
104
104
  rebuilding the player mid-load desyncs the scrubber and playhead.
105
- - Pass events to `Replayer` untouched. rrweb rebuilds them in a sandboxed iframe;
106
- pre-processing DOM, stylesheet, resource, or mutation payloads makes playback
107
- diverge from the captured page. Let rrweb own iframe sizing via Meta /
108
- ViewportResize, and keep the outer wrapper in that same coordinate system for
109
- fit-to-stage scaling. Never clamp only one layer, rewrite Meta widths, or force
110
- iframe dimensions that desyncs the FullSnapshot DOM and breaks the stage.
105
+ - Pass normal events to `Replayer` untouched. rrweb rebuilds them in a sandboxed
106
+ iframe; pre-processing DOM, stylesheet, resource, or mutation payloads makes
107
+ playback diverge from the captured page. In particular, never rewrite `href`, `src`,
108
+ `_cssText`, CSS `url()`, or Meta URLs to `about:blank`; that exact remediation
109
+ broke historical replay CSS in PR #2040. Handle request privacy at capture or
110
+ the sandbox boundary instead of mutating stored rrweb events. Historical
111
+ captures without inlined resources require live stylesheet/image/font
112
+ requests for accurate rendering; the viewer accepts that fidelity tradeoff,
113
+ uses rrweb's script-disabled sandbox plus `referrerpolicy="no-referrer"`, and
114
+ must never add credentials or proxy those URLs through a privileged server.
115
+ - Capture-time URL scrubbing must preserve load-bearing DOM resource attributes:
116
+ `src`, `srcset`, `poster`, `data`, and `href` only on resource links such as
117
+ stylesheets, preloads, and icons. Signed CDN query parameters are part of the
118
+ resource identity; redacting them produces missing CSS, fonts, images, and
119
+ oversized fallback icons. Keep scrubbing Meta/navigation URLs, anchor hrefs,
120
+ and console/network diagnostics. Captured `_cssText` and CSS `@import`/`url()`
121
+ values must remain byte-identical.
122
+ - rrweb rebuilds into an `about:srcdoc` iframe, which inherits the Analytics
123
+ document's CSP. Analytics currently sends no CSP header; if a future change
124
+ adds restrictive `style-src`, `font-src`, or `img-src` directives, verify
125
+ historical replays and resolve external imports/fonts at capture before
126
+ blocking the recorded resource origins. Do not diagnose current font loss as
127
+ CSP without checking the deployed response headers first.
128
+ - Let rrweb own normal iframe sizing via Meta / ViewportResize, and keep the
129
+ outer wrapper in that same coordinate system for fit-to-stage scaling. Some
130
+ legacy recordings contain demonstrably corrupt 4,000–7,500px-wide viewport
131
+ values from ordinary desktop sessions. Only widths of at least 4,000px with
132
+ aspect ratios above 4:1 are recovered to a 16:10 viewport; real 32:9 and
133
+ mobile portrait captures remain untouched. The resize handler applies the
134
+ recovery to **both** rrweb's iframe and the outer stage. This includes the
135
+ historical 3,000–3,999px-wide, sub-1,000px-high malformed shape; do not
136
+ narrow the guard back to only 4,000px+ recordings.
137
+ Never remove this recovery, clamp only one layer, or broadly rewrite event
138
+ geometry: those changes repeatedly recreated the ribbon/clipping regression.
139
+ - The malformed-viewport recovery's sole event-data exception is pointer
140
+ geometry: project MouseMove, TouchMove, Drag, and MouseInteraction x/y into
141
+ the corrected camera and clamp impossible coordinates. Correcting the iframe
142
+ without its paired pointer projection makes rrweb cast valid target ids
143
+ thousands of pixels off-stage. Normal-viewport pointer events must retain
144
+ their original object references and exact coordinates.
145
+ - Keep rrweb's stock cursor stylesheet and its hotspot transform. During
146
+ playback, hide the viewer's native pointer over Analytics' transparent
147
+ click-to-pause overlay so it cannot masquerade as a frozen recorded cursor.
148
+ - Keep rrweb's recorded focus handling enabled. Focus and focus-visible state
149
+ affect menus, forms, and keyboard UX; disabling `triggerFocus` makes a valid
150
+ snapshot diverge from the source page.
151
+ - `insertStyleRules` may suppress known toast/snackbar containers only. Never
152
+ hide generic framework primitives such as
153
+ `[data-radix-popper-content-wrapper]`: Radix dropdowns, selects, tooltips,
154
+ and other real recorded product UI all share that wrapper.
155
+ - Keep the realistic fidelity and malformed-viewport tests in
156
+ `SessionDetailPage.spec.ts`. Do not change their expectations merely to bless
157
+ a new sanitizer or raw ultra-wide sizing; validate the affected replay in a
158
+ browser first.
111
159
  - The event timeline soft-highlights the active marker, auto-scrolls it into
112
160
  view (pausing briefly after manual scroll), and supports search. It appears
113
161
  beside the player from ~880px content width upward.
@@ -144,3 +192,14 @@ agent answers about browser recordings in the Analytics template.
144
192
  `.an-mask` or `data-an-mask`.
145
193
  - Use `.an-block`, `.an-ignore`, `data-an-block`, or `data-an-ignore` for
146
194
  sensitive zones that should not be captured.
195
+ - A definitive upload `409` abandons only the conflicted replay identity and
196
+ immediately starts rrweb again under a fresh per-tab id, producing a new
197
+ Meta + FullSnapshot for long-lived SPA tabs. Recovery is limited to one
198
+ restart until an upload succeeds so a misconfigured endpoint cannot loop;
199
+ Analytics tracks the content-free `session replay upload rejected` lifecycle
200
+ event so conflicts and recovery success are measurable.
201
+ - Do not label an old recording "corrupt" from pointer coordinates, unknown
202
+ mutation node ids, or changing Meta geometry alone. Those shapes can be
203
+ legitimate with scrolling, iframes/shadow DOM, navigation, and resize. A
204
+ historical-artifact notice needs a durable capture/ingest marker or another
205
+ low-false-positive invariant; do not guess from playback heuristics.
@@ -41,12 +41,12 @@ details live in `.agents/skills/`.
41
41
  unless the user explicitly asks to inspect the demo dashboard.
42
42
  - Framework Demo mode is separate from the built-in `demo` source. While Demo
43
43
  mode is enabled, Analytics line and area charts reshape each returned numeric
44
- series into a deterministic, natural-looking upward trend using that series'
45
- actual minimum, maximum, and normalized volatility. Smooth source series stay
46
- smooth; spiky source series keep larger swings. This is presentation-only:
47
- query results, tables, metrics, exports, and non-time-series charts remain
48
- unchanged, so do not cite the displayed intermediate line/area points as
49
- retrieved source values.
44
+ series into a deterministic upward trend using that series' actual minimum,
45
+ maximum, and local step pattern. Source spikes and dips remain at the same
46
+ x-positions while the smallest necessary linear drift moves the series up and
47
+ to the right. This is presentation-only: query results, tables, metrics,
48
+ exports, and non-time-series charts remain unchanged, so do not cite the
49
+ displayed intermediate line/area points as retrieved source values.
50
50
  - Every analytical answer should include enough audit context for the user to
51
51
  trust it: source(s), time window, filters, sample size or row count,
52
52
  join/match method when relevant, and caveats/gaps.