File size: 4,907 Bytes
092334a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4748aae
 
 
 
37d92f0
 
ea7b176
 
 
 
 
 
c3e4cb4
 
 
 
 
f5ffed6
 
 
 
d349fee
 
 
 
 
 
 
092334a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
// ---------------------------------------------------------------------------
// viz / seriesData.ts β€” EXIT wave 2 (W2-7/W2-9). The Y1 chart block's data
// preparation, pure and React-free so `verify_ui.py` can run it under node.
//
// Both functions here guard a MEASURED trap rather than a hypothetical one:
//   Β· `inServerOrder` β€” the engine sorts a non-date category axis by VALUE
//     DESCENDING, so a trend renders as a revenue-ordered sawtooth unless the
//     server's chronology is reapplied (Y1 amendment 7).
//   Β· `asFields` β€” a chart field carries the viz `FieldType` vocabulary, NOT
//     Y1's `fmt` enum, and mixing them typechecks nowhere and renders as raw
//     text (Y1 rule 1).
// ---------------------------------------------------------------------------

import type { Bucket } from "./chartData";
import type { Field, FieldType } from "./types";

/** The Y1 `fields[]` entry β€” the server's minimal field descriptor. */
export interface WireField {
  key: string;
  label: string;
  type: string;
  source?: string;
}

/** The viz field-type vocabulary as a runtime set. ⚠ `money` is deliberately
 *  absent: it belongs to Y1's `fmt` enum, and a chart field carrying it is the
 *  mix-up rule 1 warns about. It lands on `text`, which is the visible symptom. */
export const VIZ_FIELD_TYPES: ReadonlySet<string> = new Set([
  "text", "status", "currency", "int", "date", "pct", "select", "user",
  "multiselect", "checkbox", "phone", "email", "url", "rating", "created_time", "formula",
  // Wave 18 C5 β‘£b β€” the RUNTIME copy of the vocabulary. A `Set` is not a `Record`: the
  // compiler stays green when this goes stale and `asFields` silently downgrades the column
  // to text. When a `FieldType` joins the union, this line moves WITH the batch.
  "automation",
  // Wave 22 C7 β€” `metric` is numeric (a measure over a snapshot window) and PLOTS.
  "metric",
  // Wave 19 R7 β€” `image`. Listed for the reason the comment above gives, not because a chart
  // plots pictures: it does not. `isNumericFieldType` leaves it categorical, so it groups like
  // any other reference string. What listing it BUYS is honesty β€” without it `asFields` would
  // report the column as `text`, and the one thing a viz field type decides is what the surface
  // is allowed to claim about the column.
  "image",
  // Wave 23 C7 β€” `json`. THE BATCH LINE the union's own comment names: a `Set` is not a
  // `Record`, so the compiler stays green when this goes stale and `asFields` silently reports
  // a json column as `text`. Same reasoning as `image` β€” the type is categorical either way,
  // and what listing it buys is that the surface stops mis-naming the column.
  "json",
  // 2026-08-07 β€” the relational pair. THE BATCH LINE again: a `Set` is not a `Record`, so
  // without this `asFields` would report a rollup column as `text` and the chart builder would
  // refuse to plot the one column the feature exists to produce.
  "link", "rollup",
  // ⭐ Wave-27 item 13 (R13) β€” `code`. THE BATCH LINE a fourth time, and this one was caught by
  // `verify_ui` rather than by the compiler, which is the whole point of that gate existing: the
  // union in `viz/types.ts` went red under `tsc` the moment `code` joined the grid's, and this
  // Set stayed green while `asFields` quietly reported a code column as `text`. Categorical
  // either way (`isNumericFieldType` does not name it); what listing it buys is that the surface
  // stops mis-naming the column.
  "code",
]);

/**
 * Y1 `fields[]` β†’ viz `Field[]`.
 *
 * ⚠ An UNRECOGNISED `type` falls back to `text` rather than throwing β€” the
 * server may learn a field type before this client does (Y1 rule 7's reasoning,
 * one level down), and one unknown type must not blank a whole chart. `text` is
 * the safe landing: it groups, and it never claims to be measurable.
 */
export function asFields(wire: WireField[]): Field[] {
  return wire.map((f) => ({
    key: f.key,
    label: f.label,
    type: (VIZ_FIELD_TYPES.has(f.type) ? f.type : "text") as FieldType,
    source: f.source === "overlay" ? "overlay" : "odoo",
  }));
}

/**
 * Re-order buckets to the server's declared chronology.
 *
 * β›” ANYTHING THE LIST DOES NOT NAME IS APPENDED, NEVER DROPPED. Filtering to
 * `x_order` would silently delete a period the server plotted but forgot to
 * order β€” a bar disappearing from a revenue trend, with nothing anywhere going
 * red. Sorting unnamed keys to the end keeps every value on screen and makes
 * the omission visible instead.
 */
export function inServerOrder(buckets: Bucket[], xOrder?: string[]): Bucket[] {
  if (!xOrder || xOrder.length === 0) return buckets;
  const rank = new Map(xOrder.map((k, i) => [k, i]));
  return [...buckets].sort(
    (a, b) =>
      (rank.get(a.key) ?? Number.MAX_SAFE_INTEGER) -
      (rank.get(b.key) ?? Number.MAX_SAFE_INTEGER)
  );
}