@jeffreyjyz/mpc 0.0.0-stage → 0.1.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/LICENSE +18 -0
- package/README.md +269 -2
- package/package.json +31 -5
- package/src/cli/config.ts +79 -0
- package/src/cli/engine/ability.ts +50 -0
- package/src/cli/engine/cost.ts +62 -0
- package/src/cli/engine/index.ts +59 -0
- package/src/cli/engine/project.ts +145 -0
- package/src/cli/engine/rows.ts +64 -0
- package/src/cli/engine/score.ts +118 -0
- package/src/cli/flow/check.ts +66 -0
- package/src/cli/flow/collect.ts +76 -0
- package/src/cli/flow/columns.ts +32 -0
- package/src/cli/flow/sort.ts +68 -0
- package/src/cli/options.ts +78 -0
- package/src/cli/parse/cac.ts +114 -0
- package/src/cli/parse/fields.ts +65 -0
- package/src/cli/parse/map.ts +116 -0
- package/src/cli/parse/plugins.ts +67 -0
- package/src/cli/parse/validate.ts +60 -0
- package/src/cli/run.ts +151 -0
- package/src/constants/cli.ts +153 -0
- package/src/constants/data.ts +30 -0
- package/src/constants/scoring.ts +22 -0
- package/src/constants/shape.ts +19 -0
- package/src/constants/sources.ts +69 -0
- package/src/constants/view.ts +161 -0
- package/src/data/bench/cc.ts +26 -0
- package/src/data/bench/index.ts +126 -0
- package/src/data/bench/resolve.ts +107 -0
- package/src/data/bench/store.ts +169 -0
- package/src/data/bench/types.ts +34 -0
- package/src/data/cmduse.ts +43 -0
- package/src/data/scrape/catalog/catalog.ts +99 -0
- package/src/data/scrape/catalog/deal.ts +20 -0
- package/src/data/scrape/catalog/numeric.ts +30 -0
- package/src/data/scrape/catalog/variant.ts +11 -0
- package/src/data/scrape/index.ts +21 -0
- package/src/data/scrape/roleRows.ts +41 -0
- package/src/data/scrape/tables.ts +96 -0
- package/src/data/shape.ts +205 -0
- package/src/data/sources/aa/api.ts +54 -0
- package/src/data/sources/aa/parse.ts +66 -0
- package/src/data/sources/aa/web.ts +53 -0
- package/src/data/sources/cc/catalog.ts +71 -0
- package/src/data/sources/cc/cmduse.ts +24 -0
- package/src/data/sources/cc/plans.ts +29 -0
- package/src/data/sources/opencode.ts +48 -0
- package/src/data/usage/index.ts +118 -0
- package/src/data/usage/log.ts +89 -0
- package/src/data/usage/logs.ts +117 -0
- package/src/data/usage/opencodeDb.ts +135 -0
- package/src/data/usage/opencodeV2.ts +70 -0
- package/src/data/usage/parse.ts +85 -0
- package/src/index.ts +11 -0
- package/src/keys.ts +29 -0
- package/src/types.ts +92 -0
- package/src/view/columns/cc.ts +50 -0
- package/src/view/columns/meta.ts +41 -0
- package/src/view/columns/oc.ts +50 -0
- package/src/view/footer.ts +124 -0
- package/src/view/layout/fit.ts +58 -0
- package/src/view/layout/segments.ts +98 -0
- package/src/view/layout/table.ts +85 -0
- package/src/view/layout/usage.ts +163 -0
- package/src/view/render.ts +62 -0
- package/src/view/schema.ts +81 -0
- package/src/view/text/export.ts +72 -0
- package/src/view/text/format.ts +79 -0
- package/src/view/text/index.ts +54 -0
- package/src/view/text/styles.ts +151 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { CompareRow, PlanInfo, ProviderId, Workload } from "~/types.ts";
|
|
2
|
+
|
|
3
|
+
export type Row = CompareRow;
|
|
4
|
+
|
|
5
|
+
/** One table column. */
|
|
6
|
+
export interface Column {
|
|
7
|
+
header: string;
|
|
8
|
+
value: (row: Row) => string;
|
|
9
|
+
right?: boolean;
|
|
10
|
+
/** Optional SGR code applied to the padded cell (green 32, dim 2, ...). */
|
|
11
|
+
style?: (row: Row) => string | undefined;
|
|
12
|
+
/** Lower values are dropped first when the table is too wide. */
|
|
13
|
+
drop?: number;
|
|
14
|
+
/** When false the cell is not padded to the column width. */
|
|
15
|
+
pad?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type GroupKey = "model" | "oc" | "cc" | "misc";
|
|
19
|
+
|
|
20
|
+
export interface GroupLabel {
|
|
21
|
+
title: string;
|
|
22
|
+
color: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** A contiguous run of columns belonging to one group. */
|
|
26
|
+
export interface Segment {
|
|
27
|
+
key: GroupKey;
|
|
28
|
+
columns: Column[];
|
|
29
|
+
widths: number[];
|
|
30
|
+
/** Total printed width including the gaps between columns. */
|
|
31
|
+
width: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface FitResult {
|
|
35
|
+
ids: string[];
|
|
36
|
+
dropped: string[];
|
|
37
|
+
width: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface Tally {
|
|
41
|
+
/** Rows present on both sides. */
|
|
42
|
+
headToHead: number;
|
|
43
|
+
ocWins: number;
|
|
44
|
+
ccWins: number;
|
|
45
|
+
ties: number;
|
|
46
|
+
ocOnly: number;
|
|
47
|
+
ccOnly: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ReportMeta {
|
|
51
|
+
ocPlan: PlanInfo;
|
|
52
|
+
ccPlan: PlanInfo;
|
|
53
|
+
/** The request shape actually priced — one per side when a shape was measured. */
|
|
54
|
+
workloads: Record<ProviderId, Workload>;
|
|
55
|
+
/** Ability source label, e.g. "CommandCode Intelligence". */
|
|
56
|
+
abilityLabel?: string;
|
|
57
|
+
abilityNote?: string;
|
|
58
|
+
/** Whether reqshape was used for the workload, and the alternative flags. */
|
|
59
|
+
shapeNote?: string;
|
|
60
|
+
/** Override for the rolling-window ratios, five-hour then weekly. */
|
|
61
|
+
window?: [number, number];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
import { CC_COLUMNS } from "./columns/cc.ts";
|
|
65
|
+
import { META_COLUMNS } from "./columns/meta.ts";
|
|
66
|
+
import { OC_COLUMNS } from "./columns/oc.ts";
|
|
67
|
+
|
|
68
|
+
/** Every column the table can render, keyed by the id used with --columns. */
|
|
69
|
+
const COLUMNS: Record<string, Column> = {
|
|
70
|
+
...META_COLUMNS,
|
|
71
|
+
...OC_COLUMNS,
|
|
72
|
+
...CC_COLUMNS,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export function columns(ids: string[]): Column[] {
|
|
76
|
+
return ids.map((id) => {
|
|
77
|
+
const column = COLUMNS[id];
|
|
78
|
+
if (!column) throw new Error(`unknown column "${id}"`);
|
|
79
|
+
return column;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { CSV_HEADER } from "~/constants/view.ts";
|
|
2
|
+
import type { PlanInfo } from "~/types.ts";
|
|
3
|
+
import type { Row, Tally } from "~/view/schema.ts";
|
|
4
|
+
import { fmtAbility, fmtCount, fmtTps, planTitle } from "./format.ts";
|
|
5
|
+
|
|
6
|
+
function csvCell(value: string | number): string {
|
|
7
|
+
const text = String(value);
|
|
8
|
+
return /[",\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** One line per model-provider entry, so a spreadsheet can pivot it. */
|
|
12
|
+
export function renderCsv(rows: Row[], summary: Tally): string {
|
|
13
|
+
const lines = [CSV_HEADER.join(",")];
|
|
14
|
+
for (const row of rows) {
|
|
15
|
+
for (const entry of [row.oc, row.cc]) {
|
|
16
|
+
if (!entry) continue;
|
|
17
|
+
lines.push(
|
|
18
|
+
[
|
|
19
|
+
row.name,
|
|
20
|
+
entry.plan,
|
|
21
|
+
entry.allowance,
|
|
22
|
+
Number.isFinite(entry.requestsPerMonth)
|
|
23
|
+
? Math.round(entry.requestsPerMonth)
|
|
24
|
+
: "inf",
|
|
25
|
+
(entry.payPerRequest * 1000).toPrecision(6),
|
|
26
|
+
entry.payPerRequest.toPrecision(6),
|
|
27
|
+
entry.ability ?? "",
|
|
28
|
+
entry.tps ?? "",
|
|
29
|
+
100 - entry.index,
|
|
30
|
+
entry.valueIndex ?? "",
|
|
31
|
+
]
|
|
32
|
+
.map(csvCell)
|
|
33
|
+
.join(","),
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
lines.push(
|
|
38
|
+
`# head-to-head ${summary.headToHead} · OC ${summary.ocWins} · CC ${summary.ccWins} · tie ${summary.ties}`,
|
|
39
|
+
);
|
|
40
|
+
return `${lines.join("\n")}\n`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Markdown table with the default columns. */
|
|
44
|
+
export function renderMarkdown(
|
|
45
|
+
rows: Row[],
|
|
46
|
+
plans: [PlanInfo, PlanInfo],
|
|
47
|
+
): string {
|
|
48
|
+
const lines = [
|
|
49
|
+
`# ${planTitle(plans[0])} vs ${planTitle(plans[1])}`,
|
|
50
|
+
"",
|
|
51
|
+
"| model | OC req/mo | OC $/1K | CC req/mo | CC $/1K | ability | tps | COST | VAL |",
|
|
52
|
+
"| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |",
|
|
53
|
+
];
|
|
54
|
+
const perK = (value: number): string => `$${fmtCount(value * 1000)}`;
|
|
55
|
+
for (const row of rows) {
|
|
56
|
+
const cells = [
|
|
57
|
+
row.name,
|
|
58
|
+
row.oc ? fmtCount(row.oc.requestsPerMonth) : "—",
|
|
59
|
+
row.oc ? perK(row.oc.payPerRequest) : "—",
|
|
60
|
+
row.cc ? fmtCount(row.cc.requestsPerMonth) : "—",
|
|
61
|
+
row.cc ? perK(row.cc.payPerRequest) : "—",
|
|
62
|
+
fmtAbility(row),
|
|
63
|
+
fmtTps(row),
|
|
64
|
+
String(100 - Math.max(row.oc?.index ?? -1, row.cc?.index ?? -1, 0)),
|
|
65
|
+
String(row.oc?.valueIndex ?? row.cc?.valueIndex ?? "—"),
|
|
66
|
+
];
|
|
67
|
+
lines.push(
|
|
68
|
+
`| ${cells.map((c) => c.replace(/\|/g, "\\|")).join(" | ")} |`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
return `${lines.join("\n")}\n`;
|
|
72
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { EntryMetrics } from "~/types.ts";
|
|
2
|
+
import type { Row } from "~/view/schema.ts";
|
|
3
|
+
|
|
4
|
+
export function fmtCount(n: number): string {
|
|
5
|
+
if (!Number.isFinite(n)) return "∞";
|
|
6
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
7
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
|
|
8
|
+
if (n >= 10) return n.toFixed(0);
|
|
9
|
+
return n.toFixed(1);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function fmtUsd(n: number): string {
|
|
13
|
+
if (n === 0) return "free";
|
|
14
|
+
// Stay in fixed-point for tiny per-request costs; scientific notation is
|
|
15
|
+
// hard to read at a glance. Trim trailing zeros so $0.000110 reads cleanly.
|
|
16
|
+
const digits = n >= 0.01 ? 4 : n >= 1e-6 ? 8 : 11;
|
|
17
|
+
const fixed = n.toFixed(digits).replace(/0+$/, "").replace(/\.$/, "");
|
|
18
|
+
return `$${fixed}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function fmtRate(n: number): string {
|
|
22
|
+
if (n === 0) return "free";
|
|
23
|
+
return `$${Number(n.toFixed(4))}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function fmtAllowance(m: EntryMetrics | undefined): string {
|
|
27
|
+
if (!m) return "—";
|
|
28
|
+
return m.free ? "free" : fmtRate(m.allowance);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Cost per 1,000 requests — the per-request figure scaled up to readable dollars. */
|
|
32
|
+
export function fmtPerThousand(m: EntryMetrics | undefined): string {
|
|
33
|
+
if (!m) return "—";
|
|
34
|
+
return fmtUsd(m.payPerRequest * 1000);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Requests one dollar of subscription buys on this model. */
|
|
38
|
+
export function fmtPerDollar(m: EntryMetrics | undefined): string {
|
|
39
|
+
if (!m) return "—";
|
|
40
|
+
if (m.free || m.payPerRequest === 0) return "∞";
|
|
41
|
+
return fmtCount(1 / m.payPerRequest);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function fmtRequests(
|
|
45
|
+
value: number | undefined,
|
|
46
|
+
hasEntry: boolean,
|
|
47
|
+
): string {
|
|
48
|
+
if (!hasEntry || value === undefined) return "—";
|
|
49
|
+
return fmtCount(value);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function fmtAbility(row: Row): string {
|
|
53
|
+
const ability = row.oc?.ability ?? row.cc?.ability ?? null;
|
|
54
|
+
return ability === null ? "—" : ability.toFixed(1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function fmtTps(row: Row): string {
|
|
58
|
+
const tps = row.oc?.tps ?? row.cc?.tps ?? null;
|
|
59
|
+
return tps === null ? "—" : Math.round(tps).toString();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
import type { ProviderId } from "~/types.ts";
|
|
63
|
+
|
|
64
|
+
export function providerName(provider: ProviderId): string {
|
|
65
|
+
return provider === "oc-go" ? "OpenCode" : "CommandCode";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Short provider tag, for the WIN column. */
|
|
69
|
+
export function shortProviderName(provider: ProviderId): string {
|
|
70
|
+
return provider === "oc-go" ? "OC" : "CC";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** e.g. "OpenCode Go", "CommandCode GOAT". */
|
|
74
|
+
export function planTitle(plan: {
|
|
75
|
+
provider: ProviderId;
|
|
76
|
+
label: string;
|
|
77
|
+
}): string {
|
|
78
|
+
return `${providerName(plan.provider)} ${plan.label}`;
|
|
79
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { ColorMode } from "~/cli/options.ts";
|
|
2
|
+
|
|
3
|
+
let color = true;
|
|
4
|
+
|
|
5
|
+
/** Resolve a mode; "auto" follows the TTY and NO_COLOR. */
|
|
6
|
+
export function setColorMode(mode: ColorMode): void {
|
|
7
|
+
if (mode === "always") color = true;
|
|
8
|
+
else if (mode === "never") color = false;
|
|
9
|
+
else color = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function setColor(enabled: boolean): void {
|
|
13
|
+
color = enabled;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function paint(code: string, text: string): string {
|
|
17
|
+
return color ? `\u001b[${code}m${text}\u001b[0m` : text;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export {
|
|
21
|
+
fmtAbility,
|
|
22
|
+
fmtAllowance,
|
|
23
|
+
fmtCount,
|
|
24
|
+
fmtPerDollar,
|
|
25
|
+
fmtPerThousand,
|
|
26
|
+
fmtRate,
|
|
27
|
+
fmtRequests,
|
|
28
|
+
fmtTps,
|
|
29
|
+
fmtUsd,
|
|
30
|
+
planTitle,
|
|
31
|
+
providerName,
|
|
32
|
+
shortProviderName,
|
|
33
|
+
} from "./format.ts";
|
|
34
|
+
export {
|
|
35
|
+
abilityStyle,
|
|
36
|
+
bestCost,
|
|
37
|
+
bestIndex,
|
|
38
|
+
bestValue,
|
|
39
|
+
ccRates,
|
|
40
|
+
cheaperSide,
|
|
41
|
+
costStyle,
|
|
42
|
+
enabledStyle,
|
|
43
|
+
freeCc,
|
|
44
|
+
freeOc,
|
|
45
|
+
ocRates,
|
|
46
|
+
pricingTriple,
|
|
47
|
+
setAbilityRange,
|
|
48
|
+
sideStyle,
|
|
49
|
+
tint,
|
|
50
|
+
tintOrFree,
|
|
51
|
+
valueStyle,
|
|
52
|
+
winner,
|
|
53
|
+
winnerStyle,
|
|
54
|
+
} from "./styles.ts";
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { PROVIDER_COLOR, PROVIDER_TINT, SGR } from "~/constants/view.ts";
|
|
2
|
+
import type { EntryMetrics, ProviderId } from "~/types.ts";
|
|
3
|
+
import type { Row } from "~/view/schema.ts";
|
|
4
|
+
import { fmtRate, shortProviderName } from "./format.ts";
|
|
5
|
+
|
|
6
|
+
/** Which side is cheaper per request on this row. */
|
|
7
|
+
export function cheaperSide(row: Row): "oc" | "cc" | "tie" | "none" {
|
|
8
|
+
if (!row.oc || !row.cc) return "none";
|
|
9
|
+
if (row.oc.payPerRequest === row.cc.payPerRequest) return "tie";
|
|
10
|
+
return row.oc.payPerRequest < row.cc.payPerRequest ? "oc" : "cc";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function winner(row: Row): string {
|
|
14
|
+
// One-sided rows are not wins; name the side that carries the model.
|
|
15
|
+
if (!row.oc) return `${shortProviderName("cc")} only`;
|
|
16
|
+
if (!row.cc) return `${shortProviderName("oc-go")} only`;
|
|
17
|
+
const side = cheaperSide(row);
|
|
18
|
+
if (side === "tie") return "tie";
|
|
19
|
+
return shortProviderName(side === "oc" ? "oc-go" : "cc");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function bestIndex(row: Row): number {
|
|
23
|
+
return Math.max(row.oc?.index ?? -1, row.cc?.index ?? -1, 0);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function valueScores(row: Row): number[] {
|
|
27
|
+
return [row.oc?.valueIndex, row.cc?.valueIndex].filter(
|
|
28
|
+
(value): value is number => typeof value === "number",
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const bestValue = (r: Row): string => {
|
|
33
|
+
const scores = valueScores(r);
|
|
34
|
+
return scores.length === 0 ? "—" : Math.max(...scores).toString();
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const bestCost = (r: Row): string => (100 - bestIndex(r)).toString();
|
|
38
|
+
|
|
39
|
+
export const pricingTriple = (m: EntryMetrics | undefined): string => {
|
|
40
|
+
if (!m) return "—";
|
|
41
|
+
const p = m.pricing;
|
|
42
|
+
return `${fmtRate(p.input)}/${fmtRate(p.output)}/${fmtRate(p.cacheRead)}`;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const ocRates = (r: Row): string => pricingTriple(r.oc);
|
|
46
|
+
export const ccRates = (r: Row): string => pricingTriple(r.cc);
|
|
47
|
+
export const freeOc = (r: Row): boolean => Boolean(r.oc?.free);
|
|
48
|
+
export const freeCc = (r: Row): boolean => Boolean(r.cc?.free);
|
|
49
|
+
|
|
50
|
+
function entryOf(row: Row, provider: ProviderId): EntryMetrics | undefined {
|
|
51
|
+
return provider === "oc-go" ? row.oc : row.cc;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A provider's whole column set is tinted with its identity colour, so the two
|
|
56
|
+
* halves of the table read apart. An absent cell stays `dim` (dim = missing).
|
|
57
|
+
*/
|
|
58
|
+
export const tint =
|
|
59
|
+
(provider: ProviderId) =>
|
|
60
|
+
(row: Row): string | undefined =>
|
|
61
|
+
entryOf(row, provider) ? PROVIDER_TINT[provider] : SGR.dim;
|
|
62
|
+
|
|
63
|
+
/** The tint, except a free model goes green (favourable). */
|
|
64
|
+
export const tintOrFree =
|
|
65
|
+
(provider: ProviderId) =>
|
|
66
|
+
(row: Row): string | undefined => {
|
|
67
|
+
const entry = entryOf(row, provider);
|
|
68
|
+
if (!entry) return SGR.dim;
|
|
69
|
+
return entry.free ? SGR.green : PROVIDER_TINT[provider];
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The cheaper side's cell in its provider's **bold** colour, the other side in
|
|
74
|
+
* the quiet tint — so the win reads from brightness, and an OC win (cyan) never
|
|
75
|
+
* looks like a CC win (magenta).
|
|
76
|
+
*/
|
|
77
|
+
export function sideStyle(side: "oc" | "cc") {
|
|
78
|
+
const provider: ProviderId = side === "oc" ? "oc-go" : "cc";
|
|
79
|
+
return (row: Row): string | undefined => {
|
|
80
|
+
if (!entryOf(row, provider)) return SGR.dim;
|
|
81
|
+
return cheaperSide(row) === side
|
|
82
|
+
? PROVIDER_COLOR[provider]
|
|
83
|
+
: PROVIDER_TINT[provider];
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** WIN column: the winning side's own colour; a tie or a one-sided row plain. */
|
|
88
|
+
export const winnerStyle = (row: Row): string | undefined => {
|
|
89
|
+
const side = cheaperSide(row);
|
|
90
|
+
if (side === "tie" || side === "none") return undefined;
|
|
91
|
+
return PROVIDER_COLOR[side === "oc" ? "oc-go" : "cc"];
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
let costCuts: [number, number] = [30, 60];
|
|
95
|
+
let valCuts: [number, number] = [40, 70];
|
|
96
|
+
|
|
97
|
+
/** Configure the green/yellow cut-offs for COST and VAL. */
|
|
98
|
+
export function setThresholds(
|
|
99
|
+
cost: [number, number],
|
|
100
|
+
val: [number, number],
|
|
101
|
+
): void {
|
|
102
|
+
costCuts = cost;
|
|
103
|
+
valCuts = val;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* COST is inverted (0 = cheapest/best), so colour the other way round:
|
|
108
|
+
* green = cheap, yellow/orange = mid, red = the pricier rows.
|
|
109
|
+
*/
|
|
110
|
+
export const costStyle = (r: Row): string | undefined => {
|
|
111
|
+
const cost = 100 - bestIndex(r);
|
|
112
|
+
if (cost <= costCuts[0]) return SGR.green;
|
|
113
|
+
if (cost <= costCuts[1]) return SGR.yellow;
|
|
114
|
+
return SGR.red;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const valueStyle = (row: Row): string | undefined => {
|
|
118
|
+
const scores = valueScores(row);
|
|
119
|
+
if (scores.length === 0) return SGR.dim;
|
|
120
|
+
const best = Math.max(...scores);
|
|
121
|
+
if (best >= valCuts[1]) return SGR.green;
|
|
122
|
+
if (best >= valCuts[0]) return SGR.yellow;
|
|
123
|
+
return SGR.red;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export const enabledStyle = (value: number | null): string | undefined =>
|
|
127
|
+
value === null ? SGR.dim : undefined;
|
|
128
|
+
|
|
129
|
+
let abilityRange: [number, number] | null = null;
|
|
130
|
+
|
|
131
|
+
/** The displayed ability range, so the column can rank like COST/VAL. */
|
|
132
|
+
export function setAbilityRange(min: number, max: number): void {
|
|
133
|
+
abilityRange = [min, max];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Ability is an absolute benchmark index with a narrow band, so an absolute
|
|
138
|
+
* scale would be all-red; rank it across the table (min-max) and reuse the VAL
|
|
139
|
+
* cut-offs, exactly as COST/VAL are coloured.
|
|
140
|
+
*/
|
|
141
|
+
export const abilityStyle = (row: Row): string | undefined => {
|
|
142
|
+
const ability = row.oc?.ability ?? row.cc?.ability ?? null;
|
|
143
|
+
if (ability === null) return SGR.dim;
|
|
144
|
+
if (!abilityRange || abilityRange[1] <= abilityRange[0]) return undefined;
|
|
145
|
+
const score =
|
|
146
|
+
(100 * (ability - abilityRange[0])) /
|
|
147
|
+
(abilityRange[1] - abilityRange[0]);
|
|
148
|
+
if (score >= valCuts[1]) return SGR.green;
|
|
149
|
+
if (score >= valCuts[0]) return SGR.yellow;
|
|
150
|
+
return SGR.red;
|
|
151
|
+
};
|