@m6d/cortex-cli 0.15.0 → 0.16.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/README.md +38 -22
- package/package.json +3 -13
- package/src/commands/apply.ts +61 -38
- package/src/commands/management.ts +64 -19
- package/src/commands/new.ts +8 -10
- package/src/commands/setup.ts +77 -38
- package/src/contracts/datasets/dataset-sql.ts +36 -2
- package/src/contracts/datasets/datasets.ts +84 -182
- package/src/contracts/egress-policy/scrub.ts +11 -2
- package/src/contracts/errors/index.ts +7 -0
- package/src/contracts/json/index.ts +40 -17
- package/src/contracts/manifest/index.ts +369 -124
- package/src/contracts/server/index.ts +44 -53
- package/src/contracts/triggers/index.ts +13 -9
- package/src/contracts/wire/index.ts +126 -25
- package/src/management/client.ts +79 -117
- package/src/management/profiles.ts +11 -22
- package/src/scaffold/files.ts +11 -9
- package/src/ui/prompts.ts +30 -9
- package/src/contracts/charts/chart-spec.ts +0 -217
- package/src/contracts/charts/index.ts +0 -8
- package/src/contracts/datasets/datasets.fixtures.json +0 -2849
- package/src/contracts/markdown/index.ts +0 -47
- package/src/contracts/pattern/index.ts +0 -276
- package/src/contracts/server/reserved-names.fixtures.json +0 -16
|
@@ -1,217 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* The boundary on a Vega-Lite spec the model wrote, shared by the widget
|
|
3
|
-
* (which draws it live) and Cortex (which describes it to the model):
|
|
4
|
-
* inline data only, no fetches, no interactivity, bounded work.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
export const MAX_CHART_ROWS = 200;
|
|
8
|
-
const MAX_CHART_SIZE = 4000;
|
|
9
|
-
/** Transforms whose output size is set by their own parameters or by an expression rather than by the rows. */
|
|
10
|
-
const EXPANDING_TRANSFORMS = new Set([
|
|
11
|
-
"flatten",
|
|
12
|
-
"fold",
|
|
13
|
-
"density",
|
|
14
|
-
"impute",
|
|
15
|
-
"regression",
|
|
16
|
-
"loess",
|
|
17
|
-
"quantile",
|
|
18
|
-
"sample",
|
|
19
|
-
]);
|
|
20
|
-
|
|
21
|
-
/** The boundary as the model reads it in its system prompt, so it writes specs this file accepts. */
|
|
22
|
-
export const CHART_SPEC_GUIDE = `Use Vega-Lite JSON with inline \`data.values\` only, at most ${MAX_CHART_ROWS} rows, and no \`url\` anywhere. Compute folds, trend lines and densities yourself. For several series, write one row per series and x value, or keep one row per x value and draw a layer per series (color by \`datum\`) when that would exceed the row limit. These keys are refused anywhere in the spec: ${[...EXPANDING_TRANSFORMS, "params", "selection"].join(", ")}. Expressions in \`calculate\`, \`filter\` and \`labelExpr\` may use only datum fields, literals, operators and simple math or date-part functions (no format or timeFormat), so put formatted values in the rows or use an axis \`format\`.`;
|
|
23
|
-
|
|
24
|
-
/** What a single key or value in a chart spec may not be; undefined when it is fine on its own. */
|
|
25
|
-
function chartNodeViolation(key: string, value: unknown) {
|
|
26
|
-
if (key === "url") return "Charts may only use inline data and no URLs.";
|
|
27
|
-
if (key === "sequence" || key === "graticule") return "Charts may only use inline data.";
|
|
28
|
-
if (EXPANDING_TRANSFORMS.has(key)) return "Charts may not use transforms that generate rows.";
|
|
29
|
-
if (key === "params" || key === "selection" || key === "signals" || key === "on") {
|
|
30
|
-
return "Charts may not be interactive.";
|
|
31
|
-
}
|
|
32
|
-
// An expression reference can compute what a literal could not, such as axis ticks.
|
|
33
|
-
if (key === "expr") return "Charts may not carry expressions.";
|
|
34
|
-
if (EXPRESSION_KEYS.has(key) && typeof value === "string" && !isBoundedExpression(value)) {
|
|
35
|
-
return "Charts may only use simple expressions over datum.";
|
|
36
|
-
}
|
|
37
|
-
if (key === "usermeta") return "Charts may not carry usermeta.";
|
|
38
|
-
if (typeof value === "string" && externalResourceIn(value)) {
|
|
39
|
-
return "Charts may not reference URLs.";
|
|
40
|
-
}
|
|
41
|
-
if ((key === "width" || key === "height") && dimensionPixels(value) > MAX_CHART_SIZE) {
|
|
42
|
-
return `Charts may be at most ${MAX_CHART_SIZE} pixels wide or tall.`;
|
|
43
|
-
}
|
|
44
|
-
return undefined;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Where a Vega-Lite spec holds an expression string that Vega evaluates per row. */
|
|
48
|
-
const EXPRESSION_KEYS = new Set(["calculate", "filter", "test", "signal", "labelExpr"]);
|
|
49
|
-
/** Functions an expression may call: each is constant work on scalars. */
|
|
50
|
-
const EXPRESSION_FUNCTIONS = new Set([
|
|
51
|
-
"abs",
|
|
52
|
-
"ceil",
|
|
53
|
-
"floor",
|
|
54
|
-
"round",
|
|
55
|
-
"min",
|
|
56
|
-
"max",
|
|
57
|
-
"sqrt",
|
|
58
|
-
"pow",
|
|
59
|
-
"log",
|
|
60
|
-
"exp",
|
|
61
|
-
"year",
|
|
62
|
-
"month",
|
|
63
|
-
"date",
|
|
64
|
-
"day",
|
|
65
|
-
"hours",
|
|
66
|
-
"minutes",
|
|
67
|
-
"toNumber",
|
|
68
|
-
"toString",
|
|
69
|
-
"toDate",
|
|
70
|
-
"isValid",
|
|
71
|
-
"isNaN",
|
|
72
|
-
"if",
|
|
73
|
-
"indexof",
|
|
74
|
-
"length",
|
|
75
|
-
]);
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* True when an expression is arithmetic, comparison and logic over datum
|
|
79
|
-
* fields, literals and the scalar functions above. Anything else (another
|
|
80
|
-
* identifier, a call to any other function) is refused, so no expression can
|
|
81
|
-
* allocate by its arguments the way sequence() or pad() do.
|
|
82
|
-
*/
|
|
83
|
-
function isBoundedExpression(expression: string) {
|
|
84
|
-
if (expression.length > 500 || /[\\`]/.test(expression)) return false;
|
|
85
|
-
const stripped = expression.replace(/'[^']*'|"[^"]*"/g, '""');
|
|
86
|
-
for (const [, name] of stripped.matchAll(/([A-Za-z_$][\w$]*)\s*\(/g)) {
|
|
87
|
-
if (!EXPRESSION_FUNCTIONS.has(name ?? "")) return false;
|
|
88
|
-
}
|
|
89
|
-
const identifiers = stripped
|
|
90
|
-
.replace(/(datum|parent)(\s*\.[\w$]+|\s*\[\s*""\s*\])+|\b\d[\w.]*/g, "")
|
|
91
|
-
.matchAll(/[A-Za-z_$][\w$]*/g);
|
|
92
|
-
for (const [name] of identifiers) {
|
|
93
|
-
if (!EXPRESSION_FUNCTIONS.has(name) && !["true", "false", "null"].includes(name))
|
|
94
|
-
return false;
|
|
95
|
-
}
|
|
96
|
-
return true;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/** A chart dimension in pixels: a number, or the band step of its `{ step }` form. */
|
|
100
|
-
function dimensionPixels(value: unknown) {
|
|
101
|
-
if (typeof value === "number") return value;
|
|
102
|
-
const step = (value as { step?: unknown } | null)?.step;
|
|
103
|
-
return typeof step === "number" ? step : 0;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Inline data is opaque: rows, or a GeoJSON collection's features, are
|
|
108
|
-
* counted and never read as controls. Anything else in a `values` position
|
|
109
|
-
* (an axis tick list, an expression object) is a control and is walked.
|
|
110
|
-
*/
|
|
111
|
-
function inlineDataViolation(data: unknown, key: string) {
|
|
112
|
-
const features = (data as { features?: unknown } | null)?.features;
|
|
113
|
-
const rows = Array.isArray(data) ? data.length : Array.isArray(features) ? features.length : -1;
|
|
114
|
-
if (rows < 0)
|
|
115
|
-
return (
|
|
116
|
-
chartNodeViolation(key, data) ??
|
|
117
|
-
firstViolation(childEntries(data, key), chartSpecViolation)
|
|
118
|
-
);
|
|
119
|
-
return rows > MAX_CHART_ROWS ? `Charts may hold at most ${MAX_CHART_ROWS} rows.` : undefined;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Why a Vega-Lite spec is refused, or undefined when it may be drawn. The
|
|
124
|
-
* walk is over the spec's control positions: a `url` key anywhere is a fetch
|
|
125
|
-
* (data, image marks); a generator or a row-producing transform is an
|
|
126
|
-
* unbounded row count; params would redraw after the resource scrub;
|
|
127
|
-
* `usermeta` lets the spec rewrite the embed options; a string carrying
|
|
128
|
-
* url() or a CSS escape could reach an SVG presentation attribute. Inline
|
|
129
|
-
* rows are only counted, never read as controls, and the drawing size is
|
|
130
|
-
* capped so a spec cannot exhaust the reader's page.
|
|
131
|
-
*/
|
|
132
|
-
export function chartSpecViolation(value: unknown, key = ""): string | undefined {
|
|
133
|
-
if (key === "values") return inlineDataViolation(value, key);
|
|
134
|
-
if (key === "datasets") return firstViolation(childEntries(value), inlineDataViolation);
|
|
135
|
-
return (
|
|
136
|
-
chartNodeViolation(key, value) ??
|
|
137
|
-
firstViolation(childEntries(value, key), (child, childKey) =>
|
|
138
|
-
chartSpecViolation(child, childKey),
|
|
139
|
-
)
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/** An array's items under their parent's key, an object's own entries, nothing for a scalar. */
|
|
144
|
-
function childEntries(value: unknown, key = "") {
|
|
145
|
-
if (Array.isArray(value)) return value.map((item) => [key, item] as [string, unknown]);
|
|
146
|
-
return value && typeof value === "object" ? Object.entries(value) : [];
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function firstViolation(
|
|
150
|
-
entries: [string, unknown][],
|
|
151
|
-
check: (value: unknown, key: string) => string | undefined,
|
|
152
|
-
) {
|
|
153
|
-
for (const [key, value] of entries) {
|
|
154
|
-
const reason = check(value, key);
|
|
155
|
-
if (reason) return reason;
|
|
156
|
-
}
|
|
157
|
-
return undefined;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const URL_FUNCTION = /url\(([^)]*)\)/gi;
|
|
161
|
-
/** The CSS functions besides url() that name a resource to load; only url() can point at a fragment, so its argument is checked instead. */
|
|
162
|
-
const OTHER_IMAGE_FUNCTION_NAMES = "src|image|image-set|-webkit-image-set|cross-fade|element";
|
|
163
|
-
/** Every CSS function that names a resource to load. */
|
|
164
|
-
export const IMAGE_FUNCTION = new RegExp(String.raw`\b(url|${OTHER_IMAGE_FUNCTION_NAMES})\(`, "i");
|
|
165
|
-
const OTHER_IMAGE_FUNCTION = new RegExp(String.raw`\b(${OTHER_IMAGE_FUNCTION_NAMES})\(`, "i");
|
|
166
|
-
const OTHER_IMAGE_FUNCTION_GLOBAL = new RegExp(
|
|
167
|
-
String.raw`\b(${OTHER_IMAGE_FUNCTION_NAMES})\([^)]*\)`,
|
|
168
|
-
"gi",
|
|
169
|
-
);
|
|
170
|
-
const CSS_ESCAPE = /\\([0-9a-f]{1,6})\s?|\\(.)/gi;
|
|
171
|
-
const IMPORT_RULE = /@import[^;]*;?/gi;
|
|
172
|
-
|
|
173
|
-
/** Style text with its CSS escapes decoded, so a url() hidden behind one reads as itself. */
|
|
174
|
-
function decodeCssEscapes(text: string) {
|
|
175
|
-
return text.replace(CSS_ESCAPE, (_, hex: string | undefined, literal: string | undefined) => {
|
|
176
|
-
if (!hex) return literal ?? "";
|
|
177
|
-
const code = parseInt(hex, 16);
|
|
178
|
-
// CSS maps null, surrogates and anything past the last code point to U+FFFD.
|
|
179
|
-
const invalid = code === 0 || (code >= 0xd800 && code <= 0xdfff) || code > 0x10ffff;
|
|
180
|
-
return String.fromCodePoint(invalid ? 0xfffd : code);
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/** The target inside one url(...) argument, unquoted and trimmed. */
|
|
185
|
-
function urlTarget(argument: string) {
|
|
186
|
-
return argument
|
|
187
|
-
.trim()
|
|
188
|
-
.replace(/^(['"])(.*)\1$/s, "$2")
|
|
189
|
-
.trim();
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/** True when a CSS or attribute value would make the browser fetch: a url() to anything but a fragment, or an escape that could hide one. */
|
|
193
|
-
export function externalResourceIn(value: string) {
|
|
194
|
-
if (value.includes("\\") || OTHER_IMAGE_FUNCTION.test(value)) return true;
|
|
195
|
-
for (const [, argument] of value.matchAll(URL_FUNCTION)) {
|
|
196
|
-
if (!urlTarget(argument ?? "").startsWith("#")) return true;
|
|
197
|
-
}
|
|
198
|
-
return false;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Style text with every resource-loading function neutralized: CSS escapes
|
|
203
|
-
* decoded first, url() kept only to fragments. A backslash left after the one
|
|
204
|
-
* decode would be a second escape layer the browser decodes again, and an
|
|
205
|
-
* @import may name its target as a bare string, so both are dropped. Each
|
|
206
|
-
* leaves a space behind, so the text around a removal cannot join into a
|
|
207
|
-
* new one.
|
|
208
|
-
*/
|
|
209
|
-
export function scrubStyleText(text: string) {
|
|
210
|
-
return decodeCssEscapes(text)
|
|
211
|
-
.replaceAll("\\", " ")
|
|
212
|
-
.replace(IMPORT_RULE, " ")
|
|
213
|
-
.replace(OTHER_IMAGE_FUNCTION_GLOBAL, "none")
|
|
214
|
-
.replace(URL_FUNCTION, (match, argument: string) =>
|
|
215
|
-
urlTarget(argument).startsWith("#") ? match : "none",
|
|
216
|
-
);
|
|
217
|
-
}
|