@malloy-publisher/server 0.9.9 → 0.9.11
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/dist/app/api-doc.yaml +48 -41
- package/dist/app/assets/EnvironmentPage-tIGzymgb.js +1 -0
- package/dist/app/assets/HomePage-7QkNvQ_3.js +1 -0
- package/dist/app/assets/MainPage-BpE_5WuW.js +1 -0
- package/dist/app/assets/ModelPage-ftyDI1tw.js +2 -0
- package/dist/app/assets/PackagePage-QGnFwlHu.js +1 -0
- package/dist/app/assets/RenderedResult-Cmx8sX8d.es-du-Qe1x0.js +153 -0
- package/dist/app/assets/{RouteError-ziodz3bt.js → RouteError-zM4SSZaU.js} +1 -1
- package/dist/app/assets/{ThemeEditorPage-Cs3rIs4e.js → ThemeEditorPage-DhZJXGJm.js} +1 -1
- package/dist/app/assets/ToggleButtonGroup-TKxhg0Mg.js +1 -0
- package/dist/app/assets/{documentRoutes-1Qgo5Pvl.js → documentRoutes-D4TbSIi5.js} +1 -1
- package/dist/app/assets/editPageMounted-0kzl2sgg.js +2 -0
- package/dist/app/assets/{index-CXJpj7He.js → index-B-qJVEXn.js} +1 -1
- package/dist/app/assets/{index-Bwc6-9Dc.js → index-Bzi4YH8Y.js} +1 -1
- package/dist/app/assets/{index-CA5a5gJj.js → index-CS4mmuDD.js} +1 -1
- package/dist/app/assets/{index-sKcoT1e_.js → index-DFDVJFPm.js} +32 -32
- package/dist/app/assets/index-gQEuz0Cg.js +480 -0
- package/dist/app/assets/index.es-B3P9tIm3.js +143 -0
- package/dist/app/index.html +1 -1
- package/dist/package_load_worker.mjs +36 -12
- package/dist/server.mjs +782 -653
- package/package.json +1 -1
- package/dist/app/assets/EnvironmentPage-DaNUmaHg.js +0 -1
- package/dist/app/assets/HomePage-CRLoVDIs.js +0 -1
- package/dist/app/assets/MainPage-BsV5UnlK.js +0 -1
- package/dist/app/assets/ModelPage-DOOiyJAE.js +0 -2
- package/dist/app/assets/PackagePage-QHG55rBw.js +0 -1
- package/dist/app/assets/RenderedResult-BHuJpdSn.es-C37LMpwi.js +0 -153
- package/dist/app/assets/ToggleButtonGroup-BrygJ1WP.js +0 -1
- package/dist/app/assets/index-BK0mbcyQ.js +0 -483
- package/dist/app/assets/index.es-CxcPJdYO.js +0 -195
- package/dist/sshcrypto-cqb52edz.node +0 -0
package/dist/server.mjs
CHANGED
|
@@ -262101,11 +262101,6 @@ var require_utils93 = __commonJS((exports, module) => {
|
|
|
262101
262101
|
};
|
|
262102
262102
|
});
|
|
262103
262103
|
|
|
262104
|
-
// ../../node_modules/ssh2/lib/protocol/crypto/build/Release/sshcrypto.node
|
|
262105
|
-
var require_sshcrypto = __commonJS((exports, module) => {
|
|
262106
|
-
module.exports = __require("./sshcrypto-cqb52edz.node");
|
|
262107
|
-
});
|
|
262108
|
-
|
|
262109
262104
|
// ../../node_modules/ssh2/lib/protocol/crypto/poly1305.js
|
|
262110
262105
|
var require_poly1305 = __commonJS((exports, module) => {
|
|
262111
262106
|
var __dirname = "/home/runner/work/publisher/publisher/node_modules/ssh2/lib/protocol/crypto", __filename = "/home/runner/work/publisher/publisher/node_modules/ssh2/lib/protocol/crypto/poly1305.js";
|
|
@@ -262592,7 +262587,7 @@ var require_crypto = __commonJS((exports, module) => {
|
|
|
262592
262587
|
var ChaChaPolyDecipher;
|
|
262593
262588
|
var GenericDecipher;
|
|
262594
262589
|
try {
|
|
262595
|
-
binding =
|
|
262590
|
+
binding = (()=>{throw new Error("Cannot require module "+"./crypto/build/Release/sshcrypto.node");})();
|
|
262596
262591
|
({
|
|
262597
262592
|
AESGCMCipher,
|
|
262598
262593
|
ChaChaPolyCipher,
|
|
@@ -336674,6 +336669,12 @@ function ownLevelNoteTexts(annote) {
|
|
|
336674
336669
|
function ownLevelNotes(annote) {
|
|
336675
336670
|
return [...annote?.blockNotes ?? [], ...annote?.notes ?? []];
|
|
336676
336671
|
}
|
|
336672
|
+
function sameNotePosition(a, b) {
|
|
336673
|
+
return a.url === b.url && a.range.start.line === b.range.start.line && a.range.start.character === b.range.start.character && a.range.end.line === b.range.end.line && a.range.end.character === b.range.end.character;
|
|
336674
|
+
}
|
|
336675
|
+
function notePositionKey(at) {
|
|
336676
|
+
return `${at.url}#${at.range.start.line}:${at.range.start.character}-${at.range.end.line}:${at.range.end.character}`;
|
|
336677
|
+
}
|
|
336677
336678
|
|
|
336678
336679
|
// src/service/notebook.ts
|
|
336679
336680
|
var ARTIFACT_NOTE = {
|
|
@@ -337126,7 +337127,7 @@ ${body}`;
|
|
|
337126
337127
|
if (artifactAt < 0) {
|
|
337127
337128
|
return refuse({
|
|
337128
337129
|
line: 1,
|
|
337129
|
-
message: "Line 1: this notebook has no ## artifact note, so its header cannot be told from its cells. Fix: add `## artifact { layout=linear }` after the ##! line."
|
|
337130
|
+
message: "Line 1: this notebook has no ## artifact note, so its header cannot be told from its cells. Fix: add `## artifact { layout=linear tiles=[…] }` after the ##! line."
|
|
337130
337131
|
});
|
|
337131
337132
|
}
|
|
337132
337133
|
const runCount = items.filter((item) => item.kind === "statement" && item.run).length;
|
|
@@ -337326,12 +337327,14 @@ function readReportTag(modelNotes) {
|
|
|
337326
337327
|
const kind = tagText(artifact, "kind");
|
|
337327
337328
|
if (kind !== undefined)
|
|
337328
337329
|
return { status: "kind-removed", kind };
|
|
337329
|
-
const
|
|
337330
|
-
if (
|
|
337331
|
-
return { status: "
|
|
337332
|
-
|
|
337333
|
-
|
|
337334
|
-
|
|
337330
|
+
const hasTiles = artifact.has("tiles");
|
|
337331
|
+
if (hasTiles && artifact.array("tiles") === undefined)
|
|
337332
|
+
return { status: "unparsed" };
|
|
337333
|
+
const declared = tagText(artifact, "layout");
|
|
337334
|
+
const layout = parseLayoutValue(declared) ?? (hasTiles || tags?.tag("dashboard") !== undefined ? "grid" : "linear");
|
|
337335
|
+
if (layout === "linear" && !hasTiles)
|
|
337336
|
+
return declared !== undefined && parseLayoutValue(declared) === undefined ? { status: "cells-removed", unknownLayout: declared } : { status: "cells-removed" };
|
|
337337
|
+
return { status: "report", layout };
|
|
337335
337338
|
}
|
|
337336
337339
|
function readReportTagOfText(text) {
|
|
337337
337340
|
return readReportTag(modelNotesOfText(text));
|
|
@@ -346035,7 +346038,7 @@ function mergeStatementTimeoutOption(existing, ms) {
|
|
|
346035
346038
|
}
|
|
346036
346039
|
function conninfoPairs(conninfo) {
|
|
346037
346040
|
const pairs = [];
|
|
346038
|
-
const isSpace = (c) => c !== undefined &&
|
|
346041
|
+
const isSpace = (c) => c !== undefined && /[ \t\n\v\f\r]/.test(c);
|
|
346039
346042
|
let i2 = 0;
|
|
346040
346043
|
while (i2 < conninfo.length) {
|
|
346041
346044
|
while (isSpace(conninfo[i2]))
|
|
@@ -346081,7 +346084,66 @@ function decodeUriComponentOrRaw(value) {
|
|
|
346081
346084
|
return value;
|
|
346082
346085
|
}
|
|
346083
346086
|
}
|
|
346084
|
-
|
|
346087
|
+
var LIBPQ_NO_VERIFY_SSLMODE = "require";
|
|
346088
|
+
var isUriConnectionString = (connectionString) => /^postgres(ql)?:\/\//i.test(connectionString);
|
|
346089
|
+
function mapUriQuery(uri, fn) {
|
|
346090
|
+
const queryStart = uri.indexOf("?");
|
|
346091
|
+
const base = queryStart === -1 ? uri : uri.slice(0, queryStart);
|
|
346092
|
+
const params = fn(queryStart === -1 ? [] : uri.slice(queryStart + 1).split("&"));
|
|
346093
|
+
return params.length ? `${base}?${params.join("&")}` : base;
|
|
346094
|
+
}
|
|
346095
|
+
function libpqSslmodeInRawString(connectionString) {
|
|
346096
|
+
const envNoVerify = process.env.PGSSLMODE?.toLowerCase() === "no-verify" && !process.env.PGSERVICE;
|
|
346097
|
+
const rewritten = `sslmode=${LIBPQ_NO_VERIFY_SSLMODE}`;
|
|
346098
|
+
if (isUriConnectionString(connectionString)) {
|
|
346099
|
+
return mapUriQuery(connectionString, (params) => {
|
|
346100
|
+
const keys = params.map((param) => decodeUriComponentOrRaw(param.split("=")[0]));
|
|
346101
|
+
const hasSslmode = keys.includes("sslmode");
|
|
346102
|
+
const hasCert = keys.some((key) => ["sslrootcert", "sslcert", "sslkey"].includes(key));
|
|
346103
|
+
let seen = false;
|
|
346104
|
+
const mapped = params.flatMap((param, i2) => {
|
|
346105
|
+
const [, ...rest] = param.split("=");
|
|
346106
|
+
const value = decodeUriComponentOrRaw(rest.join("="));
|
|
346107
|
+
if (keys[i2] === "service")
|
|
346108
|
+
seen = true;
|
|
346109
|
+
if (keys[i2] === "uselibpqcompat")
|
|
346110
|
+
return [];
|
|
346111
|
+
if (keys[i2] === "ssl") {
|
|
346112
|
+
if (hasSslmode)
|
|
346113
|
+
return [];
|
|
346114
|
+
if (value === "no-verify") {
|
|
346115
|
+
seen = true;
|
|
346116
|
+
return [rewritten];
|
|
346117
|
+
}
|
|
346118
|
+
if (value === "1")
|
|
346119
|
+
return ["ssl=true"];
|
|
346120
|
+
if (value === "0") {
|
|
346121
|
+
if (hasCert)
|
|
346122
|
+
return [];
|
|
346123
|
+
seen = true;
|
|
346124
|
+
return ["sslmode=disable"];
|
|
346125
|
+
}
|
|
346126
|
+
return [param];
|
|
346127
|
+
}
|
|
346128
|
+
if (keys[i2] !== "sslmode")
|
|
346129
|
+
return [param];
|
|
346130
|
+
seen = true;
|
|
346131
|
+
return [value === "no-verify" ? rewritten : param];
|
|
346132
|
+
});
|
|
346133
|
+
return !seen && envNoVerify ? [...mapped, rewritten] : mapped;
|
|
346134
|
+
});
|
|
346135
|
+
}
|
|
346136
|
+
const pairs = conninfoPairs(connectionString);
|
|
346137
|
+
let result = connectionString;
|
|
346138
|
+
for (const pair of [...pairs].reverse()) {
|
|
346139
|
+
if (pair.key === "sslmode" && pair.value === "no-verify") {
|
|
346140
|
+
result = result.slice(0, pair.start) + rewritten + result.slice(pair.end);
|
|
346141
|
+
}
|
|
346142
|
+
}
|
|
346143
|
+
return !pairs.some((pair) => pair.key === "sslmode" || pair.key === "service") && envNoVerify ? `${result} ${rewritten}` : result;
|
|
346144
|
+
}
|
|
346145
|
+
function libpqConnectionString(name, rawConnectionString, pg) {
|
|
346146
|
+
const connectionString = libpqSslmodeInRawString(rawConnectionString);
|
|
346085
346147
|
const ms = postgresStatementTimeoutMs(name, pg);
|
|
346086
346148
|
if (ms === undefined)
|
|
346087
346149
|
return connectionString;
|
|
@@ -346094,21 +346156,21 @@ function withLibpqStatementTimeout(name, connectionString, pg) {
|
|
|
346094
346156
|
}
|
|
346095
346157
|
return value;
|
|
346096
346158
|
};
|
|
346097
|
-
if (
|
|
346098
|
-
const queryStart = connectionString.indexOf("?");
|
|
346099
|
-
if (queryStart === -1) {
|
|
346100
|
-
return `${connectionString}?options=${encodeURIComponent(merged(undefined))}`;
|
|
346101
|
-
}
|
|
346102
|
-
const params = connectionString.slice(queryStart + 1).split("&");
|
|
346159
|
+
if (isUriConnectionString(connectionString)) {
|
|
346103
346160
|
const isOptions = (param) => decodeUriComponentOrRaw(param.split("=", 1)[0]) === "options";
|
|
346104
|
-
|
|
346105
|
-
|
|
346106
|
-
|
|
346107
|
-
|
|
346161
|
+
return mapUriQuery(connectionString, (params) => {
|
|
346162
|
+
const last = params.map(isOptions).lastIndexOf(true);
|
|
346163
|
+
if (last === -1) {
|
|
346164
|
+
return [
|
|
346165
|
+
...params,
|
|
346166
|
+
`options=${encodeURIComponent(merged(undefined))}`
|
|
346167
|
+
];
|
|
346168
|
+
}
|
|
346108
346169
|
const raw = params[last].slice(params[last].indexOf("=") + 1);
|
|
346109
|
-
|
|
346110
|
-
|
|
346111
|
-
|
|
346170
|
+
const out = [...params];
|
|
346171
|
+
out[last] = `options=${encodeURIComponent(merged(decodeUriComponentOrRaw(raw)))}`;
|
|
346172
|
+
return out;
|
|
346173
|
+
});
|
|
346112
346174
|
}
|
|
346113
346175
|
const existing = conninfoPairs(connectionString).filter((pair) => pair.key === "options").pop();
|
|
346114
346176
|
if (!existing) {
|
|
@@ -346121,12 +346183,13 @@ function buildPgConnectionString(pg, {
|
|
|
346121
346183
|
applyConnectionOptions
|
|
346122
346184
|
}) {
|
|
346123
346185
|
if (pg.connectionString) {
|
|
346124
|
-
if (!applyConnectionOptions)
|
|
346125
|
-
return pg.connectionString;
|
|
346186
|
+
if (!applyConnectionOptions) {
|
|
346187
|
+
return libpqSslmodeInRawString(pg.connectionString);
|
|
346188
|
+
}
|
|
346126
346189
|
if (pg.sslmode != null) {
|
|
346127
346190
|
logger.warn(`Connection '${name}' sets both postgresConnection.sslmode and ` + `connectionString; sslmode is ignored and the connectionString's own sslmode applies.`);
|
|
346128
346191
|
}
|
|
346129
|
-
return
|
|
346192
|
+
return libpqConnectionString(name, pg.connectionString, pg);
|
|
346130
346193
|
}
|
|
346131
346194
|
const parts = [];
|
|
346132
346195
|
if (pg.host)
|
|
@@ -346149,7 +346212,7 @@ function buildPgConnectionString(pg, {
|
|
|
346149
346212
|
const pgSSLMode = process.env.PGSSLMODE;
|
|
346150
346213
|
if (pgSSLMode) {
|
|
346151
346214
|
const mapping = {
|
|
346152
|
-
"no-verify":
|
|
346215
|
+
"no-verify": LIBPQ_NO_VERIFY_SSLMODE,
|
|
346153
346216
|
disable: "disable",
|
|
346154
346217
|
allow: "allow",
|
|
346155
346218
|
prefer: "prefer",
|
|
@@ -346518,7 +346581,7 @@ function libpqSslmodeParts(subject, mode, trust) {
|
|
|
346518
346581
|
case "disable":
|
|
346519
346582
|
return ["sslmode=disable"];
|
|
346520
346583
|
case "no-verify":
|
|
346521
|
-
return [
|
|
346584
|
+
return [`sslmode=${LIBPQ_NO_VERIFY_SSLMODE}`];
|
|
346522
346585
|
case "verify-ca": {
|
|
346523
346586
|
if (!trust.caBundle) {
|
|
346524
346587
|
throw new Error(`${subject} uses sslmode 'verify-ca' but no trusted CA bundle is available (NODE_EXTRA_CA_CERTS is unset).`);
|
|
@@ -349367,6 +349430,19 @@ function recordPackageLoadPhases(timings, status) {
|
|
|
349367
349430
|
schemaFetchDuration?.record(timings.schemaFetchDurationMs, attrs);
|
|
349368
349431
|
schemaFetches?.record(timings.schemaFetchCount, attrs);
|
|
349369
349432
|
}
|
|
349433
|
+
var phaseDuration = null;
|
|
349434
|
+
function recordPackageLoadPhaseDurations(durations) {
|
|
349435
|
+
phaseDuration ??= publisherMeter().createHistogram("publisher_package_load_phase_duration_ms", {
|
|
349436
|
+
description: "Per-load wall-clock time of each stage of a successful package load, main-thread stages included. Label: phase (worker, hydrate, render_validation, discovery, lint, build_plan, total).",
|
|
349437
|
+
unit: "ms",
|
|
349438
|
+
advice: { explicitBucketBoundaries: LOAD_DURATION_BUCKETS_MS }
|
|
349439
|
+
});
|
|
349440
|
+
for (const [phase, ms] of Object.entries(durations)) {
|
|
349441
|
+
if (ms === undefined)
|
|
349442
|
+
continue;
|
|
349443
|
+
phaseDuration.record(ms, { phase });
|
|
349444
|
+
}
|
|
349445
|
+
}
|
|
349370
349446
|
|
|
349371
349447
|
// src/report_metrics.ts
|
|
349372
349448
|
var resetHooks2 = [];
|
|
@@ -354723,7 +354799,7 @@ End with a short **Next steps**: one or two specific deeper analyses the data co
|
|
|
354723
354799
|
{
|
|
354724
354800
|
name: "malloy-analysis-report",
|
|
354725
354801
|
description: 'Combine validated Malloy queries into a report (a linear report, read top to bottom). Use when the user asks to "create a report", "combine these into a report", or wants a persistent multi-query artifact.',
|
|
354726
|
-
body: "# Creating Reports\n\nAn ad-hoc report is a Publisher **report** with `layout=linear`: a `.malloy` file at the package root, `<slug>.malloy` (unless the user asks for another location), that combines markdown narrative with live Malloy queries
|
|
354802
|
+
body: "# Creating Reports\n\nAn ad-hoc report is a Publisher **report** with `layout=linear`: a `.malloy` file at the package root, `<slug>.malloy` (unless the user asks for another location), that combines markdown narrative with live Malloy queries, as text tiles and query tiles read top to bottom. Load `skill:malloy-reports` for the full format and authoring rules; this skill covers when to build one and how to design good report content (tiles, chart annotations, narrative structure). Never write a new `.malloynb`, never write a report as bare `run:` statements (that older form is no longer read), and do not create a `notebooks/` or `dashboards/` directory for it.\n\n## Before building a report\n\n1. **Run each query first** via `execute_query` to verify it works and returns expected results.\n2. **Explain the results** to the user as you go: walk through the analysis step by step.\n3. **Then assemble the report** once the analysis is validated.\n\nDo NOT build the report in the same turn as `execute_query`. Explain first, then build.\n\n## Filters belong to the report, never to the source\n\nAn ad-hoc report usually needs no filters: it records an analysis already run. If the user wants one a reader can change (a manufacturer, a date range), declare the given in the report and bind it on its tiles, as `skill:malloy-reports` describes. Do not add a `given:` to the source for it: a model-level `where:` filters every read of the source, chat and every other report included, and the report builder can show such a filter but not change or remove it. A given the source already reads (a gate, a value every query must supply) shows in the report with no work, and stays in the model.\n\nNever add a `#(filter)` annotation: it is deprecated, and every use has a `given:` form (`malloy-model` covers the migration).\n\n## What goes in the report\n\nDo NOT add an H1 heading in any markdown (use H2 and below for sections); the `title` in the `## artifact` tag serves as the title. To redo the structure rather than tweak one tile, rewrite the report file end-to-end.\n\nText tiles own narrative; query tiles own a single Malloy query whose chart annotation tells the renderer how to display the result. Markdown supports H2 headings, lists, bold, and inline code. Keep text tiles short, one idea per tile, so the rendered output reads as a story instead of a wall of text.\n\nThe file has four parts, in this order:\n\n1. **The tag**, `##| artifact { layout=linear title=\"...\" tiles=[…] }`, listing every tile in reading order. A text tile is `name { kind=text }`; a query tile is the quoted string `\"<extension> -> <view>\"`.\n2. **The `import`** of the model file, relative to the report's own location (`\"order_analysis.malloy\"` from the package root).\n3. **The text tiles**, each a `##|(markdown) name` block: the name on the opener line, the body on the lines below, `|##` closing it.\n4. **One extension of the source**, `source: <source>_tiles is <source> extend { … }`, holding a `view:` per query tile. The view's chart tag and `# label=\"…\"` (the tile's heading) go directly above it.\n\nCompile the file with `/compile` (`\"scope\": \"file\"`, at the path `<slug>.malloy`) before saving; a report compiles as a model, so its errors come back there. After saving and reloading, read the package warnings: a tile that names a missing view is reported there, not by the compile. A complete report:\n\n```malloy\n##| artifact { layout=linear title=\"Sales report\"\n tiles=[\n overview { kind=text },\n \"order_analysis_tiles -> headline\",\n trend { kind=text },\n \"order_analysis_tiles -> revenue_by_month\",\n breakdown { kind=text },\n \"order_analysis_tiles -> top_categories\",\n takeaways { kind=text }\n ]\n}\n|##\nimport \"order_analysis.malloy\"\n\n##|(markdown) overview\n## Overview\nWhat is driving sales, and which categories carry it? The tiles below cover every order in the model.\n|##\n\n##|(markdown) trend\n## Trend\nHow does revenue move over time?\n|##\n\n##|(markdown) breakdown\n## Breakdown\nWhich categories account for the most revenue?\n|##\n\n##|(markdown) takeaways\n## Key takeaways\nWhat to look at next.\n|##\n\nsource: order_analysis_tiles is order_analysis extend {\n # big_value\n # label=\"Headline\"\n view: headline is {\n aggregate:\n # label=\"Revenue\"\n # currency\n total_revenue\n\n # label=\"Orders\"\n # number=auto\n order_count\n }\n\n # line_chart\n # label=\"Revenue by month\"\n view: revenue_by_month is {\n group_by: order_date.month\n aggregate: total_revenue\n order_by: 1\n }\n\n # bar_chart\n # label=\"Top ten categories by revenue\"\n view: top_categories is {\n group_by: category\n aggregate: total_revenue\n order_by: total_revenue desc\n limit: 10\n }\n}\n```\n\nWhen the model already has the view you want, the tile view refines it rather than repeating it: `view: top_categories is by_category + { limit: 10 }`.\n\nA well-structured report typically follows this pattern:\n\n```\n[Text] ## Overview: what question are we answering, what data is in scope (date range, entity count)\n[Query] KPI tile: headline numbers (e.g., # big_value, or # dashboard with nested # big_value views)\n[Text] ## Trend: describe what we should look for over time\n[Query] Time-series tile (e.g., # line_chart on a date dimension)\n[Text] ## Breakdown: where the signal is\n[Query] Categorical tile (e.g., # bar_chart on a categorical dimension)\n[Text] ## Key takeaways: what the user should walk away with\n```\n\nUse this as a default; deviate when the analysis warrants. A grounded report names the time range and entity count up front so every number that follows has context.\n\n## Choosing chart types and annotations\n\nRead `skill:malloy-charts` before picking visualizations: it owns chart-type selection, properties, and the placement rules for chart annotations. `skill:malloy-queries` covers Malloy query patterns and the critical placement rules for chart-annotation tags.\n\nWhen in doubt:\n\n- KPIs / single numbers -> `# big_value`, often nested inside `# dashboard`.\n- Trend over time -> `# line_chart`, usually on the primary date dimension.\n- Category comparisons -> `# bar_chart`, ordered by the metric.\n- Tabular data with many columns -> a plain table tile with `# table.size=fill`.\n- Multiple coordinated charts -> `# dashboard` with `nest:` blocks.\n\nAnnotations go **directly above** the tile's `view:`, never inside its curly braces:\n\n```malloy\n# bar_chart\nview: top_categories is {\n group_by: category\n aggregate: revenue\n order_by: revenue desc\n limit: 10\n}\n```\n\nA `# dashboard` tile composes nested views, useful for KPIs alongside a trend in a single tile. Each `nest:` is a tile; any top-level `aggregate:` measures render as KPI cards. For a fixed grid, use `# dashboard { columns=N }` with `# colspan` on each tile (see `skill:malloy-charts`):\n\n```malloy\n# dashboard { columns=2 }\nview: summary is {\n nest:\n # colspan=2\n # big_value\n kpis is {\n aggregate:\n # label=\"Revenue\"\n # currency\n total_revenue\n\n # label=\"Orders\"\n # number=auto\n order_count\n }\n nest:\n # line_chart\n trend is {\n group_by: order_date.month\n aggregate: total_revenue\n order_by: 1\n }\n}\n```\n\nKey rendering rules to keep in mind when shaping a tile:\n\n- FIRST `group_by` = x-axis, FIRST `aggregate` = y-axis.\n- Override field roles with `# x`, `# y`, `# series` on individual fields.\n- For multiple measure series, place `# y` above the `aggregate:` keyword.\n- One aggregate per chart view: use `# dashboard` with nested views for multiple charts.\n- Use `# table.size=fill` for standalone table queries.\n\n## Editing an existing report\n\nFor small targeted changes (fix one view, insert one tile), edit that view or text block in place and add its entry to `tiles=[…]`, rather than recreating the whole report. For structural rewrites (reordering many tiles, changing the narrative arc), rewrite the report file. An existing `.malloynb` is read, not edited: to change its story, write a `.malloy` report. A tag that sets `kind=notebook` is no longer read, so the file is not a report (`report-kind-removed`): replace it with `layout=linear` and list the tiles. A linear file with no `tiles=[…]`, written as bare `run:` statements, is no longer a report: rewrite it as tiles (`skill:malloy-reports`, `reference/linear.md`).\n\n## IMPORTANT\n\nYou CANNOT see the rendered output of report tiles. Do not claim to see charts, values, or patterns from report tiles you haven't explicitly executed via `execute_query`. If you need to analyze results, run the query via `execute_query` first."
|
|
354727
354803
|
},
|
|
354728
354804
|
{
|
|
354729
354805
|
name: "malloy-charts",
|
|
@@ -354908,7 +354984,7 @@ The user will:
|
|
|
354908
354984
|
- **Remove** fields they don't need.
|
|
354909
354985
|
- **Change** priorities.
|
|
354910
354986
|
|
|
354911
|
-
Once the definitions are confirmed, write them into the \`.malloy\` model (see your modeling workflow). Use \`#(doc)\` annotations to document sources and fields, and \`given:\`
|
|
354987
|
+
Once the definitions are confirmed, write them into the \`.malloy\` model (see your modeling workflow). Use \`#(doc)\` annotations to document sources and fields, and a \`given:\` only where the data's meaning or access depends on a runtime value (a gate, a value every query must supply). A filter a reader picks on a dashboard belongs to the report, not the model. \`#(filter)\` is deprecated. Never add a \`#(filter)\` annotation: every use, including \`required\`, \`implicit\`, and date/number ranges, has a \`given:\` form. See \`skill:malloy-model\` § Parameterizing sources with \`given:\`. Keep the confirmed definitions in the conversation; there is no separate plan-file store.
|
|
354912
354988
|
|
|
354913
354989
|
## Data-driven proposals
|
|
354914
354990
|
|
|
@@ -357446,31 +357522,39 @@ gets unwieldy.
|
|
|
357446
357522
|
|
|
357447
357523
|
## Parameterizing sources with \`given:\` (preferred)
|
|
357448
357524
|
|
|
357449
|
-
Native Malloy **\`given:\` parameters** are
|
|
357525
|
+
Native Malloy **\`given:\` parameters** are runtime values a model reads in its own logic; callers supply them at query time. \`#(filter)\` is deprecated: never add one. Enable givens with \`##! experimental.givens\` at the top of the model.
|
|
357526
|
+
|
|
357527
|
+
**A given belongs in the model only when the data's meaning or access depends on it:**
|
|
357528
|
+
|
|
357529
|
+
- a row-level gate: \`#(authorize)\`, \`#(access_filter)\`, a \`#(secure)\` given, \`$GROUPS\`;
|
|
357530
|
+
- a value every query must supply (the no-default row in the table below);
|
|
357531
|
+
- a parameter a measure, dimension or view is defined with;
|
|
357532
|
+
- scoping the author means to apply to every consumer of the source: chat, MCP and every report.
|
|
357533
|
+
|
|
357534
|
+
**A filter a reader picks on a dashboard or notebook is not modeling.** Manufacturer, a date range, region, a "major recalls only" toggle: don't add it to a source. Leave the source unfiltered, and let the report declare the given and bind it on its tiles (\`skill:malloy-reports\`). A model-level \`where:\` applies to every read of the source, chat and every other report included, and a report cannot take it over, because binding is per declaration, not per name. The report builder shows such a filter but cannot remove or reconfigure it.
|
|
357450
357535
|
|
|
357451
357536
|
\`\`\`malloy
|
|
357452
357537
|
##! experimental.givens
|
|
357453
357538
|
|
|
357454
|
-
|
|
357455
|
-
|
|
357456
|
-
subject_filter :: filter<string> is f''
|
|
357539
|
+
# A value every query must supply: the table is partitioned by day.
|
|
357540
|
+
given: EVENT_DATE :: date
|
|
357457
357541
|
|
|
357458
|
-
source:
|
|
357459
|
-
where:
|
|
357460
|
-
measure:
|
|
357542
|
+
source: events is duckdb.table('data/events.parquet') extend {
|
|
357543
|
+
where: event_date = $EVENT_DATE
|
|
357544
|
+
measure: event_count is count()
|
|
357461
357545
|
}
|
|
357462
357546
|
\`\`\`
|
|
357463
357547
|
|
|
357464
|
-
A given is **declared bare** but **referenced with a \`$\` sigil** in expressions (\`$
|
|
357548
|
+
A given is **declared bare** but **referenced with a \`$\` sigil** in expressions (\`$EVENT_DATE\`), as above.
|
|
357465
357549
|
|
|
357466
|
-
- **
|
|
357550
|
+
- **An optional given in the model gets a neutral, match-all default** (a \`filter<>\` given defaulting to \`f''\`), so an unsupplied value returns unfiltered rows. Because it bakes an always-on \`where:\` into the source, a non-neutral default (a date floor, say) applies to *every* read of the source. Defaults must be Malloy literals.
|
|
357467
357551
|
- **Givens don't auto-inject a \`where:\`.** Unlike \`#(filter)\`, you write the filter expression that references the given yourself (e.g. \`where: dimension ~ $given_name\`).
|
|
357468
357552
|
- **Every \`#(filter)\` use has a \`given:\` form.** Never add a \`#(filter)\` annotation to a model, not even for \`required\`, \`implicit\`, or a date/number range:
|
|
357469
357553
|
|
|
357470
357554
|
| You want | Write this | Not this |
|
|
357471
357555
|
|---|---|---|
|
|
357472
|
-
| An optional value or list filter | \`given: REGION :: filter<string> is f''\`
|
|
357473
|
-
| A date or number range | \`given: MIN_SALE :: filter<number> is f''\` (or \`filter<date>\`)
|
|
357556
|
+
| An optional value or list filter a reader picks | In the **report**, not the model: \`given: REGION :: filter<string> is f''\` there, bound on its tiles with \`+ { where: region ~ $REGION }\` (\`skill:malloy-reports\`). In the model only for one of the cases above. | \`#(filter) dimension=region type=in\` |
|
|
357557
|
+
| A date or number range a reader picks | In the **report** the same way: \`given: MIN_SALE :: filter<number> is f''\` (or \`filter<date>\`), bound with \`+ { where: sale_price ~ $MIN_SALE }\`. The caller sends a filter expression such as \`>= 50\`. | two \`#(filter)\` lines, \`greater_than\` and \`less_than\` |
|
|
357474
357558
|
| A value every query must supply (the primary key is only unique within it, or the table is too big to scan whole) | a given with **no default**, e.g. \`given: EVENT_DATE :: date\` and \`where: event_date = $EVENT_DATE\`. A query that omits it fails with "Given 'EVENT_DATE' has no value and no default". | \`#(filter) ... required\` |
|
|
357475
357559
|
| A row filter the system applies, not the caller | \`#(access_filter) org_id in $ORG_IDS\`, with \`ORG_IDS\` set by a trusted tier (see the trust caveat below) | \`#(filter) ... implicit\` |
|
|
357476
357560
|
|
|
@@ -357500,7 +357584,7 @@ Publisher formats the value from the dimension's type (\`'value'\`, bare \`true\
|
|
|
357500
357584
|
|
|
357501
357585
|
\`#(filter)\` is not a security boundary. A caller skips it with \`bypass_filters=true\` (REST) or \`bypassFilters: true\` (POST body), or by writing their own query text. Use givens with \`#(authorize)\` / \`#(access_filter)\` for access control.
|
|
357502
357586
|
|
|
357503
|
-
**To migrate** a source, replace each annotation using the table above, then delete it. \`type=greater_than\` and \`type=less_than\` are exclusive, so a plain comparison you convert one to is \`>\` or \`<\`, not \`>=\` or \`<=\`. \`docs/givens.md\` § "Coming from \`#(filter)\`" has a worked conversion.
|
|
357587
|
+
**To migrate** a source, replace each annotation using the table above, then delete it. An optional filter (\`type=in\`, \`like\`, \`equal\`, a range) moves to the reports that offer it, declared and bound there, and the source is left unfiltered; a \`required\` one stays in the model as a no-default given. \`type=greater_than\` and \`type=less_than\` are exclusive, so a plain comparison you convert one to is \`>\` or \`<\`, not \`>=\` or \`<=\`. \`docs/givens.md\` § "Coming from \`#(filter)\`" has a worked conversion.
|
|
357504
357588
|
|
|
357505
357589
|
## Access Control: \`#(authorize)\` and \`#(access_filter)\`
|
|
357506
357590
|
|
|
@@ -357558,7 +357642,7 @@ source: orders_admin is orders_base extend {}
|
|
|
357558
357642
|
- **Both only gate from the \`source:\` line.** The same annotation on a \`dimension:\`/\`measure:\`/\`join_*:\`/\`view:\` line, or on a top-level \`query:\`, is refused at load naming the position rather than silently protecting nothing.
|
|
357559
357643
|
- **Every given the gate references must be declared on the entry model's own surface, and must carry no default.** In a package curated with \`index.malloy\`, "on the surface" means \`index.malloy\` imports it: import the declaring file whole, or name the given in a selective import (\`import { orders, GROUPS } from "orders.malloy"\`). A given the model cannot resolve is refused at load. So is a referenced given declared *with* a default: a caller who supplies nothing would get that default and be admitted or excluded by a value the gate's own line never shows, so it is refused rather than reasoned about case by case.
|
|
357560
357644
|
- **A scalar/array mismatch between the operator and the given's declared type is a load-time refusal**, not a request-time warehouse error: \`org_id in $GROUPS\` requires \`GROUPS\` to be array-typed, \`region = $REGION\` requires \`REGION\` scalar. Negation is likewise refused outright, so there is no empty-given inversion surprise to warn about.
|
|
357561
|
-
- **Entry point only: not joined, but inherited through \`extend\`.** The gate applies to the source a query enters through. A gate on a source reached only via \`join_*\` **never fires**, at any depth, so anything ungated that joins a locked base hands the base's rows to every caller. A source that \`extend\`s a locked base and declares no gate of its own **does** carry the base's gate; declaring its own annotation replaces it. A source derived from a locked base via a query (\`source: z is locked -> { … }\`) instead **always carries the base's gate in addition to its own**: the derivation recurses into the base unconditionally, so an own gate does not replace it, and the two combine as separate AND'd entries. Pair a locked base with curated extension sources, using access modifiers (\`include { public: …, private: * }\`), so an extension re-exposes only a curated column surface, and keep sensitive sources out of ungated joins.
|
|
357645
|
+
- **Entry point only: not joined, but inherited through \`extend\`.** The gate applies to the source a query enters through. A gate on a source reached only via \`join_*\` **never fires**, at any depth, so anything ungated that joins a locked base hands the base's rows to every caller. A source that \`extend\`s a locked base and declares no gate of its own **does** carry the base's gate; declaring its own annotation replaces it. A source derived from a locked base via a query (\`source: z is locked -> { … }\`) instead **always carries the base's gate in addition to its own**: the derivation recurses into the base unconditionally, so an own gate does not replace it, and the two combine as separate AND'd entries. Pair a locked base with curated extension sources, using access modifiers (\`include { public: …, private: * }\`), so an extension re-exposes only a curated column surface, and keep sensitive sources out of ungated joins. A join the *caller* writes in query text is the caller entering the joined source, so that source's gate does apply to it: a lock is decided, a row filter is grafted into the join. That holds for a query source the model declares over an inline \`table(...) extend { … }\` base as much as for a table source: the join anchors on the declared source, and the derivation carries the base's gate. The author-join side has a contract of its own: when a gated entry source author-joins a tenant-scoped source, carry the tenant key in the \`on\` (\`on list_id = values.list_id and org_id = values.org_id\`), because the joined source's gate does not fire through that join and the ON is what keeps the rows to the caller's tenant. Where a join cannot be keyed that way, the entry source's own gate may reference the joined column (\`#(access_filter) values.org_id = $ORG_ID\`): that is enforced, at the cost of turning that \`join_one\` into an inner join, so a row with no match drops rather than surviving with nulls.
|
|
357562
357646
|
- **A derivation that drops a column the gate reads fails CLOSED.** \`extend { except: org_id }\`, or an \`accept:\` that omits it, leaves the grafted filter unable to compile, so the request is denied rather than served ungated. The one hole to know: dropping the gated column and then \`rename:\`-ing a *different* column onto that exact name grafts successfully and binds the gate to the wrong column. Narrow, but real, so don't recycle a gated column's name. If a derivation's own projection needs to drop the column a row-level term would read, use a source-level term (\`'literal' in/= $GIVEN\`) instead, since it never depends on any projected column.
|
|
357563
357647
|
- **The quoted-string and file-level forms are refused at load and no longer exist.** A quoted expression on the \`source:\` line, in either quote, a file-level \`##(…) "<expr>"\` applying to every source in the file, and the earlier \`internal dimension: authorized is <expr>\` form are all retired; the load names the rewrite. Every \`.malloy\` file in a package compiles at load and any failure aborts the package, so a retired-form gate anywhere in the package is refused. Only a declaring file *outside* the package escapes that: it loads and denies every request instead, with no compile-time hint. See your deployment's reference documentation.
|
|
357564
357648
|
- **A gated source can be persisted, but the gating column freezes.** \`storage=\` and \`#@ preaggregate\` refuse a gated source outright; a colocated \`#@ persist\` is admitted when the gate is provably the entry point's own row filter. The gate still runs live on every query, so rows come back filtered - but the column it filters ON is frozen at build time, so a row whose access decision changes keeps being served under the old decision until the next rebuild. Pair \`#@ persist\` on a gated source with a freshness window (\`fallback="live"\`), which is the only control that bounds that - and read \`skill:malloy-materialization\` for where that window binds, because on a standalone Publisher it does not.
|
|
@@ -357931,7 +358015,7 @@ When you've been doing analysis, writing queries, building views, creating repor
|
|
|
357931
358015
|
|
|
357932
358016
|
## Step 1: IDENTIFY, what belongs in a model?
|
|
357933
358017
|
|
|
357934
|
-
Scan your analysis \`.malloy\` files and reports (any \`.malloy\` with an \`## artifact\` tag, wherever it sits, or an existing \`.malloynb\`). A report (a \`tiles=[…]\` layout
|
|
358018
|
+
Scan your analysis \`.malloy\` files and reports (any \`.malloy\` with an \`## artifact\` tag, wherever it sits, or an existing \`.malloynb\`). A report (a \`tiles=[…]\` layout, of either layout) is not a model file to restructure: lift its reusable definitions into a model and leave the report to import them. For each dimension, measure, join, or calculation, ask: **is this a reusable building block?**
|
|
357935
358019
|
|
|
357936
358020
|
A measure or dimension belongs in a model if it meets ANY of these criteria:
|
|
357937
358021
|
|
|
@@ -360350,7 +360434,7 @@ A flat layout at the package root is the simplest default, for models and report
|
|
|
360350
360434
|
|
|
360351
360435
|
Publishable contents:
|
|
360352
360436
|
- \`.malloy\` files - Semantic model definitions (base sources + joined sources)
|
|
360353
|
-
- Reports: \`.malloy\` files with an \`## artifact { layout=linear … }\` or \`## artifact { layout=grid … }\` tag, anywhere in the package (outside \`public/\`), written as tiles
|
|
360437
|
+
- Reports: \`.malloy\` files with an \`## artifact { layout=linear … }\` or \`## artifact { layout=grid … }\` tag, anywhere in the package (outside \`public/\`), written as tiles (see \`skill:malloy-reports\`). A linear tag with no tiles, the retired \`run:\`-cell form, is no report. The tag alone makes a file a report and sets its layout; the folder means nothing. An existing \`.malloynb\` is still served as a linear report; never write a new one.
|
|
360354
360438
|
- Data files (CSV/Parquet/XLSX) - Embedded data published with package
|
|
360355
360439
|
|
|
360356
360440
|
## Version Management
|
|
@@ -360467,7 +360551,7 @@ run: source -> {
|
|
|
360467
360551
|
|
|
360468
360552
|
## Field Paths and Joins
|
|
360469
360553
|
|
|
360470
|
-
**Use the joins the model declares.** When you answer a question over a published model, every query is rooted on one source, and you reach a joined source's fields by a dotted path within the query body (for example \`stores.region\`). Do not write \`join_one\` or \`join_many\` in an answer query, and never invent a join key. (A join written in a query does compile, and model files and notebooks use them; the rule is that an answer uses the joins the model declares.) If the field you need is on a source the model does not join, say so and ask the user to add the join to the model.
|
|
360554
|
+
**Use the joins the model declares.** When you answer a question over a published model, every query is rooted on one source, and you reach a joined source's fields by a dotted path within the query body (for example \`stores.region\`). Do not write \`join_one\` or \`join_many\` in an answer query, and never invent a join key. (A join written in a query does compile, and model files and notebooks use them; the rule is that an answer uses the joins the model declares.) If the field you need is on a source the model does not join, say so and ask the user to add the join to the model. If you do write a join in query text, it is gated as if you had entered that source yourself: a locked source refuses the query with a 403 that names your alias, and a row-filtered source filters the rows your join sees, so a denial on a join you wrote means the joined source refused you, not that the model is broken.
|
|
360471
360555
|
|
|
360472
360556
|
The \`->\` operator separates a **source** from a **view** (query transformation). It does NOT navigate between joined sources.
|
|
360473
360557
|
|
|
@@ -360719,7 +360803,7 @@ For anything not covered here, call \`search_malloy_docs\` with the topic (for e
|
|
|
360719
360803
|
{
|
|
360720
360804
|
name: "malloy-reports",
|
|
360721
360805
|
description: "Build or modify a Malloy Publisher report: a .malloy file with an ## artifact tag, written at the package root unless asked otherwise, laid out as a grid (a dashboard, scanned at a glance) or linear (a notebook or data story, read top to bottom), with filter controls rendered from givens and # drill click-through. Use when the user asks for a dashboard, a notebook, a report, a data story, a filterable view, or drill-through between views, and no code is wanted. Legacy .malloynb files are read, not authored.",
|
|
360722
|
-
body: "# Publisher Reports\n\n> A `.malloy` file with an `## artifact { layout=… tiles=[…] }` tag **is** a report. It imports the model, names the views to show, binds the filtering on those views, and says how to lay them out. Publisher discovers it at package load, renders the filter controls from the givens its tiles reference, offers `# drill` click-through, and serves it at `/<env>/<pkg>/<path>`, its package path without `.malloy`. No code, no build step.\n\nWhat other tools call a dashboard and a notebook are both reports here. The **layout** is the only difference:\n\n- **`layout=grid`**: tiles across a grid behind one filter row, scanned at a glance (a dashboard). Read `reference/grid.md` before you lay one out.\n- **`layout=linear`**: tiles and prose top to bottom, read in order (a notebook, a data story). Read `reference/linear.md` before you write one, and before you edit a report written as `run:` cells.\n\nEverything else (filters, text tiles, drill, the builder, the lint, the write path) is the same for both, and switching layout later is a one-property edit.\n\n## Where the file goes\n\n- **Write a new report at the package root**, as `<slug>.malloy`, unless the author asks for another location. No directory is required, and none is a convention: a report can live anywhere in the package except `public/`, and the root `index.malloy` is never a report. Do not create a `dashboards/` or `notebooks/` directory for it.\n- **Always write `layout=grid` or `layout=linear`** in the tag. A tag without one is read by a fallback rule, and a linear report that lists tiles without `layout=linear` is read as a grid. `kind=` is no longer read: a tag that sets it makes no report (`report-kind-removed`), so replace it with `layout=`.\n- **The package path is the report's identity.** `sales/q3.malloy` is the report `sales/q3`: its URL, its `GET …/reports/{path}` address, and its `# drill` target. Two reports may share a basename in different directories.\n- **Imports are relative to the report's own location.** At the root: `import { order_items } from \"storefront.malloy\"`. In a subdirectory: `\"../storefront.malloy\"`.\n- **An untagged `.malloy` file is an ordinary model**, wherever it lives. Shared definitions several reports import go in one.\n\n## When this is the right tool\n\n| The user wants | Use |\n| ---------------------------------------------------- | ------------------------------------------------------------ |\n| A recurring, at-a-glance view behind shared filters | this skill, `layout=grid` |\n| A narrative, with prose between the numbers | this skill, `layout=linear` |\n| A quick write-up of queries you already ran | `skill:malloy-analysis-report`, which writes a linear report |\n| Custom design, branding, or interactions beyond tags | an HTML data app |\n| The model itself: sources, measures, joins | `malloy-model` |\n\nInteractivity is not the axis: both layouts get filter controls, URL-addressable state, Apply batching, and `# drill`. Pick on the shape of the document: scanned at a glance is grid, read top to bottom is linear. When in doubt, linear.\n\n## Build sequence\n\nTool names below are bare (`get_context`); match each against the tools you actually have, since a host may prefix or rename them. A host names its compile and reload tools its own way. Where you have none, use the REST endpoint named beside the step.\n\n1. **READ THE MODEL FIRST.** Get the real source, view, dimension, and given names from the package: `get_context` if you have it, otherwise the REST model endpoint for `index.malloy` (it answers 404 for a file off the surface) or the `.malloy` files. Never guess a name. A guessed field in a query fails the whole package load, not just that one report; a guessed tile or suggest source is quieter, and only shows up in the package warnings. If the package root holds an `index.malloy` (every scaffolded package does), a tile reads only the sources that file exports.\n2. **PICK THE LAYOUT AND THE TILES.** Which views, in what order, and on a grid how wide each sits. A query tile is a `view:` you declare in the report, named as `\"source -> view\"`.\n3. **DECLARE THE GIVENS** the report will filter by, in the report file itself, with their control tags (see \"Filter controls\"). The builder edits the report file and nothing else, so a filter it can add, change or remove is a declaration in that file. A given the model already declares and reads is imported by name, never declared again (see \"Givens that stay in the model\").\n4. **COMPOSE THE FILE** at its path (the package root unless asked otherwise), following the template below, but do not save it yet. Name the sources you need in the import. Each tile binds its controls with a refinement, `view: t is v + { where: category ~ $CATEGORY }`, one clause per given it answers to; a `filter<...>` binds with `~`, a plain `date` or `number` given is a value and binds with `>=`, `<=` or `=`. Only the givens some tile references become controls. Then import every source or query any referenced given names in a `suggest`; **a `suggest` naming a `query=` needs that query's own source imported as well**, because an import is not transitive.\n5. **COMPILE IT** with your compile tool (`POST …/models/<path>/compile` over REST) at the path the file will have, with **`\"scope\": \"file\"`**: a report opens with an `import`, which the default `append` scope refuses, and an existing file compiled at `append` collides with itself. **`\"scope\": \"file\"` does not check tiles, drills or suggest queries.** To see those before you save, compile the same text at `\"scope\": \"package\"` with `modelPath` set to the report's path: it runs the lint step 6 reads and returns its findings with code `dashboard-lint` (or `render-tag`). Editing an untagged model several reports import also wants `\"scope\": \"package\"`, which recompiles every file as saved.\n6. **SAVE IT, RELOAD, AND READ THE MANIFEST AND THE WARNINGS.** Save by writing the file, or with `PUT …/models/<path>` over REST (omit `expectedHash` to create; it creates at any new path and replaces only a file the package already serves as a report). Then your reload tool, or `GET …/packages/<pkg>?reload=true`. A 424 means the package did not load and your edit is not live. **The `warnings` key is absent when there are none.** Then read `GET …/packages/<pkg>/reports/<path>`: its `givens` are exactly the controls that will render, and its `layout` is the layout Publisher read. See \"Read the lint\".\n7. **OPEN IT AND LOOK.** Not optional; see \"What done means\".\n\n## The form\n\n```malloy\n##! experimental.givens\n##\" One line saying what this report is for.\n##| artifact { layout=grid title=\"Storefront overview\"\n tiles=[\n intro { kind=text colspan=12 },\n \"overview -> kpis\",\n \"overview -> revenue_trend\",\n \"overview -> revenue_by_state\"\n ]\n} dashboard { columns=12 }\n|##\nimport { order_items, products } from \"storefront.malloy\"\n\n# label=\"Category\" control=select suggest { source=products dimension=category }\ngiven: CATEGORY :: filter<string> is f''\n# label=\"Ordered since\"\ngiven: SINCE :: date is @2023-01-01\n\n##|(markdown) intro\nRevenue and margin for the selected category.\n|##\n\nsource: overview is order_items extend {\n # colspan=12\n # label=\"Key figures\"\n # big_value\n view: kpis is key_figures + { where: category ~ $CATEGORY, where: created_at >= $SINCE }\n\n # colspan=8\n # break\n # label=\"Revenue by month\"\n view: revenue_trend is sales_by_month + { where: category ~ $CATEGORY, where: created_at >= $SINCE }\n\n # colspan=4\n # label=\"Revenue by state\"\n view: revenue_by_state is sales_by_state + { where: category ~ $CATEGORY, where: created_at >= $SINCE }\n}\n```\n\nThe same file with `layout=linear` and no `dashboard { … }` or `colspan` is a linear report.\n\n- **The tag is model-level**, one line (`## artifact { … }`) or a `##|` … `|##` block with the same text inside, the form to use once the list is long: one tile per line, `}` on its own line, `|##` closing at the opener's column. A one-line `##` tag must stay on one line: wrapping it is a compile error, or worse, compiles clean and quietly stops being a report.\n- **The header** above the tag holds only `##!` flags, `//` comments and the unnamed `\"` notes that are the report's description. A statement or a `#` tag above `## artifact` is an error.\n- **Quote every query entry.** `\"overview -> kpis\"` is a string; unquoted, the tag does not parse (`notebook-artifact-unparsed`). A bare quoted name (`\"top_products\"`) names a model-level `query:` in the file.\n- **A query tile is a thin `view:` on an extension the report declares** (`source: overview is order_items extend { … }`): the modelled view keeps its chart tag, the wrapper carries the tile's `# label`, its width on a grid, and its binding.\n- **A text tile is a `##|(markdown) name` block** listed as `name { kind=text }`. The name is one bare word on the opener line, the body starts on the next line (a heading goes inside the block), and `|##` closes it at the opener's column. A bare `intro` without `{ kind=text }` is the query-tile form for a `query:`, so the block is not shown. On a grid the entry also takes `colspan` and `break`, and nothing else.\n- **File order:** header, imports and givens, the prose blocks in tile order, then the extension that defines the tile views. That is the order the builder writes.\n- **A `run:` in a report that lists `tiles` is never shown** (`notebook-layout-run`). Define a view, list it, delete the `run:`.\n- **A tile expression is a string, so the compiler never checks it.** Rename a view and the file still compiles; the tile fails at package load, where the lint names it.\n- **Chart tags go on the view** (`# line_chart`, `# bar_chart`, `# big_value`, `# label=\"…\"`). `skill:malloy-charts` covers choosing them.\n\n## Filter controls\n\nControls come from the `given:` declarations the tiles reference. The tags on the declaration are the control contract:\n\n```malloy\n##! experimental.givens\n\n# label=\"Category\" control=select suggest { source=products dimension=category }\ngiven: CATEGORY :: filter<string> is f''\n\n# label=\"Brand\" control=multiselect suggest { query=brand_suggest dimension=brand }\ngiven: BRAND :: filter<string> is f''\n\n# label=\"Minimum line total\" range_min=0 range_max=250\ngiven: MIN_SALE :: filter<number> is f''\n\n# label=\"Ordered since\"\ngiven: SINCE :: date is @2023-01-01\n```\n\n`control=select`/`multiselect` with a `suggest` renders a picker filled from the data; `range_min`/`range_max` on a `filter<number>` renders a two-handled range slider; a `filter<date>` or `filter<timestamp>` renders a time-range control with preset windows and a custom day range; a bare `date` or `timestamp` renders a date picker; a `filter<string>` with no tag renders a text box taking filter syntax. Which controls appear is per report, decided by which givens its tiles reference.\n\n- `#(description=\"…\")` renders as helper text under the control and draws a cosmetic \"not a well-formed route\" compile diagnostic for a multi-word value; `# description=\"…\"` publishes to the API but the UI does not render it.\n- `dimension=` can name a field of a directly joined source, quoted: `suggest { source=sales dimension=\"products.department\" }`. On the `query=` form, `dimension` names a column of that query's output; match it to the query's `group_by`.\n- **A suggest's source or query has to resolve in the report file**, so import it there. Getting it wrong does not error: the control still looks like a picker and says \"Could not load the options for this control\"; the package warnings name it.\n- Two options on the artifact tag, either layout: `autorun=false` batches control changes behind an Apply button; `givens { CATEGORY=f'Outerwear' }` sets starting values (a URL parameter wins). A file-level `## autorun=false` does nothing on a `.malloy` report.\n\n### Binding, and givens that stay in the model\n\n**Binding is per declaration, not per name.** A report declaring its own `CATEGORY` over a source whose model-level `where:` reads the model's `CATEGORY` compiles, shows the control, and filters nothing. So bind on the tiles, with `+ { where: … }`. The other design, model-level scoping, still works: put a scoped source in an untagged model that imports the givens it reads, and import that model and those givens into the report. Do not mix the two on one given.\n\nA given stays in the model, declared once there, when a model source, view or measure, an `#(authorize)` or `#(access_filter)` gate, or an HTML data app reads it. The report imports it by name (`import { CATEGORY } from \"givens.malloy\"`) and never re-declares it:\n\n- **Import a name and also declare it: a compile error.** Drop the local declaration.\n- **Declare a name the model already reads, without importing it: no error at all.** The control renders and moves, and the model's own `where:` or gate never sees the value.\n\nA given has to be in the report file's own scope to be bindable: declared there or imported. Never add a `#(filter)` annotation: it is deprecated, a report renders no control for it, and every use has a `given:` form (`malloy-model` covers the migration). `malloy-model` and `docs/givens.md` cover givens themselves.\n\n## Drill\n\n`# drill` goes on a model **dimension**, never on a report:\n\n```malloy\n# drill { to=[\"category\", \"self\"] given=CATEGORY }\ndimension: category is products.category\n\n# drill { to=\"regions/west\" given=REGION }\ndimension: region is regions.region\n```\n\n- **`to=<path>`** navigates to a report, of either layout, by its package path without `.malloy`, read from the package root: `to=category` is `category.malloy` at the root, `to=\"regions/west\"` is `regions/west.malloy`. Quote a path with a `/`. **Always write the path from the package root**: a bare name is a root file, never a report of that name elsewhere.\n- **`to=self`** filters in place; two or more destinations pop a menu.\n- **Always write `given=`.** Without it the given is the dimension name verbatim, so `dimension: category` seeds `category`, not `CATEGORY`. `to=self` survives that (it folds case); `to=<path>` does not, and lands on an unfiltered page with nothing erroring.\n- **The destination needs a control for the given**, declared as `filter<T>`. No lint checks that half, so click it and look.\n- **Only table cells are marked drillable.** A report meant to be drilled wants at least one untagged (table) tile, grouped by the tagged dimension. A column whose rendered header another, non-drillable tile also shows loses its marking: give the two different `# label=`s.\n\n## Read the lint\n\nPackage warnings after a reload are the report's test suite. Fix all of them, and read the text rather than the severity or the count. **\"Dashboard lint stopped early, so this list is incomplete\"** means the rest of the list is partial.\n\n| Finding | Fix |\n| ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |\n| `# drill … targets \"x\", which is not a report in this package` | A dead click. Write the target's path |\n| `to=self, but no model in this package declares a given \"X\"` | The clicked value has nowhere to go. Declare the given, or fix `given=` |\n| `given \"X\" suggests options from source \"y\", which this file does not define` | Import `y` into the report |\n| `filters by given \"X\", which this file does not import` | No control is shown for it. Import the given |\n| A tile that does not resolve to a real view | Fix the view name, or declare the view |\n| `notebook-artifact-unparsed` | The tag does not parse, usually an unquoted `source -> view` entry |\n| `notebook-layout-run` | A `run:` beside `tiles`. Make it a view, list it, delete the `run:` |\n| `notebook-tile-layout-ignored` | `colspan` or `break` on a linear report. Remove them, or switch to `layout=grid` |\n| `report-kind-removed` | The tag sets `kind=`, which is no longer read, so the file is no report. Write the `layout=` the finding gives |\n| `report-layout-implicit` | A tag with no tiles and no `layout=`. Add `layout=` |\n| `report-on-surface` | A model `index.malloy` imports (or `explores` lists) is tagged, so its sources left the surface. Untag it, or move the report out |\n| `index-is-not-a-report` | The root `index.malloy` carries a tag. Remove it; put the report in its own file |\n| `report-name-ambiguous` | Two grid reports share a file name, so the deprecated by-name dashboards route answers 409 for it. Rename one of the files |\n| `Tile … reads <source>, which index.malloy doesn't export` | Add the source to the `export { ... }` in `index.malloy`. Never add an `explores` to `publisher.json` |\n| A grid's `columns`, `colspan`, or `dashboard_columns=` findings | `reference/grid.md` |\n| A statement or tag above the artifact tag; prose the reader cannot place | `reference/linear.md`, the cell rules |\n\n**Read the status the reload itself returns, not the listing.** One report that fails to compile fails the whole package load (424), and a package already serving keeps its previous version, so the listing still looks healthy. `GET /api/v0/status` still knows: a package serving an older model than its files appears in `loadErrors` with `stale: true`. If the reload is 200 and your report is not listed, discovery skipped it: usually a missing or misspelled artifact tag, or a file under `public/`.\n\n**A clean reload is not proof the tags are right.** The parse warning is syntax only and carries no position. That is why the last step is opening the page.\n\n## Template\n\n```malloy\n##! experimental.givens\n##\" [One-line description of the report.]\n##| artifact { layout=linear title=\"[Title]\"\n tiles=[\n intro { kind=text },\n \"main_source_tiles -> broad_tile\",\n \"main_source_tiles -> drill_tile\"\n ]\n}\n|##\nimport { main_source } from \"model.malloy\"\n\n##|(markdown) intro\n[Framing question: what are we trying to understand?]\n|##\n\nsource: main_source_tiles is main_source extend {\n view: broad_tile is [broad view]\n view: drill_tile is [drill view]\n}\n```\n\nFor a grid, write `layout=grid`, add `dashboard { columns=12 }` after the tag's closing `}`, and give every tile view a `# colspan` (`reference/grid.md`).\n\n## Editing in the Console\n\nA person can open any report in the Console's builder (**Edit** on the report page; **New report** on the package page writes `<slug>.malloy` at the package root). The builder splices the file rather than regenerating it, so comments and anything it does not model survive; its layout switch rewrites `layout=` and nothing else. A report written as `run:` cells opens converted to tiles, and the first Save asks before it rewrites the file (`reference/linear.md`). A save whose text declares a real `#(authorize)` or `#(access_filter)` gate outside prose is refused with a 400: gates live in a model file the report imports.\n\n## What \"done\" means\n\n- The file is at the package root (or where the author asked), and its tag says `layout=`.\n- Every source, view, and field name came from the model you read in step 1.\n- The reload returned **200**, not 424, with **zero** report warnings, and your report is in `GET …/reports`.\n- You opened the page and every tile shows real numbers: not stuck loading, not an error, not an empty state you did not intend.\n- Each control renders as the widget you intended, and changing one changes the numbers.\n- If you added a `# drill`, its `to=` is a path, and you clicked it and landed where you meant to, with the given seeded.\n- On a grid: every tile carries a colspan, each row sums to `columns`, rows end flush, nothing is clipped, and no legend or card label ends in `…`.\n\n## Reference\n\n- `reference/grid.md`: the grid recipe, its traps, and one query as the whole page.\n- `reference/linear.md`: linear reports, `run:` cells, legacy `.malloynb`, style, and common mistakes.\n- `docs/reports.md`: the full guide this skill condenses.\n- `docs/givens.md`: the givens the controls are generated from.\n\n## Reference files over MCP\n\nThis skill's `reference/` files are served as separate prompts, one per file, fetched only when you ask for them. Where the text above says to read `reference/<name>.md`, get the prompt named `malloy-reports/<name>` instead.\n\nAvailable: grid, linear."
|
|
360806
|
+
body: "# Publisher Reports\n\n> A `.malloy` file with an `## artifact { layout=… tiles=[…] }` tag **is** a report. It imports the model, names the views to show, binds the filtering on those views, and says how to lay them out. Publisher discovers it at package load, renders the filter controls from the givens its tiles reference, offers `# drill` click-through, and serves it at `/<env>/<pkg>/<path>`, its package path without `.malloy`. No code, no build step.\n\nWhat other tools call a dashboard and a notebook are both reports here. The **layout** is the only difference:\n\n- **`layout=grid`**: tiles across a grid behind one filter row, scanned at a glance (a dashboard). Read `reference/grid.md` before you lay one out.\n- **`layout=linear`**: tiles and prose top to bottom, read in order (a notebook, a data story). Read `reference/linear.md` before you write one.\n\nEverything else (filters, text tiles, drill, the builder, the lint, the write path) is the same for both, and switching layout later is a one-property edit.\n\n## Where the file goes\n\n- **Write a new report at the package root**, as `<slug>.malloy`, unless the author asks for another location. No directory is required, and none is a convention: a report can live anywhere in the package except `public/`, and the root `index.malloy` is never a report. Do not create a `dashboards/` or `notebooks/` directory for it.\n- **Always write `layout=grid` or `layout=linear`** in the tag. A tag without one is read by a fallback rule, and a linear report that lists tiles without `layout=linear` is read as a grid. `kind=` is no longer read: a tag that sets it makes no report (`report-kind-removed`), so replace it with `layout=`.\n- **The package path is the report's identity.** `sales/q3.malloy` is the report `sales/q3`: its URL, its `GET …/reports/{path}` address, and its `# drill` target. Two reports may share a basename in different directories.\n- **Imports are relative to the report's own location.** At the root: `import { order_items } from \"storefront.malloy\"`. In a subdirectory: `\"../storefront.malloy\"`.\n- **An untagged `.malloy` file is an ordinary model**, wherever it lives. Shared definitions several reports import go in one.\n\n## When this is the right tool\n\n| The user wants | Use |\n| ---------------------------------------------------- | ------------------------------------------------------------ |\n| A recurring, at-a-glance view behind shared filters | this skill, `layout=grid` |\n| A narrative, with prose between the numbers | this skill, `layout=linear` |\n| A quick write-up of queries you already ran | `skill:malloy-analysis-report`, which writes a linear report |\n| Custom design, branding, or interactions beyond tags | an HTML data app |\n| The model itself: sources, measures, joins | `malloy-model` |\n\nInteractivity is not the axis: both layouts get filter controls, URL-addressable state, Apply batching, and `# drill`. Pick on the shape of the document: scanned at a glance is grid, read top to bottom is linear. When in doubt, linear.\n\n## Build sequence\n\nTool names below are bare (`get_context`); match each against the tools you actually have, since a host may prefix or rename them. A host names its compile and reload tools its own way. Where you have none, use the REST endpoint named beside the step.\n\n1. **READ THE MODEL FIRST.** Get the real source, view, dimension, and given names from the package: `get_context` if you have it, otherwise the REST model endpoint for `index.malloy` (it answers 404 for a file off the surface) or the `.malloy` files. Never guess a name. A guessed field in a query fails the whole package load, not just that one report; a guessed tile or suggest source is quieter, and only shows up in the package warnings. If the package root holds an `index.malloy` (every scaffolded package does), a tile reads only the sources that file exports.\n2. **PICK THE LAYOUT AND THE TILES.** Which views, in what order, and on a grid how wide each sits. A query tile is a `view:` you declare in the report, named as `\"source -> view\"`.\n3. **DECLARE THE GIVENS** the report will filter by, in the report file itself, with their control tags (see \"Filter controls\"). A report holds its own filters: the builder edits the report file and nothing else, so a filter it can add, change or remove is a declaration in that file. A given the model already declares and reads is imported by name, never declared again; if it is only a reader's filter, move it into the report instead (see \"Givens that stay in the model\").\n4. **COMPOSE THE FILE** at its path (the package root unless asked otherwise), following the template below, but do not save it yet. Name the sources you need in the import. Each tile binds its controls with a refinement, `view: t is v + { where: category ~ $CATEGORY }`, one clause per given it answers to; a `filter<...>` binds with `~`, a plain `date` or `number` given is a value and binds with `>=`, `<=` or `=`. Only the givens some tile references become controls. Then import every source or query any referenced given names in a `suggest`; **a `suggest` naming a `query=` needs that query's own source imported as well**, because an import is not transitive.\n5. **COMPILE IT** with your compile tool (`POST …/models/<path>/compile` over REST) at the path the file will have, with **`\"scope\": \"file\"`**: a report opens with an `import`, which the default `append` scope refuses, and an existing file compiled at `append` collides with itself. **`\"scope\": \"file\"` does not check tiles, drills or suggest queries.** To see those before you save, compile the same text at `\"scope\": \"package\"` with `modelPath` set to the report's path: it runs the lint step 6 reads and returns its findings with code `dashboard-lint` (or `render-tag`). Editing an untagged model several reports import also wants `\"scope\": \"package\"`, which recompiles every file as saved.\n6. **SAVE IT, RELOAD, AND READ THE MANIFEST AND THE WARNINGS.** Save by writing the file, or with `PUT …/models/<path>` over REST (omit `expectedHash` to create; it creates at any new path and replaces only a file the package already serves as a report). Then your reload tool, or `GET …/packages/<pkg>?reload=true`. A 424 means the package did not load and your edit is not live. **The `warnings` key is absent when there are none.** Then read `GET …/packages/<pkg>/reports/<path>`: its `givens` are exactly the controls that will render, and its `layout` is the layout Publisher read. See \"Read the lint\".\n7. **OPEN IT AND LOOK.** Not optional; see \"What done means\".\n\n## The form\n\n```malloy\n##! experimental.givens\n##\" One line saying what this report is for.\n##| artifact { layout=grid title=\"Storefront overview\"\n tiles=[\n intro { kind=text colspan=12 },\n \"overview -> kpis\",\n \"overview -> revenue_trend\",\n \"overview -> revenue_by_state\"\n ]\n} dashboard { columns=12 }\n|##\nimport { order_items, products } from \"storefront.malloy\"\n\n# label=\"Category\" control=select suggest { source=products dimension=category }\ngiven: CATEGORY :: filter<string> is f''\n# label=\"Ordered since\"\ngiven: SINCE :: date is @2023-01-01\n\n##|(markdown) intro\nRevenue and margin for the selected category.\n|##\n\nsource: overview is order_items extend {\n # colspan=12\n # label=\"Key figures\"\n # big_value\n view: kpis is key_figures + { where: category ~ $CATEGORY, where: created_at >= $SINCE }\n\n # colspan=8\n # break\n # label=\"Revenue by month\"\n view: revenue_trend is sales_by_month + { where: category ~ $CATEGORY, where: created_at >= $SINCE }\n\n # colspan=4\n # label=\"Revenue by state\"\n view: revenue_by_state is sales_by_state + { where: category ~ $CATEGORY, where: created_at >= $SINCE }\n}\n```\n\nThe same file with `layout=linear` and no `dashboard { … }` or `colspan` is a linear report.\n\n- **The tag is model-level**, one line (`## artifact { … }`) or a `##|` … `|##` block with the same text inside, the form to use once the list is long: one tile per line, `}` on its own line, `|##` closing at the opener's column. A one-line `##` tag must stay on one line: wrapping it is a compile error, or worse, compiles clean and quietly stops being a report.\n- **The header** above the tag holds only `##!` flags, `//` comments and the unnamed `\"` notes that are the report's description. Keep statements and `#` tags below `## artifact`: nothing refuses one above it, but the header is where readers and the builder look.\n- **Quote every query entry.** `\"overview -> kpis\"` is a string; unquoted, the tag does not parse (`notebook-artifact-unparsed`). A bare quoted name (`\"top_products\"`) names a model-level `query:` in the file.\n- **A query tile is a thin `view:` on an extension the report declares** (`source: overview is order_items extend { … }`): the modelled view keeps its chart tag, the wrapper carries the tile's `# label`, its width on a grid, and its binding.\n- **A text tile is a `##|(markdown) name` block** listed as `name { kind=text }`. The name is one bare word on the opener line, the body starts on the next line (a heading goes inside the block), and `|##` closes it at the opener's column. A bare `intro` without `{ kind=text }` is the query-tile form for a `query:`, so the block is not shown. On a grid the entry also takes `colspan` and `break`, and nothing else.\n- **File order:** header, imports and givens, the prose blocks in tile order, then the extension that defines the tile views. That is the order the builder writes.\n- **A `run:` in a report that lists `tiles` is never shown** (`notebook-layout-run`). Define a view, list it, delete the `run:`.\n- **A tile expression is a string, so the compiler never checks it.** Rename a view and the file still compiles; the tile fails at package load, where the lint names it.\n- **Chart tags go on the view** (`# line_chart`, `# bar_chart`, `# big_value`, `# label=\"…\"`). `skill:malloy-charts` covers choosing them.\n\n## Filter controls\n\nControls come from the `given:` declarations the tiles reference. The tags on the declaration are the control contract:\n\n```malloy\n##! experimental.givens\n\n# label=\"Category\" control=select suggest { source=products dimension=category }\ngiven: CATEGORY :: filter<string> is f''\n\n# label=\"Brand\" control=multiselect suggest { query=brand_suggest dimension=brand }\ngiven: BRAND :: filter<string> is f''\n\n# label=\"Minimum line total\" range_min=0 range_max=250\ngiven: MIN_SALE :: filter<number> is f''\n\n# label=\"Ordered since\"\ngiven: SINCE :: date is @2023-01-01\n```\n\n`control=select`/`multiselect` with a `suggest` renders a picker filled from the data; `range_min`/`range_max` on a `filter<number>` renders a two-handled range slider; a `filter<date>` or `filter<timestamp>` renders a time-range control with preset windows and a custom day range; a bare `date` or `timestamp` renders a date picker; a `filter<string>` with no tag renders a text box taking filter syntax. Which controls appear is per report, decided by which givens its tiles reference.\n\n- `#(description=\"…\")` renders as helper text under the control and draws a cosmetic \"not a well-formed route\" compile diagnostic for a multi-word value; `# description=\"…\"` publishes to the API but the UI does not render it.\n- `dimension=` can name a field of a directly joined source, quoted: `suggest { source=sales dimension=\"products.department\" }`. On the `query=` form, `dimension` names a column of that query's output; match it to the query's `group_by`.\n- **A suggest's source or query has to resolve in the report file**, so import it there. Getting it wrong does not error: the control still looks like a picker and says \"Could not load the options for this control\"; the package warnings name it.\n- Two options on the artifact tag, either layout: `autorun=false` batches control changes behind an Apply button; `givens { CATEGORY=f'Outerwear' }` sets starting values (a URL parameter wins). A file-level `## autorun=false` does nothing on a `.malloy` report.\n\n### Binding, and givens that stay in the model\n\n**Binding is per declaration, not per name.** A report declaring its own `CATEGORY` over a source whose model-level `where:` reads the model's `CATEGORY` compiles, shows the control, and filters nothing. So bind on the tiles, with `+ { where: … }`. The other design, model-level scoping, still works: put a scoped source in an untagged model that imports the givens it reads, and import that model and those givens into the report. Do not mix the two on one given.\n\nA given stays in the model, declared once there, when a model source, view or measure, an `#(authorize)` or `#(access_filter)` gate, or an HTML data app reads it. The report imports it by name (`import { CATEGORY } from \"givens.malloy\"`) and never re-declares it:\n\n- **Import a name and also declare it: a compile error.** Drop the local declaration.\n- **Declare a name the model already reads, without importing it: no error at all.** The control renders and moves, and the model's own `where:` or gate never sees the value.\n\n**A model that already scopes a source by an optional filter given** (`where: Manufacturer ~ $manufacturer_filter` in the model) needs a decision:\n\n- **It is a reader's filter and you may change the model: move it.** Delete the given and its `where:` clause from the model, declare it in the report with its control tags, and bind it on each tile that should answer to it. A reader's filter is anything a person picks on the page; a given stays in the model only for the reasons `malloy-model` lists (a gate, a value every query must supply, a parameter a field is defined with, scoping meant for every consumer).\n- **The model can't change, or the given is semantic or governance: import it** as above. Say so plainly: the filter shows in the reader and in the builder, and can be set in both, but the builder cannot remove or reconfigure it. It is the model's.\n\nBefore, in the model:\n\n```malloy\n##! experimental.givens\ngiven:\n manufacturer_filter :: filter<string> is f''\n subject_filter :: filter<string> is f''\n major_recall_filter :: filter<boolean> is f''\n recall_date_filter :: filter<date> is f''\n\nsource: recalls is duckdb.table('data/auto_recalls.csv') extend {\n where:\n Manufacturer ~ $manufacturer_filter,\n Subject ~ $subject_filter,\n `Major Recall` ~ $major_recall_filter,\n `Report Received Date` ~ $recall_date_filter\n dimension: `Major Recall` is ifnull(`Potentially Affected`, 0) > 100000\n}\n```\n\nAfter: the model declares no givens and `recalls` has no `where:`; the report declares the four and binds each tile:\n\n```malloy\n##! experimental.givens\n##| artifact { layout=grid title=\"Recalls\"\n tiles=[\"recalls_tiles -> recalls_by_year\", \"recalls_tiles -> recalls_by_manufacturer\"]\n} dashboard { columns=12 }\n|##\nimport { recalls } from \"auto_recalls.malloy\"\n\n# label=\"Manufacturer\" control=multiselect suggest { source=recalls dimension=Manufacturer }\ngiven: manufacturer_filter :: filter<string> is f''\n# label=\"Subject\"\ngiven: subject_filter :: filter<string> is f''\n# label=\"Major recalls only\"\ngiven: major_recall_filter :: filter<boolean> is f''\n# label=\"Filed\"\ngiven: recall_date_filter :: filter<date> is f''\n\nsource: recalls_tiles is recalls extend {\n # colspan=6\n view: recalls_by_year is by_year + {\n where:\n Manufacturer ~ $manufacturer_filter,\n Subject ~ $subject_filter,\n `Major Recall` ~ $major_recall_filter,\n `Report Received Date` ~ $recall_date_filter\n }\n # colspan=6\n view: recalls_by_manufacturer is by_manufacturer + {\n where:\n Subject ~ $subject_filter,\n `Major Recall` ~ $major_recall_filter,\n `Report Received Date` ~ $recall_date_filter\n }\n}\n```\n\nA tile view takes a name of its own: `view: by_year is by_year + { … }` is \"Cannot redefine 'by_year'\". Each tile binds only the givens it should answer to: `recalls_by_manufacturer` leaves the manufacturer filter off, since it is the breakdown by manufacturer. Every given here is a `filter<…>`, so each binds with `~`; a plain `date` or `number` given binds with `>=`, `<=` or `=` (step 4).\n\nA given has to be in the report file's own scope to be bindable: declared there or imported. Never add a `#(filter)` annotation: it is deprecated, a report renders no control for it, and every use has a `given:` form (`malloy-model` covers the migration). `malloy-model` and `docs/givens.md` cover givens themselves.\n\n## Drill\n\n`# drill` goes on a model **dimension**, never on a report:\n\n```malloy\n# drill { to=[\"category\", \"self\"] given=CATEGORY }\ndimension: category is products.category\n\n# drill { to=\"regions/west\" given=REGION }\ndimension: region is regions.region\n```\n\n- **`to=<path>`** navigates to a report, of either layout, by its package path without `.malloy`, read from the package root: `to=category` is `category.malloy` at the root, `to=\"regions/west\"` is `regions/west.malloy`. Quote a path with a `/`. **Always write the path from the package root**: a bare name is a root file, never a report of that name elsewhere.\n- **`to=self`** filters in place; two or more destinations pop a menu.\n- **Always write `given=`.** Without it the given is the dimension name verbatim, so `dimension: category` seeds `category`, not `CATEGORY`. `to=self` survives that (it folds case); `to=<path>` does not, and lands on an unfiltered page with nothing erroring.\n- **The destination needs a control for the given**, declared as `filter<T>`. No lint checks that half, so click it and look.\n- **Only table cells are marked drillable.** A report meant to be drilled wants at least one untagged (table) tile, grouped by the tagged dimension. A column whose rendered header another, non-drillable tile also shows loses its marking: give the two different `# label=`s.\n\n## Read the lint\n\nPackage warnings after a reload are the report's test suite. Fix all of them, and read the text rather than the severity or the count. **\"Dashboard lint stopped early, so this list is incomplete\"** means the rest of the list is partial.\n\n| Finding | Fix |\n| ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |\n| `# drill … targets \"x\", which is not a report in this package` | A dead click. Write the target's path |\n| `to=self, but no model in this package declares a given \"X\"` | The clicked value has nowhere to go. Declare the given, or fix `given=` |\n| `given \"X\" suggests options from source \"y\", which this file does not define` | Import `y` into the report |\n| `filters by given \"X\", which this file does not import` | No control is shown for it. Import the given |\n| A tile that does not resolve to a real view | Fix the view name, or declare the view |\n| `notebook-artifact-unparsed` | The tag does not parse, usually an unquoted `source -> view` entry |\n| `notebook-layout-run` | A `run:` beside `tiles`. Make it a view, list it, delete the `run:` |\n| `notebook-tile-layout-ignored` | `colspan` or `break` on a linear report. Remove them, or switch to `layout=grid` |\n| `report-kind-removed` | The tag sets `kind=`, which is no longer read, so the file is no report. Write the `layout=` the finding gives |\n| `report-cells-removed` | A linear tag with no `tiles`, the retired cell format, so the file is no report. List its tiles (`reference/linear.md`) |\n| `report-on-surface` | A model `index.malloy` imports (or `explores` lists) is tagged, so its sources left the surface. Untag it, or move the report out |\n| `index-is-not-a-report` | The root `index.malloy` carries a tag. Remove it; put the report in its own file |\n| `report-name-ambiguous` | Two grid reports share a file name, so the deprecated by-name dashboards route answers 409 for it. Rename one of the files |\n| `Tile … reads <source>, which index.malloy doesn't export` | Add the source to the `export { ... }` in `index.malloy`. Never add an `explores` to `publisher.json` |\n| A grid's `columns`, `colspan`, or `dashboard_columns=` findings | `reference/grid.md` |\n| Prose the reader cannot place (`notebook-markdown-attached-nowhere`, `notebook-markdown-block-unnamed`) | `reference/linear.md` |\n\n**Read the status the reload itself returns, not the listing.** One report that fails to compile fails the whole package load (424), and a package already serving keeps its previous version, so the listing still looks healthy. `GET /api/v0/status` still knows: a package serving an older model than its files appears in `loadErrors` with `stale: true`. If the reload is 200 and your report is not listed, discovery skipped it: usually a missing or misspelled artifact tag, or a file under `public/`.\n\n**A clean reload is not proof the tags are right.** The parse warning is syntax only and carries no position. That is why the last step is opening the page.\n\n## Template\n\n```malloy\n##! experimental.givens\n##\" [One-line description of the report.]\n##| artifact { layout=linear title=\"[Title]\"\n tiles=[\n intro { kind=text },\n \"main_source_tiles -> broad_tile\",\n \"main_source_tiles -> drill_tile\"\n ]\n}\n|##\nimport { main_source } from \"model.malloy\"\n\n##|(markdown) intro\n[Framing question: what are we trying to understand?]\n|##\n\nsource: main_source_tiles is main_source extend {\n view: broad_tile is [broad view]\n view: drill_tile is [drill view]\n}\n```\n\nFor a grid, write `layout=grid`, add `dashboard { columns=12 }` after the tag's closing `}`, and give every tile view a `# colspan` (`reference/grid.md`).\n\n## Editing in the Console\n\nA person can open any report in the Console's builder (**Edit** on the report page; **+ Report** on the package page writes `<slug>.malloy` at the package root). The builder splices the file rather than regenerating it, so comments and anything it does not model survive; its layout switch rewrites `layout=` and nothing else. A file written as `run:` cells, the retired linear form, is no report and does not open (`reference/linear.md`). A save whose text declares a real `#(authorize)` or `#(access_filter)` gate outside prose is refused with a 400: gates live in a model file the report imports.\n\n## What \"done\" means\n\n- The file is at the package root (or where the author asked), and its tag says `layout=`.\n- Every source, view, and field name came from the model you read in step 1.\n- The reload returned **200**, not 424, with **zero** report warnings, and your report is in `GET …/reports`.\n- You opened the page and every tile shows real numbers: not stuck loading, not an error, not an empty state you did not intend.\n- Each control renders as the widget you intended, and changing one changes the numbers.\n- If you added a `# drill`, its `to=` is a path, and you clicked it and landed where you meant to, with the given seeded.\n- On a grid: every tile carries a colspan, each row sums to `columns`, rows end flush, nothing is clipped, and no legend or card label ends in `…`.\n\n## Reference\n\n- `reference/grid.md`: the grid recipe, its traps, and one query as the whole page.\n- `reference/linear.md`: linear reports, rewriting a retired `run:`-cell file, legacy `.malloynb`, style, and common mistakes.\n- `docs/reports.md`: the full guide this skill condenses.\n- `docs/givens.md`: the givens the controls are generated from.\n\n## Reference files over MCP\n\nThis skill's `reference/` files are served as separate prompts, one per file, fetched only when you ask for them. Where the text above says to read `reference/<name>.md`, get the prompt named `malloy-reports/<name>` instead.\n\nAvailable: grid, linear."
|
|
360723
360807
|
},
|
|
360724
360808
|
{
|
|
360725
360809
|
name: "malloy-reports/grid",
|
|
@@ -360771,94 +360855,21 @@ source: category_tiles is order_items extend {
|
|
|
360771
360855
|
|
|
360772
360856
|
- **\`layout=linear\` is required** when the tag lists tiles; without it the file is read as a grid.
|
|
360773
360857
|
- \`dashboard { columns }\` has no effect, and a \`colspan\` or \`break\` on a tile entry is ignored with a warning (\`notebook-tile-layout-ignored\`).
|
|
360774
|
-
- **Tiles run through the model query endpoint**,
|
|
360775
|
-
|
|
360776
|
-
## Reports written as \`run:\` cells (the older form)
|
|
360777
|
-
|
|
360778
|
-
A linear report can also be a file of \`run:\` statements with \`(markdown)\` notes between them. Publisher still reads and serves it, and a person saving it in the Console converts it to tiles. Write new reports as tiles; maintain a cell report in its own form until someone converts it. \`skill:malloy-analysis-report\` writes this form for a quick write-up.
|
|
360858
|
+
- **Tiles run through the model query endpoint**, against the report's own path, like a grid's.
|
|
360779
360859
|
|
|
360780
|
-
|
|
360860
|
+
## Files written as \`run:\` cells (retired)
|
|
360781
360861
|
|
|
360782
|
-
|
|
360862
|
+
An older linear form was a file of \`run:\` statements with \`(markdown)\` notes between them, under a tag with no \`tiles\`. Publisher no longer reads it: such a file is not a report, and the package warnings name it (\`report-cells-removed\`). Never write one. To bring one back, rewrite it as tiles:
|
|
360783
360863
|
|
|
360784
|
-
1.
|
|
360785
|
-
2.
|
|
360786
|
-
3.
|
|
360787
|
-
4. **\`given:\` uses \`NAME :: filter<T> is f''\`, bound with \`~\`, declared before first use.** Put \`##! experimental.givens\` at the top of the file.
|
|
360788
|
-
5. **Trailing prose is \`##(markdown)\`, never \`#(markdown)\` or \`#"\`.** Both belong to the statement below them, and at the end of a file there is none. An \`import\` or an \`export\` takes no annotation either, so prose above one is \`##|(markdown)\`.
|
|
360789
|
-
|
|
360790
|
-
### How the file becomes cells
|
|
360791
|
-
|
|
360792
|
-
Cells come from the file's own notes and statements, in file order (imported files add none):
|
|
360793
|
-
|
|
360794
|
-
| In the file | Cell |
|
|
360795
|
-
| ------------------------------------------------------------------------ | ---------------------------------------------------------------------- | --------------------------------------------------------- |
|
|
360796
|
-
| \`##!\` lines, \`## artifact\`, and every \`"\` note above the tag | the header: no cell |
|
|
360797
|
-
| \`##(markdown)\` lines (contiguous) or a \`## | (markdown)\` block, after the tag | one markdown cell |
|
|
360798
|
-
| \`#(markdown)\` or a \`# | (markdown)\`block above a\`run:\`, \`source:\`, \`query:\`, \`given:\`or\`type:\` | no cell of its own: it renders with that statement's cell |
|
|
360799
|
-
| \`run:\` with its contiguous \`#\` tag block, including \`run: <named query>\` | one query cell |
|
|
360800
|
-
| \`import\`, \`source:\`, \`query:\`, \`given:\`, \`export { … }\`, \`type:\` | one definition cell **per statement** |
|
|
360801
|
-
| a non-\`"\` \`##\` note after the tag (\`## title=…\`) | not a cell |
|
|
360802
|
-
|
|
360803
|
-
Definition cells render folded; only query cells run. Earlier spellings are still read: \`##"\`, \`##|"\`, \`##(text)\` and \`##|(text)\` notes below the tag are markdown cells, like \`(markdown)\`. Write \`(markdown)\`. A query cell runs as a _restricted query_ against the already-compiled file, so an \`import\` inside a \`run:\` is rejected (\`file imports are not permitted in a restricted query\`).
|
|
360804
|
-
|
|
360805
|
-
### A complete cell report
|
|
360806
|
-
|
|
360807
|
-
\`\`\`malloy
|
|
360808
|
-
##! experimental.givens
|
|
360809
|
-
##" One category at a time: how it sells across the year and which brands carry it.
|
|
360810
|
-
## artifact { layout=linear title="Category review" }
|
|
360811
|
-
import { order_items, products } from "storefront.malloy"
|
|
360812
|
-
|
|
360813
|
-
##|(markdown)
|
|
360814
|
-
## Category review
|
|
360815
|
-
|
|
360816
|
-
Pick a **Category** in the controls above and every chart below re-runs for it.
|
|
360817
|
-
Leave the control empty to read the whole catalog.
|
|
360818
|
-
|##
|
|
360819
|
-
|
|
360820
|
-
#(description="Narrow to one product category")
|
|
360821
|
-
# label="Category" control=select suggest { source=products dimension=category }
|
|
360822
|
-
given: CATEGORY :: filter<string> is f''
|
|
360823
|
-
|
|
360824
|
-
##(markdown) How is revenue trending for the selected category?
|
|
360825
|
-
|
|
360826
|
-
#(markdown) ### Revenue by month
|
|
360827
|
-
#" Revenue by month for the selected category
|
|
360828
|
-
# line_chart
|
|
360829
|
-
# label="Revenue by month"
|
|
360830
|
-
run: order_items -> sales_by_month + { where: category ~ $CATEGORY }
|
|
360831
|
-
|
|
360832
|
-
##(markdown) Which brands are behind those numbers?
|
|
360833
|
-
|
|
360834
|
-
#" The eight brands with the most revenue in the selected category
|
|
360835
|
-
# bar_chart
|
|
360836
|
-
# label="Top brands"
|
|
360837
|
-
run: order_items -> top_brands + { where: category ~ $CATEGORY }
|
|
360838
|
-
|
|
360839
|
-
##(markdown) Last, the best-selling products, defined once as a named query and then run.
|
|
360840
|
-
|
|
360841
|
-
query: top_products_in_category is order_items -> top_products + { where: category ~ $CATEGORY }
|
|
360842
|
-
|
|
360843
|
-
#(markdown) ### Best sellers
|
|
360844
|
-
run: top_products_in_category
|
|
360845
|
-
\`\`\`
|
|
360846
|
-
|
|
360847
|
-
The \`##|(markdown)\` block and each \`##(markdown)\` line are floating markdown cells. The two \`#(markdown)\` lines are attached: each renders as a header with the \`run:\` below it. The \`given:\` and the \`query:\` are definition cells, and each \`run:\` is a query cell.
|
|
360848
|
-
|
|
360849
|
-
### When the Console converts it
|
|
360850
|
-
|
|
360851
|
-
Saving a cell report in the builder turns each \`run:\` into a \`view:\` on a \`<source>_tiles\` extension appended to the file (a \`query:\` used by exactly one run is folded into its view), each prose note into a \`##|(markdown) text_N\` block, and leaves definitions, imports, givens and comments where they were. The first Save asks before it rewrites the file ("Convert this notebook?"); Cancel writes nothing, and the file's history in its repository is the way back. Write a cell report so it converts:
|
|
360852
|
-
|
|
360853
|
-
- Every \`run:\` is \`<source> -> <view or query body>\` with a named source. An inline \`extend\` before the arrow, a source that is not a name, or a refinement (\`q + { … }\`) of a multi-stage query is refused, and so is a run that resolves through more than ten named queries.
|
|
360854
|
-
- A chart line the editor does not model (for example \`# bar_chart { size=spark }\`) is kept, and the Viz type picker is disabled for that tile with the reason shown.
|
|
360855
|
-
- The editor opens a report **read-only**, saying why, when it cannot place cells one by one: two statements or notes on one line, text after a block closer, a comment straddling two cells, a lone carriage return (use LF or CRLF), a statement above the \`## artifact\` tag, a statement no cell can hold, or a tag value Malloy cannot read, such as a malformed date literal (\`@2024-13-01\`).
|
|
360864
|
+
1. Each floating \`##(markdown)\` note or \`##|(markdown)\` block becomes a named \`##|(markdown) name\` block, listed as \`name { kind=text }\`.
|
|
360865
|
+
2. Each \`run: <source> -> <query>\` becomes a \`view:\` on a \`<source>_tiles\` extension the report declares, listed as \`"<source>_tiles -> <view>"\`. The render tags above the \`run:\` move to the view; a \`#"\` caption becomes \`# label="…"\`. A \`run:\` of a named \`query:\` is listed by the query's name.
|
|
360866
|
+
3. List the entries in \`tiles=[…]\` in reading order, keep \`layout=linear\`, and delete every \`run:\`.
|
|
360856
360867
|
|
|
360857
360868
|
## Multiple models and cross-model joins
|
|
360858
360869
|
|
|
360859
|
-
A report compiles with **all** its \`import\`s in scope, so it can import several \`.malloy\` models, and a tile's source
|
|
360870
|
+
A report compiles with **all** its \`import\`s in scope, so it can import several \`.malloy\` models, and a tile's source can reference, and **join**, sources from different imported files. Import each model you need at the top; no re-export wrapper is required.
|
|
360860
360871
|
|
|
360861
|
-
**When the package root holds an \`index.malloy\`, tiles
|
|
360872
|
+
**When the package root holds an \`index.malloy\`, tiles read only what it exports.** A tile over a source \`index.malloy\` does not export answers 404, even when the report imports that source's file. A source the report derives from an exported one (\`source: mine is customers extend { ... }\`) still works. A tile's own source over a raw table (\`duckdb.table(...)\`) is refused. To use a source, add it to the \`export { ... }\` in \`index.malloy\`.
|
|
360862
360873
|
|
|
360863
360874
|
## Style
|
|
360864
360875
|
|
|
@@ -360867,14 +360878,20 @@ A report compiles with **all** its \`import\`s in scope, so it can import severa
|
|
|
360867
360878
|
**No data-specific insights in markdown.** Data refreshes will make them stale.
|
|
360868
360879
|
|
|
360869
360880
|
\`\`\`malloy
|
|
360870
|
-
|
|
360881
|
+
##|(markdown) spike_note
|
|
360882
|
+
WRONG: will become stale when data refreshes. Revenue spiked 23% in March.
|
|
360883
|
+
|##
|
|
360871
360884
|
|
|
360872
|
-
|
|
360885
|
+
##|(markdown) trend_q
|
|
360886
|
+
RIGHT: frames the question and lets the query answer it. How is revenue trending?
|
|
360887
|
+
|##
|
|
360873
360888
|
\`\`\`
|
|
360874
360889
|
|
|
360890
|
+
Each is a text tile, listed in \`tiles=[…]\` by name, for example \`trend_q { kind=text }\`.
|
|
360891
|
+
|
|
360875
360892
|
To add \`limit\`, \`where\`, or other options to an existing view, refine it with \`+\`: \`my_view + { limit: 15 }\`.
|
|
360876
360893
|
|
|
360877
|
-
One query per tile
|
|
360894
|
+
One query per tile, and remember a chart renders only the **first** aggregate in its result.
|
|
360878
360895
|
|
|
360879
360896
|
## Existing \`.malloynb\` files
|
|
360880
360897
|
|
|
@@ -360882,28 +360899,21 @@ A \`.malloynb\` (cells delimited by \`>>>markdown\` and \`>>>malloy\`) is a depr
|
|
|
360882
360899
|
|
|
360883
360900
|
## Common mistakes
|
|
360884
360901
|
|
|
360885
|
-
| Mistake | Fix
|
|
360886
|
-
| -------------------------------------------------------------------------- |
|
|
360887
|
-
| A tile list without \`layout=linear\` | Read as a grid. Write \`layout=linear\`.
|
|
360888
|
-
|
|
|
360889
|
-
|
|
|
360890
|
-
| \`##
|
|
360891
|
-
| \`##
|
|
360892
|
-
|
|
|
360893
|
-
| Text on the opener line after
|
|
360894
|
-
|
|
|
360895
|
-
|
|
|
360896
|
-
|
|
|
360897
|
-
| \`
|
|
360898
|
-
|
|
|
360899
|
-
|
|
|
360900
|
-
| \`tiles=[intro { kind=text }, orders_tiles -> headline]\` (entry not quoted) | Every \`source -> view\` entry is a quoted string. Unquoted, the tag does not parse. |
|
|
360901
|
-
| A bare \`intro\` in \`tiles\` for a \`## | (markdown) intro\` block | A bare name is the query-tile form for a \`query:\`. Write \`intro { kind=text }\`. |
|
|
360902
|
-
| \`colspan\` or \`break\` on a linear report's tile entry | Ignored and reported (\`notebook-tile-layout-ignored\`). Remove them, or switch to \`layout=grid\`. |
|
|
360903
|
-
| \`#(filter)\` in any form | Deprecated. Declare a \`given:\` on the model's source instead. |
|
|
360904
|
-
| \`import\` inside a \`run:\` cell | Imports are file-wide; put them at the top. |
|
|
360905
|
-
| Data-specific insights in markdown | Frame questions instead. |
|
|
360906
|
-
| A cross-model join in a quick \`skill:malloy-analysis-report\` cell | That cell renders against a single model. Write it as a report tile over a source the report declares with the join, since the report compiles with all its imports. |`
|
|
360902
|
+
| Mistake | Fix |
|
|
360903
|
+
| -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
|
|
360904
|
+
| A tile list without \`layout=linear\` | Read as a grid. Write \`layout=linear\`. |
|
|
360905
|
+
| A linear tag with no \`tiles=[…]\` | The retired cell format: no report (\`report-cells-removed\`). List the tiles. |
|
|
360906
|
+
| \`view_name { limit: 10 }\` | Use \`+\`: \`view_name + { limit: 10 }\` |
|
|
360907
|
+
| Floating \`##(markdown)\` prose in a tiled report | Never shown (\`notebook-markdown-block-unnamed\`). Make it a named text tile and list it. |
|
|
360908
|
+
| \`## Heading\` for a section title | That line is model tags, not prose. Write the heading inside a \`##\\|(markdown) name\` text tile. |
|
|
360909
|
+
| \`##\\| markdown\` or \`##\\|markdown\` (no parentheses) | Write \`##\\|(markdown) name\`. |
|
|
360910
|
+
| Text on the opener line after \`##\\|(markdown)\` | Only the tile's name may follow it. Put the text on the next line. |
|
|
360911
|
+
| \`run:\` in a report that lists \`tiles=[…]\` | Never shown. Define a \`view:\`, list \`"source -> view"\` in \`tiles\`, delete the \`run:\`. |
|
|
360912
|
+
| \`tiles=[intro { kind=text }, orders_tiles -> headline]\` (entry not quoted) | Every \`source -> view\` entry is a quoted string. Unquoted, the tag does not parse. |
|
|
360913
|
+
| A bare \`intro\` in \`tiles\` for a \`##\\|(markdown) intro\` block | A bare name is the query-tile form for a \`query:\`. Write \`intro { kind=text }\`. |
|
|
360914
|
+
| \`colspan\` or \`break\` on a linear report's tile entry | Ignored and reported (\`notebook-tile-layout-ignored\`). Remove them, or switch to \`layout=grid\`. |
|
|
360915
|
+
| \`#(filter)\` in any form | Deprecated. Declare a \`given:\` on the model's source instead. |
|
|
360916
|
+
| Data-specific insights in a text tile | Frame questions instead. |`
|
|
360907
360917
|
},
|
|
360908
360918
|
{
|
|
360909
360919
|
name: "malloy-review",
|
|
@@ -361106,7 +361116,7 @@ This review covers 10,173 lines across 47 files. Only ~1,500 lines were deep-rev
|
|
|
361106
361116
|
{
|
|
361107
361117
|
name: "malloy-review/rubric-structure",
|
|
361108
361118
|
description: "Rubric: Structure. Reference detail for the malloy-review skill.",
|
|
361109
|
-
body: "# Rubric: Structure\n\n**Dimension:** `structure`\n**Rules:** 5\n\nApplies to the organization of `.malloy` files within a package. Skip a file for structural review if it's a report (a `.malloy` file anywhere in the package whose model-level notes carry an `## artifact` tag, of either layout, `layout=grid` or `layout=linear` or the deprecated `kind=`, written as `tiles=[…]
|
|
361119
|
+
body: "# Rubric: Structure\n\n**Dimension:** `structure`\n**Rules:** 5\n\nApplies to the organization of `.malloy` files within a package. Skip a file for structural review if it's a report (a `.malloy` file anywhere in the package whose model-level notes carry an `## artifact` tag, of either layout, `layout=grid` or `layout=linear` or the deprecated `kind=`, written as `tiles=[…]`; a legacy query-level `# artifact` on a `query:`; or a `.malloynb`), which has its own conventions (the `malloy-reports` skill), or a one-off sandbox script. Do not flag a report for where it sits: no directory is required for one, and the package root is the default.\n\nFor every rule, the linked instruction-skill section is the canonical source for rationale and examples.\n\n---\n\n## S-01: Base vs joined source separation\n\n- **Severity:** major (non-blocking) · **Category:** structure · LLM-judgment\n- **Detection:** LLM heuristic, a source named after a base table (`customers`, `orders`) that includes cross-table joins, or a file with multiple sources where one clearly joins the others, is a candidate for splitting\n- **Fix:** move the joined source into its own file; leave the base source clean\n- **See:** `skill:malloy-model` § Base vs Joined Sources · `skill:malloy-model` § Joined Source File Template\n\n---\n\n## S-02: Sources should declare `primary_key:` when one exists\n\n- **Severity:** minor (non-blocking) · **Category:** structure · machine-checkable\n- **Why this isn't a correctness rule.** The Malloy compiler auto-synthesizes a UUID-based `__distinct_key` for any `join_one:` target without a declared PK (`packages/malloy/src/model/field_instance.ts:644-670`, `query_query.ts:946-951`), so symmetric aggregation is correct either way, see the C-07 entry in `rubric-correctness.md`'s \"Rules we dropped\" section. The `with` shortcut, which DOES require a declared PK, errors at compile time and surfaces as a diagnostic. The actual silent-correctness hazard (\"declared PK isn't actually unique in the data\") is `rubric-correctness.md` § C-12.\n- **What this rule catches.** A discoverability / hygiene gap: when a source has a natural primary key, declaring it lets downstream code use the `with` shortcut, makes grain explicit in the model, and gives tooling a stable identifier per row. Treat as a recommendation, not a merge gate.\n- **Detection:** for every `source:` declaration, check whether its body contains a `primary_key:` clause. Skip flagging when:\n - The source is a query-based / computed source (`source: x is t -> {...}` or `source: x is (t -> {...}) extend {...}`) where grain is determined by the `group_by` columns, declaring a `primary_key:` on the result is fine but not required.\n - The source represents an event/log table or a denormalized analytical source where no natural PK exists. Both situations are legitimate; the LLM should recognize them and skip the finding.\n- **Fix (when the source does have a natural PK):** declare it, `primary_key: <col>` inside `extend {}`. When there isn't a natural PK, leave it undeclared; if the absence is non-obvious, a one-line `#(doc)` on the source explaining the grain helps future readers.\n- **See:** `skill:malloy-model` § Key Rules · `rubric-correctness.md` § C-12 (the related correctness rule that checks whether a declared PK is actually unique in the data) · `rubric-style.md` § Y-03 (`join_one:` style consistency, which is the other consequence of declared-vs-undeclared PKs)\n\n---\n\n## S-03: Joined tables defined before the source that references them\n\n- **Severity:** blocker (blocking) · **Category:** structure · machine-checkable\n- **Detection:** AST, within a single file, for each source reference inside a `join_*:` or `source is <name>`, verify the target source is declared earlier\n- **Fix:** reorder source declarations so dependencies come first. If two sources reference each other, split into separate files with explicit `import` (cross-file declarations don't have this ordering constraint).\n- **See:** `skill:malloy-gotchas-modeling` § Source Order, Define Joined Tables First\n\n---\n\n## S-04: Base sources curate their column surface\n\n- **Severity:** major (non-blocking) · **Category:** structure · machine-checkable\n- **Detection:** AST, for each `source: ... is conn.table(...)` declaration (a direct-table base source), check whether the source uses **any** of the three column-curation mechanisms below. The finding fires only when the source uses none of them, every column from the underlying table is implicitly public. Query-based sources and sources derived from other sources are exempt.\n- **Fix:** pick whichever option matches the source's needs. Curation options, lightest to heaviest:\n 1. `extend { except: a, b, c }`, drop a small named set of columns. Compatible with `rename:`. No experimental flag.\n 2. `extend { accept: a, b, c }`, keep only a small named set. Compatible with `rename:`. No experimental flag.\n 3. `##! experimental.access_modifiers` + `include { public: …, internal: …, private: … }`, full per-column visibility tiers. **Composes with `rename:` in one order only** (the `extend { rename: }` before the `include {}`, which then names the field by its new name) and disallows measures/dimensions whose names shadow `internal:` columns. Use only when the visibility distinction matters (e.g., shared sources joined into multiple consumers).\n\n Most files only need option 1 or 2, for the small set of columns that shouldn't be public, prefer `except:` over the heavier `include {}` machinery. Reach for `include {}` only when per-column tiers are genuinely worth the constraints.\n- **See:** `skill:malloy-gotchas-modeling` § Field Management: `extend {}` and `include {}`, in that order · `skill:malloy-model` § Base Source Templates · `malloy-model/reference/access-modifiers.md`\n\n---\n\n## S-05: One file per table (base) / one per analytical domain (joined)\n\n- **Severity:** minor (non-blocking) · **Category:** structure · machine-checkable\n- **Detection:** count `source: X is conn.table(...)` base-source declarations per file. More than one → flag.\n- **Fix:** split each base source into its own file named after the source\n- **See:** `skill:malloy-model` § Base Source Templates (file convention is implicit; weak counterpart)\n\n---\n\n## Cross-cutting note\n\nWhen a file violates both S-01 (base+joined mixed) and S-05 (multiple base sources), surface the combined observation as a **Cross-Cutting Theme** in the review output: \"File layout suggests a restructure, see S-series findings for suggested splits.\""
|
|
361110
361120
|
},
|
|
361111
361121
|
{
|
|
361112
361122
|
name: "malloy-review/rubric-style",
|
|
@@ -361483,7 +361493,7 @@ Close with what you used, in a few lines: the agent and package, which skills yo
|
|
|
361483
361493
|
{
|
|
361484
361494
|
name: "malloy-source-unreachable",
|
|
361485
361495
|
description: `Work out why a source is missing from get_context, or why a query against it was refused with a 404 or a 403, and what to do next. Use when a source you expect is absent from discovery, when execute_query answers "No queryable source", when a query that used to work stops working, or when deciding whether a package's curation is hiding something you need.`,
|
|
361486
|
-
body: '# A source you cannot reach\n\n> **Tool names** are written bare here - `get_context`, `execute_query`, `get_status`, `compile_model`. The exact prefixed name depends on the host surface; match each against the tools you actually have.\n\nMost packages publish a curated surface, so a source being absent or refused is usually the package working as its author intended, not a fault. Read the refusal before working around it, because the three things that can refuse you need three different responses and they are told apart by the status code.\n\n## The three answers\n\n| what you got | what it means | what to do |\n| --- | --- | --- |\n| **404** that says "not on this package\'s published surface" | the name is real and the package does not publish it. It is curation, not a typo | follow the fix the message names (usually: name it in the `export { ... }` of `index.malloy` and address the query there), or query what IS published |\n| **404**, plain "No queryable source", "No queryable model" or "Query target is not queryable" | the name does not exist, or it is hidden in a model that carries an `#(authorize)` or `#(access_filter)` gate. In a gated model the two are deliberately indistinguishable, so a refusal cannot be used to probe for hidden names | check the name against `get_context`, then query something on the surface or ask the package\'s author to publish it |\n| **403**, "Access denied" | the source exists and is on the surface, but an `#(authorize)` gate did not admit you | supply the givens the gate reads, or accept that this caller may not read it |\n| **200** with zero rows | you were admitted, and an `#(access_filter)` narrowed the rows to none of them | this is a real answer. Report it as "no matching rows", never as an error |\n\nA 403 names a source, so it tells you the source exists. A plain 404 tells you nothing at all. That asymmetry is deliberate: a source that is both hidden AND gated answers a plain **404**, so you can never use a 403, or the wording of a 404, to discover that a hidden gated name is real.\n\n**One 404 is not about the source at all.** If a query that joins an exported source in its own text answers `Query target is not queryable`, check the joined source\'s givens. A source that reads a given with no default, which `index.malloy` does not import, is refused that way when joined. Queried directly, the same source answers a 400 that names the cause. Run the joining query through `compile_model`: the problem then reads "references given NAME ... which is not surfaced in this model". The fix belongs in the model, not the query, so report it to the model\'s author: `index.malloy` has to import that given, either by importing its declaring file whole or by naming the given in a selective import.\n\n## Addressing the surface, which is the most common 404\n\nA query names a model file. When a package curates its surface, only files on that surface are valid entry points, **even for a source that file declares itself**:\n\n```\nPOST .../packages/sales/models/index.malloy/query {"sourceName": "orders"} -> 200\nPOST .../packages/sales/models/orders.malloy/query {"sourceName": "orders"} -> 404\n```\n\nSame source, same package. `orders.malloy` is off the surface, so it is not an entry point.\n\n**Use the `model_path` that `get_context` gave you, verbatim.** Its `source_info.resource_id` holds `environment`, `package`, `model_path` and `source`, and those are exactly `execute_query`\'s `environmentName`, `packageName`, `modelPath` and `sourceName`. On a curated package the `model_path` is the surface file, not the file that declares the source. Substituting the declaring file because it looks more correct is how this 404 happens.\n\n## Other places a hidden source answers 404\n\nThe query route is not the only one held to the surface. On a curated package, a source or file off it also shows up as a 404 here:\n\n- **Reading a model.** `GET .../models/{path}` answers 404 for a file off the surface, with the same words as the query route. The file still exists and still compiles; read `index.malloy` instead. A model that is on the surface lists only the names it publishes, not everything it imports.\n- **A report tile.** A tile, or a filter `suggest` over a hidden source answers 404. The report itself is still listed. The package load warns once per tile, for example `Tile orders_staging -> by_flag on report overview reads orders_staging, which index.malloy doesn\'t export, so it won\'t load. Fix: add orders_staging to the export { ... } in index.malloy.` It is in the `warnings` on the package\'s own response, `GET .../packages/{pkg}`.\n
|
|
361496
|
+
body: '# A source you cannot reach\n\n> **Tool names** are written bare here - `get_context`, `execute_query`, `get_status`, `compile_model`. The exact prefixed name depends on the host surface; match each against the tools you actually have.\n\nMost packages publish a curated surface, so a source being absent or refused is usually the package working as its author intended, not a fault. Read the refusal before working around it, because the three things that can refuse you need three different responses and they are told apart by the status code.\n\n## The three answers\n\n| what you got | what it means | what to do |\n| --- | --- | --- |\n| **404** that says "not on this package\'s published surface" | the name is real and the package does not publish it. It is curation, not a typo | follow the fix the message names (usually: name it in the `export { ... }` of `index.malloy` and address the query there), or query what IS published |\n| **404**, plain "No queryable source", "No queryable model" or "Query target is not queryable" | the name does not exist, or it is hidden in a model that carries an `#(authorize)` or `#(access_filter)` gate. In a gated model the two are deliberately indistinguishable, so a refusal cannot be used to probe for hidden names | check the name against `get_context`, then query something on the surface or ask the package\'s author to publish it |\n| **403**, "Access denied" | the source exists and is on the surface, but an `#(authorize)` gate did not admit you | supply the givens the gate reads, or accept that this caller may not read it |\n| **200** with zero rows | you were admitted, and an `#(access_filter)` narrowed the rows to none of them | this is a real answer. Report it as "no matching rows", never as an error |\n\nA 403 names a source, so it tells you the source exists. A plain 404 tells you nothing at all. That asymmetry is deliberate: a source that is both hidden AND gated answers a plain **404**, so you can never use a 403, or the wording of a 404, to discover that a hidden gated name is real.\n\n**One 404 is not about the source at all.** If a query that joins an exported source in its own text answers `Query target is not queryable`, check the joined source\'s givens. A source that reads a given with no default, which `index.malloy` does not import, is refused that way when joined. Queried directly, the same source answers a 400 that names the cause. Run the joining query through `compile_model`: the problem then reads "references given NAME ... which is not surfaced in this model". The fix belongs in the model, not the query, so report it to the model\'s author: `index.malloy` has to import that given, either by importing its declaring file whole or by naming the given in a selective import.\n\n## Addressing the surface, which is the most common 404\n\nA query names a model file. When a package curates its surface, only files on that surface are valid entry points, **even for a source that file declares itself**:\n\n```\nPOST .../packages/sales/models/index.malloy/query {"sourceName": "orders"} -> 200\nPOST .../packages/sales/models/orders.malloy/query {"sourceName": "orders"} -> 404\n```\n\nSame source, same package. `orders.malloy` is off the surface, so it is not an entry point.\n\n**Use the `model_path` that `get_context` gave you, verbatim.** Its `source_info.resource_id` holds `environment`, `package`, `model_path` and `source`, and those are exactly `execute_query`\'s `environmentName`, `packageName`, `modelPath` and `sourceName`. On a curated package the `model_path` is the surface file, not the file that declares the source. Substituting the declaring file because it looks more correct is how this 404 happens.\n\n## Other places a hidden source answers 404\n\nThe query route is not the only one held to the surface. On a curated package, a source or file off it also shows up as a 404 here:\n\n- **Reading a model.** `GET .../models/{path}` answers 404 for a file off the surface, with the same words as the query route. The file still exists and still compiles; read `index.malloy` instead. A model that is on the surface lists only the names it publishes, not everything it imports.\n- **A report tile.** A tile, or a filter `suggest` over a hidden source answers 404. The report itself is still listed. The package load warns once per tile, for example `Tile orders_staging -> by_flag on report overview reads orders_staging, which index.malloy doesn\'t export, so it won\'t load. Fix: add orders_staging to the export { ... } in index.malloy.` It is in the `warnings` on the package\'s own response, `GET .../packages/{pkg}`.\n\nThe fix is the same in each case: add the source to the `export { ... }` in `index.malloy`, or use what is already published.\n\n## When the source is not in `get_context` at all\n\nWork down this list. The first three are far more common than the last.\n\n1. **It is off the surface.** The package publishes a curated set and this source is not in it. Nothing you can do from the query side; it is a package edit.\n2. **The package failed to load, or is serving a stale model.** Call `list_packages`: a package that failed to load appears with an `error`, and one serving the model it compiled *before* a failed reload carries `stale: true` and the `error` too. `get_status` has the full load errors.\n3. **You are talking to a different server than you think.** Call `list_packages` and check the environment and package names are the ones you expect. A stale `.mcp.json` outlives the server that wrote it, and another Publisher may hold that port.\n4. **Your search phrasing missed it.** Retry with a bare target (no `search_text`) to enumerate rather than rank. If it appears there, the source exists and was a ranking miss.\n\n## What curation does not do\n\n- **It does not hide fields.** A published source may `join` an unpublished one, and a query grouping by a joined field returns that field\'s values normally. Hiding a source removes it as a landing point; it does not redact columns a published source pulls in.\n- **It does not gate `/compile`.** `compile_model` is exempt, because compile is the authoring loop. A hidden source can still be compile-checked, and that is intended. The exceptions are a hidden source that is also gated, which answers 404 at compile too, and a document (text with a model-level `## artifact` tag): its tiles and cells are held to the surface, so one over a source off it comes back as a `query-not-queryable` problem rather than compiling.\n- **It is not access control.** Curation answers "what is queryable by name", not "who may query it". Identity is `#(authorize)` and `#(access_filter)`. A source is not protected by being hidden.\n\n## Reaching an unpublished source from your own model\n\nIf you are writing Malloy rather than just querying, an unpublished source is still usable, with one condition that trips people.\n\n`export { ... }` decides what an **importing file** can see, which is Malloy\'s rule and not Publisher\'s:\n\n- a file that declares **no** `export` hands an importer everything it declares, so import the file that declares the source and join it normally;\n- a file that **does** declare one hands over exactly that list. Importing a file whose `export` omits the source and then naming it fails to compile with `Reference to undefined object`.\n\nSo an unpublished source is reachable through a file that exports it, or that exports nothing, and not through a file whose `export` leaves it out. Re-export chains are fine at any depth: a source re-exported through several files stays queryable through the surface, because admission follows the declaration rather than the path taken to it.\n\n## Before you report a source as missing\n\nConfirm all three, in this order. Each one has been mistaken for "the data is not there".\n\n1. `get_status` shows the package loaded and not `stale`.\n2. `list_packages` names the environment and package you meant.\n3. An enumerating `get_context` call (a target with no `search_text`) does not list it.\n\nOnly then is it genuinely not published, and the answer to the user is that the package does not expose it, not that the data does not exist.'
|
|
361487
361497
|
}
|
|
361488
361498
|
]
|
|
361489
361499
|
};
|
|
@@ -368233,6 +368243,24 @@ function tryAcquire(sync, alreadyAcquiredError = E_ALREADY_LOCKED) {
|
|
|
368233
368243
|
// src/mcp/tools/embedding_index.ts
|
|
368234
368244
|
init_logger();
|
|
368235
368245
|
|
|
368246
|
+
// src/service/event_loop_yield.ts
|
|
368247
|
+
var DEFAULT_BUDGET_MS = 10;
|
|
368248
|
+
var budgetMs = DEFAULT_BUDGET_MS;
|
|
368249
|
+
function yieldToEventLoop() {
|
|
368250
|
+
return new Promise((resolve3) => setImmediate(resolve3));
|
|
368251
|
+
}
|
|
368252
|
+
function createEventLoopYielder() {
|
|
368253
|
+
let since = performance.now();
|
|
368254
|
+
return {
|
|
368255
|
+
async maybeYield() {
|
|
368256
|
+
if (performance.now() - since < budgetMs)
|
|
368257
|
+
return;
|
|
368258
|
+
await yieldToEventLoop();
|
|
368259
|
+
since = performance.now();
|
|
368260
|
+
}
|
|
368261
|
+
};
|
|
368262
|
+
}
|
|
368263
|
+
|
|
368236
368264
|
// src/mcp/tools/embedding_sync_queue.ts
|
|
368237
368265
|
init_logger();
|
|
368238
368266
|
|
|
@@ -369385,7 +369413,6 @@ function fingerprintOfRows(rows) {
|
|
|
369385
369413
|
return contentHash(rows.join(`
|
|
369386
369414
|
`));
|
|
369387
369415
|
}
|
|
369388
|
-
var yieldToEventLoop = () => new Promise((resolve3) => setImmediate(resolve3));
|
|
369389
369416
|
async function desiredFacetsChunked(entities, plan) {
|
|
369390
369417
|
const desired = [];
|
|
369391
369418
|
for (let i2 = 0;i2 < entities.length; i2 += FINGERPRINT_CHUNK_ENTITIES) {
|
|
@@ -372968,6 +372995,11 @@ function readGivenControlSpec(annotationTexts2) {
|
|
|
372968
372995
|
}
|
|
372969
372996
|
return spec;
|
|
372970
372997
|
}
|
|
372998
|
+
function givenDeclaresNoDefault(given) {
|
|
372999
|
+
const loose = given;
|
|
373000
|
+
const parsed = "default" in loose ? loose.default : loose._internal?.default;
|
|
373001
|
+
return parsed === undefined && loose._internal?.defaultText === undefined;
|
|
373002
|
+
}
|
|
372971
373003
|
function malloyGivenToApi(given) {
|
|
372972
373004
|
const type = given.type;
|
|
372973
373005
|
const renderedType = type.type === "filter expression" ? `filter<${type.filterType}>` : type.type === "array" && type.elementTypeDef?.type ? `${type.elementTypeDef.type}[]` : type.type;
|
|
@@ -372978,7 +373010,8 @@ function malloyGivenToApi(given) {
|
|
|
372978
373010
|
annotations: allNotes.filter((note) => !isReservedRoute(note.route) && note.route !== MARKDOWN_ROUTE).map((note) => note.text),
|
|
372979
373011
|
...readGivenControlSpec(allNotes.map((note) => note.text)),
|
|
372980
373012
|
...allNotes.some((note) => note.route === "secure") && { secure: true },
|
|
372981
|
-
default: given._internal?.defaultText
|
|
373013
|
+
default: given._internal?.defaultText,
|
|
373014
|
+
...givenDeclaresNoDefault(given) && { required: true }
|
|
372982
373015
|
};
|
|
372983
373016
|
}
|
|
372984
373017
|
function collectGivenRefs(value, into) {
|
|
@@ -373187,6 +373220,7 @@ function readReportModelFacts(modelPath, modelDef, surfacedGivenNames, gatesBySo
|
|
|
373187
373220
|
name: given.name,
|
|
373188
373221
|
type: isFilter ? `filter<${type.filterType}>` : type.type,
|
|
373189
373222
|
default: given.defaultText,
|
|
373223
|
+
...given.default === undefined && given.defaultText === undefined ? { required: true } : {},
|
|
373190
373224
|
annotations: (given.annotations?.blockNotes ?? []).concat(given.annotations?.notes ?? []).map((note) => note.text)
|
|
373191
373225
|
});
|
|
373192
373226
|
}
|
|
@@ -373338,6 +373372,7 @@ function givenSpec(declaration, facts) {
|
|
|
373338
373372
|
name: declaration.name,
|
|
373339
373373
|
type: declaration.type,
|
|
373340
373374
|
default: declaration.type.startsWith("filter<") && declaration.default !== undefined ? unwrapFilterLiteral(declaration.default) : declaration.default,
|
|
373375
|
+
...declaration.required ? { required: true } : {},
|
|
373341
373376
|
annotations: declaration.annotations.filter((text) => /^##?\(/.test(text) && !isMarkdownNote(text)),
|
|
373342
373377
|
...declaration.annotations.some((text) => /^#\(secure\)/.test(text)) && {
|
|
373343
373378
|
secure: true
|
|
@@ -380195,6 +380230,9 @@ function extractSourcesFromModelDef(modelDef, givens, onParseError) {
|
|
|
380195
380230
|
const authorizeMap = new Map;
|
|
380196
380231
|
const misplacedAuthorize = [];
|
|
380197
380232
|
const gatedSourceOwnAuthorizeNotes = new Set;
|
|
380233
|
+
const gatedSourceOwnAuthorizeNoteKeys = new Set;
|
|
380234
|
+
const noteKey = (note) => `${notePositionKey(note.at)}#${note.text}`;
|
|
380235
|
+
const isGatedSourceOwnNote = (note) => gatedSourceOwnAuthorizeNotes.has(note) || gatedSourceOwnAuthorizeNoteKeys.has(noteKey(note));
|
|
380198
380236
|
const authorizeNoteDeclaredBy = new Map;
|
|
380199
380237
|
const nearMissAuthorize = [];
|
|
380200
380238
|
const sweptStructs = [];
|
|
@@ -380206,6 +380244,7 @@ function extractSourcesFromModelDef(modelDef, givens, onParseError) {
|
|
|
380206
380244
|
for (const note of ownLevelNotes(struct.annotations)) {
|
|
380207
380245
|
if (containsAuthorizeAnnotationTag([note.text])) {
|
|
380208
380246
|
gatedSourceOwnAuthorizeNotes.add(note);
|
|
380247
|
+
gatedSourceOwnAuthorizeNoteKeys.add(noteKey(note));
|
|
380209
380248
|
considerNoteOwner(authorizeNoteDeclaredBy, note, struct);
|
|
380210
380249
|
}
|
|
380211
380250
|
}
|
|
@@ -380224,6 +380263,7 @@ function extractSourcesFromModelDef(modelDef, givens, onParseError) {
|
|
|
380224
380263
|
for (const note of ownLevelNotes(entry.annotations)) {
|
|
380225
380264
|
if (containsAuthorizeAnnotationTag([note.text])) {
|
|
380226
380265
|
gatedSourceOwnAuthorizeNotes.add(note);
|
|
380266
|
+
gatedSourceOwnAuthorizeNoteKeys.add(noteKey(note));
|
|
380227
380267
|
considerNoteOwner(authorizeNoteDeclaredBy, note, entry);
|
|
380228
380268
|
}
|
|
380229
380269
|
}
|
|
@@ -380322,7 +380362,7 @@ function extractSourcesFromModelDef(modelDef, givens, onParseError) {
|
|
|
380322
380362
|
const fieldAuthorizeNotes = ownLevelNotes(field.annotations).filter((note) => containsAuthorizeAnnotationTag([note.text]));
|
|
380323
380363
|
if (fieldAuthorizeNotes.length === 0)
|
|
380324
380364
|
continue;
|
|
380325
|
-
if (fieldAuthorizeNotes.every(
|
|
380365
|
+
if (fieldAuthorizeNotes.every(isGatedSourceOwnNote)) {
|
|
380326
380366
|
continue;
|
|
380327
380367
|
}
|
|
380328
380368
|
const fieldName = field.as || field.name;
|
|
@@ -380381,276 +380421,10 @@ function extractQueriesFromModelDef(modelDef) {
|
|
|
380381
380421
|
return { queries, misplacedAuthorize };
|
|
380382
380422
|
}
|
|
380383
380423
|
|
|
380384
|
-
// src/service/compile_document.ts
|
|
380385
|
-
var DECLARATION = /\b(?:source|query)\s*:\s*(`(?:[^`\\]|\\.)*`|[\p{L}_][\p{L}\p{N}_]*)\s+is\b/giu;
|
|
380386
|
-
function blankSpans(text, cells) {
|
|
380387
|
-
if (cells.length === 0)
|
|
380388
|
-
return text;
|
|
380389
|
-
const units = text.split("");
|
|
380390
|
-
for (const cell of cells) {
|
|
380391
|
-
const span = cellOffsets.get(cell);
|
|
380392
|
-
if (!span)
|
|
380393
|
-
throw new Error("restricted cell has no recorded span");
|
|
380394
|
-
const [start, end] = span;
|
|
380395
|
-
for (let i2 = start;i2 < end && i2 < units.length; i2++) {
|
|
380396
|
-
if (units[i2] !== `
|
|
380397
|
-
` && units[i2] !== "\r")
|
|
380398
|
-
units[i2] = " ";
|
|
380399
|
-
}
|
|
380400
|
-
}
|
|
380401
|
-
return units.join("");
|
|
380402
|
-
}
|
|
380403
|
-
function declaredNames(text) {
|
|
380404
|
-
const found = new Set(buildDerivationBaseMap(text).keys());
|
|
380405
|
-
for (const match of stripMalloyCommentsAndLiterals(text).matchAll(DECLARATION)) {
|
|
380406
|
-
const name = match[1];
|
|
380407
|
-
found.add(name.startsWith("`") ? name.slice(1, -1) : name);
|
|
380408
|
-
}
|
|
380409
|
-
return found;
|
|
380410
|
-
}
|
|
380411
|
-
function readsSource(prepared) {
|
|
380412
|
-
const ref = prepared._query?.structRef;
|
|
380413
|
-
if (typeof ref === "string")
|
|
380414
|
-
return ref;
|
|
380415
|
-
const named = ref;
|
|
380416
|
-
return named?.as || named?.name;
|
|
380417
|
-
}
|
|
380418
|
-
async function denied(gates, text) {
|
|
380419
|
-
try {
|
|
380420
|
-
await gates.text(text);
|
|
380421
|
-
return false;
|
|
380422
|
-
} catch (error2) {
|
|
380423
|
-
if (error2 instanceof AccessDeniedError)
|
|
380424
|
-
return true;
|
|
380425
|
-
throw error2;
|
|
380426
|
-
}
|
|
380427
|
-
}
|
|
380428
|
-
function tileProblem(expression, reason, cause) {
|
|
380429
|
-
return {
|
|
380430
|
-
code: "tile-does-not-compile",
|
|
380431
|
-
severity: "error",
|
|
380432
|
-
message: `Tile "${expression}" ${cause === "parse" ? "could not be parsed" : "does not compile"}: ${reason.replace(/\.?\s*$/, ".")} ` + `Fix: write the tile as one query over a source and a view, such as \`source -> view\` or \`source -> { group_by: field }\`.`
|
|
380433
|
-
};
|
|
380434
|
-
}
|
|
380435
|
-
function problemLine(problem) {
|
|
380436
|
-
const line = problem.at?.range?.start?.line;
|
|
380437
|
-
return line === undefined ? undefined : line + 1;
|
|
380438
|
-
}
|
|
380439
|
-
async function compileDocument(input) {
|
|
380440
|
-
const { base, source, modelName, gates } = input;
|
|
380441
|
-
const tag = readReportTagOfText(source);
|
|
380442
|
-
if (tag.status !== "report")
|
|
380443
|
-
return;
|
|
380444
|
-
const parse5 = parseNotebookText(source);
|
|
380445
|
-
if (isNotebookReaderError(parse5))
|
|
380446
|
-
return;
|
|
380447
|
-
const read = readNotebookCells(parse5, undefined, source);
|
|
380448
|
-
if (read.error) {
|
|
380449
|
-
return { problems: [cellsUnreadableProblem(read.error, "")] };
|
|
380450
|
-
}
|
|
380451
|
-
const code = read.cells.filter((cell) => cell.type === "code");
|
|
380452
|
-
const restrictedCells = new Set;
|
|
380453
|
-
const tainted = new Set;
|
|
380454
|
-
const mentionsTainted = (text) => tainted.size > 0 && [...scanIdentifiers(text)].some((name) => tainted.has(name));
|
|
380455
|
-
for (const cell of code) {
|
|
380456
|
-
if (mentionsTainted(cell.text) || await denied(gates, cell.text)) {
|
|
380457
|
-
restrictedCells.add(cell);
|
|
380458
|
-
if (cell.kind === "definition") {
|
|
380459
|
-
for (const name of declaredNames(cell.text))
|
|
380460
|
-
tainted.add(name);
|
|
380461
|
-
}
|
|
380462
|
-
}
|
|
380463
|
-
}
|
|
380464
|
-
const tileExpressions = readDocumentTileExpressions(read.annotations);
|
|
380465
|
-
const restrictedTiles = new Set;
|
|
380466
|
-
for (const expression of tileExpressions) {
|
|
380467
|
-
const text = `run: ${expression}`;
|
|
380468
|
-
if (mentionsTainted(text) || await denied(gates, text)) {
|
|
380469
|
-
restrictedTiles.add(normalizeTileExpression(expression));
|
|
380470
|
-
}
|
|
380471
|
-
}
|
|
380472
|
-
const definitions = code.filter((cell) => cell.kind === "definition" && !restrictedCells.has(cell)).map((cell) => cell.text).join(`
|
|
380473
|
-
`);
|
|
380474
|
-
const unparsedTiles = new Set;
|
|
380475
|
-
const tileProblems = [];
|
|
380476
|
-
for (const expression of tileExpressions) {
|
|
380477
|
-
const key = normalizeTileExpression(expression);
|
|
380478
|
-
if (restrictedTiles.has(key))
|
|
380479
|
-
continue;
|
|
380480
|
-
try {
|
|
380481
|
-
await gates.constructs(`${definitions}
|
|
380482
|
-
run: ${expression}`);
|
|
380483
|
-
} catch (error2) {
|
|
380484
|
-
if (!(error2 instanceof UnparseableTextError))
|
|
380485
|
-
throw error2;
|
|
380486
|
-
unparsedTiles.add(key);
|
|
380487
|
-
tileProblems.push(tileProblem(expression, error2.detail, "parse"));
|
|
380488
|
-
}
|
|
380489
|
-
}
|
|
380490
|
-
const boundaryProblems = [];
|
|
380491
|
-
const checkBoundary = async (query, line) => {
|
|
380492
|
-
try {
|
|
380493
|
-
await gates.boundary(query, definitions);
|
|
380494
|
-
} catch (error2) {
|
|
380495
|
-
if (!(error2 instanceof OffSurfaceError))
|
|
380496
|
-
throw error2;
|
|
380497
|
-
boundaryProblems.push({
|
|
380498
|
-
code: "query-not-queryable",
|
|
380499
|
-
severity: "error",
|
|
380500
|
-
message: error2.message,
|
|
380501
|
-
...line !== undefined && {
|
|
380502
|
-
at: {
|
|
380503
|
-
url: "",
|
|
380504
|
-
range: {
|
|
380505
|
-
start: { line: line - 1, character: 0 },
|
|
380506
|
-
end: { line: line - 1, character: 0 }
|
|
380507
|
-
}
|
|
380508
|
-
}
|
|
380509
|
-
}
|
|
380510
|
-
});
|
|
380511
|
-
}
|
|
380512
|
-
};
|
|
380513
|
-
for (const cell of code) {
|
|
380514
|
-
if (cell.kind === "query" && !restrictedCells.has(cell)) {
|
|
380515
|
-
await checkBoundary(cell.text, cell.startLine);
|
|
380516
|
-
}
|
|
380517
|
-
}
|
|
380518
|
-
for (const expression of tileExpressions) {
|
|
380519
|
-
const key = normalizeTileExpression(expression);
|
|
380520
|
-
if (!restrictedTiles.has(key) && !unparsedTiles.has(key)) {
|
|
380521
|
-
await checkBoundary(`run: ${expression}`, undefined);
|
|
380522
|
-
}
|
|
380523
|
-
}
|
|
380524
|
-
if (boundaryProblems.length > 0)
|
|
380525
|
-
return { problems: boundaryProblems };
|
|
380526
|
-
const readable = [
|
|
380527
|
-
...code.filter((cell) => cell.kind === "query" && !restrictedCells.has(cell)).map((cell) => cell.text),
|
|
380528
|
-
...tileExpressions.filter((expression) => {
|
|
380529
|
-
const key = normalizeTileExpression(expression);
|
|
380530
|
-
return !restrictedTiles.has(key) && !unparsedTiles.has(key);
|
|
380531
|
-
}).map((expression) => `run: ${expression}`)
|
|
380532
|
-
];
|
|
380533
|
-
for (const query of readable.length > 0 ? readable : [""]) {
|
|
380534
|
-
gates.nameVisible(query, definitions);
|
|
380535
|
-
}
|
|
380536
|
-
const compiledText = blankSpans(source, [...restrictedCells]);
|
|
380537
|
-
await gates.document(compiledText);
|
|
380538
|
-
const extended = base.extendModel(compiledText);
|
|
380539
|
-
let model;
|
|
380540
|
-
try {
|
|
380541
|
-
model = await extended.getModel();
|
|
380542
|
-
} catch (error2) {
|
|
380543
|
-
if (error2 instanceof MalloyError4)
|
|
380544
|
-
return { problems: [...tileProblems, ...error2.problems] };
|
|
380545
|
-
throw error2;
|
|
380546
|
-
}
|
|
380547
|
-
const modelDef = model._modelDef;
|
|
380548
|
-
const registry2 = modelDef.givens ?? {};
|
|
380549
|
-
const sources = extractSourcesFromModelDef(modelDef, []).sources;
|
|
380550
|
-
const settle2 = async (runnable, onDenied) => {
|
|
380551
|
-
try {
|
|
380552
|
-
await gates.compiled(runnable);
|
|
380553
|
-
} catch (error2) {
|
|
380554
|
-
if (error2 instanceof AccessDeniedError)
|
|
380555
|
-
onDenied();
|
|
380556
|
-
else
|
|
380557
|
-
throw error2;
|
|
380558
|
-
}
|
|
380559
|
-
};
|
|
380560
|
-
const compiledBoundary = async (runnable, prepared, query) => {
|
|
380561
|
-
try {
|
|
380562
|
-
await gates.boundaryCompiled(runnable, readsSource(prepared), query, definitions);
|
|
380563
|
-
} catch (error2) {
|
|
380564
|
-
if (!(error2 instanceof OffSurfaceError))
|
|
380565
|
-
throw error2;
|
|
380566
|
-
boundaryProblems.push({
|
|
380567
|
-
code: "query-not-queryable",
|
|
380568
|
-
severity: "error",
|
|
380569
|
-
message: error2.message
|
|
380570
|
-
});
|
|
380571
|
-
}
|
|
380572
|
-
};
|
|
380573
|
-
for (const cell of code) {
|
|
380574
|
-
if (cell.kind !== "query" || restrictedCells.has(cell))
|
|
380575
|
-
continue;
|
|
380576
|
-
const runnable = extended.loadRestrictedQuery(cell.text);
|
|
380577
|
-
let denied2 = false;
|
|
380578
|
-
await settle2(runnable, () => {
|
|
380579
|
-
denied2 = true;
|
|
380580
|
-
restrictedCells.add(cell);
|
|
380581
|
-
});
|
|
380582
|
-
if (denied2)
|
|
380583
|
-
continue;
|
|
380584
|
-
try {
|
|
380585
|
-
await compiledBoundary(runnable, await runnable.getPreparedQuery(), cell.text);
|
|
380586
|
-
} catch (error2) {
|
|
380587
|
-
if (!(error2 instanceof MalloyError4))
|
|
380588
|
-
throw error2;
|
|
380589
|
-
}
|
|
380590
|
-
}
|
|
380591
|
-
const compiledTiles = await compileTileGivens(tileExpressions, { loadQuery: (text) => extended.loadRestrictedQuery(text) }, registry2, (sourceName) => gateGivenSource(sources, sourceName), new Set([...restrictedTiles, ...unparsedTiles]), async (tile, prepared) => {
|
|
380592
|
-
let denied2 = false;
|
|
380593
|
-
await settle2({ getPreparedQuery: async () => prepared }, () => {
|
|
380594
|
-
denied2 = true;
|
|
380595
|
-
restrictedTiles.add(normalizeTileExpression(tile));
|
|
380596
|
-
});
|
|
380597
|
-
if (!denied2) {
|
|
380598
|
-
await compiledBoundary({ getPreparedQuery: async () => prepared }, prepared, `run: ${tile}`);
|
|
380599
|
-
}
|
|
380600
|
-
}, (tile, error2) => {
|
|
380601
|
-
if (!(error2 instanceof MalloyError4))
|
|
380602
|
-
return;
|
|
380603
|
-
gates.nameVisible(`run: ${tile}`, definitions);
|
|
380604
|
-
tileProblems.push(tileProblem(tile, error2.problems.map((problem) => problem.message).join("; "), "compile"));
|
|
380605
|
-
});
|
|
380606
|
-
if (boundaryProblems.length > 0)
|
|
380607
|
-
return { problems: boundaryProblems };
|
|
380608
|
-
const spans = [...restrictedCells];
|
|
380609
|
-
const problems = [...tileProblems, ...model.problems].filter((problem) => {
|
|
380610
|
-
const line = problemLine(problem);
|
|
380611
|
-
return line === undefined || !spans.some((cell) => line >= cell.startLine && line <= cell.endLine);
|
|
380612
|
-
});
|
|
380613
|
-
const slug = modelName.replace(/^.*\//, "").replace(/\.malloy$/, "");
|
|
380614
|
-
const path13 = `${slug}.malloy`;
|
|
380615
|
-
const facts = readReportModelFacts(path13, modelDef, Object.values(registry2).map((given) => given.name), new Map(sources.flatMap((entry) => entry.name ? [
|
|
380616
|
-
[
|
|
380617
|
-
entry.name,
|
|
380618
|
-
gateGivenSource(sources, entry.name) ?? []
|
|
380619
|
-
]
|
|
380620
|
-
] : [])));
|
|
380621
|
-
const manifest = buildReportManifest({ ...facts, compiledTileGivens: compiledTiles }, { restrictedTiles });
|
|
380622
|
-
const hasTiles = manifest?.tiles !== undefined;
|
|
380623
|
-
const cells = read.cells.filter((cell) => !hasTiles || cell.kind === "definition").map((cell) => {
|
|
380624
|
-
const restricted = restrictedCells.has(cell);
|
|
380625
|
-
if (cell.kind === "markdown") {
|
|
380626
|
-
return { type: "markdown", kind: "markdown", text: cell.text };
|
|
380627
|
-
}
|
|
380628
|
-
return {
|
|
380629
|
-
type: "code",
|
|
380630
|
-
kind: cell.kind,
|
|
380631
|
-
text: cell.text,
|
|
380632
|
-
...cell.markdown !== undefined && { markdown: cell.markdown },
|
|
380633
|
-
...cell.proseLines && { proseLines: cell.proseLines },
|
|
380634
|
-
...cell.codeLine !== undefined && { codeLine: cell.codeLine },
|
|
380635
|
-
...cell.caption !== undefined && { caption: cell.caption },
|
|
380636
|
-
...restricted && { restricted: true }
|
|
380637
|
-
};
|
|
380638
|
-
});
|
|
380639
|
-
if (!manifest) {
|
|
380640
|
-
return { problems, document: { layout: tag.layout, cells } };
|
|
380641
|
-
}
|
|
380642
|
-
const { entryFile: _entry, ...rest } = manifest;
|
|
380643
|
-
return {
|
|
380644
|
-
problems,
|
|
380645
|
-
document: {
|
|
380646
|
-
layout: manifest.layout,
|
|
380647
|
-
manifest: { ...rest, name: slug },
|
|
380648
|
-
cells
|
|
380649
|
-
}
|
|
380650
|
-
};
|
|
380651
|
-
}
|
|
380652
|
-
|
|
380653
380424
|
// src/service/notebook_lint.ts
|
|
380425
|
+
var CELLS_REMOVED = "report-cells-removed";
|
|
380426
|
+
var LAYOUT_UNKNOWN = "report-layout-unknown";
|
|
380427
|
+
var CELLS_REMOVED_MESSAGE = "a linear report written as `run:` cells is no longer read, so this file is not a report. Fix: list its tiles in `tiles=[…]` (a text tile per prose block, a query tile per view), or remove the `## artifact` tag.";
|
|
380654
380428
|
var TILE_ENTRY_FIX = 'Fix: every `source -> view` entry in `tiles` is a quoted string (`"orders_tiles -> headline"`), and a text entry is a bare name followed by `{ kind=text }` (`intro { kind=text }`).';
|
|
380655
380429
|
var quoted = (line, max = 60) => line.trim().slice(0, max);
|
|
380656
380430
|
var asTileName = (token) => {
|
|
@@ -380683,10 +380457,7 @@ function lintNotebookText(modelPath, text) {
|
|
|
380683
380457
|
const tag = readReportTag(tagNotes);
|
|
380684
380458
|
if (tag.status === "none")
|
|
380685
380459
|
return [];
|
|
380686
|
-
const hasTiles = tag.status === "report" && motlyTag(tagNotes)?.tag("artifact")?.has("tiles") === true;
|
|
380687
380460
|
const linear = tag.status === "report" && tag.layout === "linear";
|
|
380688
|
-
const inNotebooks = linear && !hasTiles;
|
|
380689
|
-
const layoutNotebook = linear && hasTiles;
|
|
380690
380461
|
const page = linear ? "notebook" : "dashboard";
|
|
380691
380462
|
const nodeText = (node) => node.start && node.stop ? text.slice(map[node.start.startIndex], map[node.stop.stopIndex + 1]) : "";
|
|
380692
380463
|
const lineOfNode = (node) => node.start?.line ?? 1;
|
|
@@ -380703,12 +380474,10 @@ function lintNotebookText(modelPath, text) {
|
|
|
380703
380474
|
let artifact;
|
|
380704
380475
|
let givensEnabled = false;
|
|
380705
380476
|
let firstGiven;
|
|
380706
|
-
const spans = [];
|
|
380707
380477
|
let closerLine;
|
|
380708
|
-
const aboveArtifact = [];
|
|
380709
380478
|
const modelNotes = [];
|
|
380710
380479
|
const docNotes = [];
|
|
380711
|
-
children.forEach((child
|
|
380480
|
+
children.forEach((child) => {
|
|
380712
380481
|
if (!isRuleNode(child)) {
|
|
380713
380482
|
const token = child?.symbol;
|
|
380714
380483
|
const name = token ? symbolOf(token) : undefined;
|
|
@@ -380721,16 +380490,11 @@ function lintNotebookText(modelPath, text) {
|
|
|
380721
380490
|
return;
|
|
380722
380491
|
}
|
|
380723
380492
|
closerLine = undefined;
|
|
380724
|
-
if (child.start && child.stop)
|
|
380725
|
-
spans.push([child.start.startIndex, child.stop.stopIndex]);
|
|
380726
380493
|
const group = callAccessor(child, "docAnnotations");
|
|
380727
380494
|
if (group) {
|
|
380728
380495
|
const notes = callAccessor(group, "docAnnotation") ?? [];
|
|
380729
380496
|
for (const note of notes) {
|
|
380730
380497
|
const noteText = nodeText(note);
|
|
380731
|
-
const route = routeOfNote(noteText);
|
|
380732
|
-
const first = noteText.split(`
|
|
380733
|
-
`, 1)[0];
|
|
380734
380498
|
const bodyText = modelNoteBody(noteText);
|
|
380735
380499
|
modelNotes.push(bodyText);
|
|
380736
380500
|
if (/^##!\s*experimental\b[\s\S]*\bgivens\b/.test(noteText))
|
|
@@ -380739,23 +380503,8 @@ function lintNotebookText(modelPath, text) {
|
|
|
380739
380503
|
if (!artifact && note.start && isArtifactNoteText(noteText)) {
|
|
380740
380504
|
artifact = {
|
|
380741
380505
|
text: bodyText,
|
|
380742
|
-
line: lineOfNode(note)
|
|
380743
|
-
startIndex: note.start.startIndex
|
|
380506
|
+
line: lineOfNode(note)
|
|
380744
380507
|
};
|
|
380745
|
-
} else if (!artifact && inNotebooks) {
|
|
380746
|
-
if (route === MARKDOWN_ROUTE || route === "text") {
|
|
380747
|
-
aboveArtifact.push({
|
|
380748
|
-
line: lineOfNode(note),
|
|
380749
|
-
code: "notebook-markdown-above-artifact",
|
|
380750
|
-
what: quoted(first)
|
|
380751
|
-
});
|
|
380752
|
-
} else if (!/^##!/.test(noteText) && !/^#{1,2}\|?"/.test(noteText)) {
|
|
380753
|
-
aboveArtifact.push({
|
|
380754
|
-
line: lineOfNode(note),
|
|
380755
|
-
code: "notebook-tag-above-artifact",
|
|
380756
|
-
what: quoted(first)
|
|
380757
|
-
});
|
|
380758
|
-
}
|
|
380759
380508
|
}
|
|
380760
380509
|
}
|
|
380761
380510
|
const last = notes[notes.length - 1];
|
|
@@ -380767,35 +380516,15 @@ function lintNotebookText(modelPath, text) {
|
|
|
380767
380516
|
if (callAccessor(child, "defineGivenStatement")) {
|
|
380768
380517
|
firstGiven ??= keywordLine(child, "GIVEN");
|
|
380769
380518
|
}
|
|
380770
|
-
if (
|
|
380519
|
+
if (linear && callAccessor(child, "runStatement")) {
|
|
380771
380520
|
const line = keywordLine(child, "RUN");
|
|
380772
380521
|
add(line, "notebook-layout-run", "a `run:` is never shown in a notebook written as tiles, whose cells are the entries in `tiles=[…]`. Fix: define the query as a `view:` on a source, list `source -> view` in `tiles`, and delete the `run:`.");
|
|
380773
380522
|
}
|
|
380774
380523
|
if (callAccessor(child, "ignoredObjectAnnotations")) {
|
|
380775
380524
|
const notes = leadingNotes(child);
|
|
380776
380525
|
for (const note of notes.filter((n) => isMarkdownNote(n.text))) {
|
|
380777
|
-
add(note.line, "notebook-markdown-attached-nowhere", `\`${quoted(note.text)}\` annotates no statement, since what follows it (the end of the file, an import or export, or a \`##\` model-level note) takes no annotation. Fix: ${
|
|
380778
|
-
}
|
|
380779
|
-
if (inNotebooks && notes.some((n) => !isMarkdownNote(n.text))) {
|
|
380780
|
-
const next = children[index + 1];
|
|
380781
|
-
const nextRun = children.slice(index + 1).find((sibling) => callAccessor(sibling, "runStatement"));
|
|
380782
|
-
add(lineOfNode(child), "notebook-orphaned-tag", `this # tag is followed by ${describeNext(next)}, not by a run:, so it annotates nothing. Render tags sit directly above their run:. Fix: move the tag, and any #" caption, directly above ${nextRun ? `the run: on line ${keywordLine(nextRun, "RUN")}` : "its run:"}.`);
|
|
380783
|
-
}
|
|
380784
|
-
} else if (inNotebooks && !artifact) {
|
|
380785
|
-
for (const note of leadingNotes(child).filter((n) => isMarkdownNote(n.text))) {
|
|
380786
|
-
aboveArtifact.push({
|
|
380787
|
-
line: note.line,
|
|
380788
|
-
code: "notebook-markdown-above-artifact",
|
|
380789
|
-
what: quoted(note.text)
|
|
380790
|
-
});
|
|
380526
|
+
add(note.line, "notebook-markdown-attached-nowhere", `\`${quoted(note.text)}\` annotates no statement, since what follows it (the end of the file, an import or export, or a \`##\` model-level note) takes no annotation. Fix: ${tileEntryFix("name")}, with the text as its body, or delete it.`, "error");
|
|
380791
380527
|
}
|
|
380792
|
-
const first = firstCodeToken(child);
|
|
380793
|
-
aboveArtifact.push({
|
|
380794
|
-
line: first?.line ?? lineOfNode(child),
|
|
380795
|
-
code: "notebook-statement-above-artifact",
|
|
380796
|
-
what: quoted(first ? text.slice(map[first.startIndex]).split(/\r?\n/, 1)[0] : nodeText(child).split(`
|
|
380797
|
-
`, 1)[0])
|
|
380798
|
-
});
|
|
380799
380528
|
}
|
|
380800
380529
|
});
|
|
380801
380530
|
if (!artifact)
|
|
@@ -380810,44 +380539,13 @@ function lintNotebookText(modelPath, text) {
|
|
|
380810
380539
|
leadingNoteLines.add(note.line);
|
|
380811
380540
|
lintBlocks();
|
|
380812
380541
|
lintHeadings();
|
|
380813
|
-
for (const { line, code, what } of aboveArtifact) {
|
|
380814
|
-
add(line, code, `\`${what}\` sits above the \`## artifact\` tag, and only \`##!\` flags, \`//\` comments and \`"\` notes may. Fix: move it below the artifact tag.`, "error");
|
|
380815
|
-
}
|
|
380816
380542
|
if (firstGiven !== undefined && !givensEnabled) {
|
|
380817
380543
|
add(firstGiven, "notebook-givens-not-enabled", "`given:` needs the givens experiment, which this file does not enable, so it does not compile. Fix: add the line `##! experimental.givens` at the top of the file.");
|
|
380818
380544
|
}
|
|
380819
|
-
|
|
380820
|
-
|
|
380821
|
-
|
|
380822
|
-
lintUnreferencedMarkdownBlocks(artifact);
|
|
380823
|
-
if (artifact && !inNotebooks)
|
|
380824
|
-
lintDescriptionBelow(artifact.line);
|
|
380825
|
-
if (inNotebooks && artifact)
|
|
380826
|
-
lintComments(artifact.startIndex);
|
|
380545
|
+
lintArtifact(artifact);
|
|
380546
|
+
lintUnreferencedMarkdownBlocks(artifact);
|
|
380547
|
+
lintDescriptionBelow(artifact.line);
|
|
380827
380548
|
return findings.sort((a, b3) => a.line - b3.line);
|
|
380828
|
-
function lintComments(from) {
|
|
380829
|
-
const all3 = tokens;
|
|
380830
|
-
const isComment = (t3) => t3.channel !== 0 && /COMMENT/.test(symbolOf(t3) ?? "");
|
|
380831
|
-
const endLine = (t3) => t3.line + (tokenText(t3).replace(/[\r\n]+$/, "").match(/\n/g)?.length ?? 0);
|
|
380832
|
-
const isNoteToken = (t3) => children.some((c3) => c3.start !== undefined && c3.stop !== undefined && t3.startIndex >= c3.start.startIndex && t3.stopIndex <= c3.stop.stopIndex && callAccessor(c3, "docAnnotations") !== undefined);
|
|
380833
|
-
const attached = [];
|
|
380834
|
-
for (let i2 = all3.length - 1;i2 >= 0; i2--) {
|
|
380835
|
-
const next = all3[i2 + 1];
|
|
380836
|
-
attached[i2] = next !== undefined && symbolOf(next) !== "EOF" && next.line === endLine(all3[i2]) + 1 && (isComment(next) ? attached[i2 + 1] : !isNoteToken(next));
|
|
380837
|
-
}
|
|
380838
|
-
all3.forEach((token, i2) => {
|
|
380839
|
-
if (!isComment(token) || token.startIndex < from)
|
|
380840
|
-
return;
|
|
380841
|
-
if (!attached[i2])
|
|
380842
|
-
return;
|
|
380843
|
-
const before = all3[i2 - 1];
|
|
380844
|
-
if (before && endLine(before) === token.line)
|
|
380845
|
-
return;
|
|
380846
|
-
if (spans.some(([lo, hi]) => token.startIndex >= lo && token.stopIndex <= hi))
|
|
380847
|
-
return;
|
|
380848
|
-
add(token.line, "notebook-comment-not-shown", "this comment sits directly above a cell but is not part of it, so the notebook does not show it. Fix: write it as a `##(markdown)` prose note, or move it inside the statement it describes.");
|
|
380849
|
-
});
|
|
380850
|
-
}
|
|
380851
380549
|
function leadingNotes(node) {
|
|
380852
380550
|
const from = node.start?.startIndex ?? 0;
|
|
380853
380551
|
const to2 = node.stop?.stopIndex ?? -1;
|
|
@@ -380870,24 +380568,6 @@ function lintNotebookText(modelPath, text) {
|
|
|
380870
380568
|
}
|
|
380871
380569
|
return notes;
|
|
380872
380570
|
}
|
|
380873
|
-
function describeNext(next) {
|
|
380874
|
-
if (!next)
|
|
380875
|
-
return "the end of the file";
|
|
380876
|
-
if (!isRuleNode(next)) {
|
|
380877
|
-
const token = next.symbol;
|
|
380878
|
-
return !token || symbolOf(token) === "EOF" ? "the end of the file" : `\`${tokenText(token)}\``;
|
|
380879
|
-
}
|
|
380880
|
-
if (callAccessor(next, "docAnnotations"))
|
|
380881
|
-
return "a note";
|
|
380882
|
-
const first = nodeText(next).trim().split(`
|
|
380883
|
-
`)[0].slice(0, 40);
|
|
380884
|
-
return `\`${first}\``;
|
|
380885
|
-
}
|
|
380886
|
-
function firstCodeToken(node) {
|
|
380887
|
-
const from = node.start?.startIndex ?? 0;
|
|
380888
|
-
const to2 = node.stop?.stopIndex ?? -1;
|
|
380889
|
-
return tokens?.find((t3) => t3.channel === 0 && t3.startIndex >= from && t3.stopIndex <= to2 && !/ANNOTATION/.test(symbolOf(t3) ?? ""));
|
|
380890
|
-
}
|
|
380891
380571
|
function keywordLine(node, keyword) {
|
|
380892
380572
|
const from = node.start?.startIndex ?? 0;
|
|
380893
380573
|
const to2 = node.stop?.stopIndex ?? -1;
|
|
@@ -380906,26 +380586,26 @@ function lintNotebookText(modelPath, text) {
|
|
|
380906
380586
|
const line = tokenText(token).replace(/\r?\n$/, "");
|
|
380907
380587
|
if (/^##"\S/.test(line)) {
|
|
380908
380588
|
const rest = quoted(line).slice(3);
|
|
380909
|
-
const fix = belowTag(token.line) ?
|
|
380589
|
+
const fix = belowTag(token.line) ? `Fix: ${tileEntryFix("name")}, with the text as its body.` : `Did you mean \`##" ${rest}\`?`;
|
|
380910
380590
|
add(token.line, "notebook-block-opener-spacing", `\`${quoted(line)}\` has no space after the route, so Malloy drops the note. ${fix}`);
|
|
380911
380591
|
continue;
|
|
380912
380592
|
}
|
|
380913
380593
|
if (/^##\(markdown\)\S/.test(line)) {
|
|
380914
|
-
const fix =
|
|
380594
|
+
const fix = `Fix: ${tileEntryFix("name")}, with the text as its body.`;
|
|
380915
380595
|
add(token.line, "notebook-block-opener-spacing", `\`${quoted(line)}\` has no space after the route, so Malloy drops the note. ${fix}`);
|
|
380916
380596
|
continue;
|
|
380917
380597
|
}
|
|
380918
380598
|
if (/^##\(?markdown\)?(?=[ \t]|$)/i.test(line) && !isMarkdownNote(line)) {
|
|
380919
|
-
add(token.line, "notebook-markdown-opener", `\`${quoted(line)}\` is not on the \`(markdown)\` route, so it is not shown as prose.
|
|
380599
|
+
add(token.line, "notebook-markdown-opener", `\`${quoted(line)}\` is not on the \`(markdown)\` route, so it is not shown as prose. Fix: ${tileEntryFix("name")}, with the text as its body.`);
|
|
380920
380600
|
continue;
|
|
380921
380601
|
}
|
|
380922
|
-
if (
|
|
380602
|
+
if (isMarkdownNote(line) || routeOfNote(line) === "text") {
|
|
380923
380603
|
add(token.line, "notebook-markdown-block-unnamed", `\`${quoted(line)}\` is a floating \`(${routeOfNote(line)})\` line, which a ${page} does not show, since its text tiles are named blocks listed in \`tiles=[…]\`. Fix: ${tileEntryFix("name")}, or delete the line.`);
|
|
380924
380604
|
continue;
|
|
380925
380605
|
}
|
|
380926
380606
|
const content = /^##[ \t]+(\S.*)$/.exec(line)?.[1];
|
|
380927
380607
|
if (content && /^[A-Za-z_]\w*[ \t]+[A-Za-z0-9_]/.test(content) && !/[={]/.test(content)) {
|
|
380928
|
-
const fix = !belowTag(token.line) ? 'Did you mean `##"`?' :
|
|
380608
|
+
const fix = !belowTag(token.line) ? 'Did you mean `##"`?' : `Fix: ${tileEntryFix("name")}, with the text as its body.`;
|
|
380929
380609
|
add(token.line, "notebook-heading-line", `\`${quoted(line)}\` is read as model tags, not shown as prose. ${fix}`);
|
|
380930
380610
|
}
|
|
380931
380611
|
}
|
|
@@ -380944,13 +380624,11 @@ function lintNotebookText(modelPath, text) {
|
|
|
380944
380624
|
return `this \`|##\` closes the block, so the text after it on line ${strayLine} is not prose and does not compile. Fix: a body line cannot start with \`|##\`, so reword it if the block should go on, or delete the stray text if the block is over.`;
|
|
380945
380625
|
}
|
|
380946
380626
|
function nestedMarkdownMessage(note) {
|
|
380947
|
-
return `\`${quoted(note)}\` sits inside a statement, where nothing reads a \`(markdown)\` note, so it is not shown. Fix:
|
|
380627
|
+
return `\`${quoted(note)}\` sits inside a statement, where nothing reads a \`(markdown)\` note, so it is not shown. Fix: a ${page} reads no attached note, so ${tileEntryFix("name")}, with the text as its body, or delete it.`;
|
|
380948
380628
|
}
|
|
380949
380629
|
function placement(sigil, line) {
|
|
380950
380630
|
if (sigil === "#" && !leadingNoteLines.has(line))
|
|
380951
|
-
return
|
|
380952
|
-
if (sigil === "##" && inNotebooks && !belowTag(line))
|
|
380953
|
-
return " It also sits above the `## artifact` tag, where a `(markdown)` note is not read, so put it below the tag.";
|
|
380631
|
+
return " It also sits inside a statement, and a dashboard reads no attached note, so use a text tile or delete it.";
|
|
380954
380632
|
return "";
|
|
380955
380633
|
}
|
|
380956
380634
|
function lintBlocks() {
|
|
@@ -380969,11 +380647,11 @@ function lintNotebookText(modelPath, text) {
|
|
|
380969
380647
|
const markdown = parseMarkdownOpener(opener);
|
|
380970
380648
|
const spelled = `(${markdown?.route})`;
|
|
380971
380649
|
const topLevel = sigil === "##" || leadingNoteLines.has(line);
|
|
380972
|
-
const dashboardTile = sigil === "##"
|
|
380650
|
+
const dashboardTile = sigil === "##";
|
|
380973
380651
|
const glued = /^("|\(markdown\))(\S.*)$/.exec(rest);
|
|
380974
380652
|
if (glued) {
|
|
380975
380653
|
const [, glueRoute, after] = glued;
|
|
380976
|
-
const description2 = glueRoute === '"'
|
|
380654
|
+
const description2 = glueRoute === '"';
|
|
380977
380655
|
const asTile = dashboardTile && (glueRoute !== '"' || belowTag(line));
|
|
380978
380656
|
const fix = !asTile ? `write \`${sigil}|${description2 ? '"' : "(markdown)"}\` and put \`${after}\` on the line below it` : TEXT_BLOCK_NAME.test(after) ? `put a space after the route, as in \`##|(markdown) ${after}\`, and list \`${after} { kind=text }\` in \`tiles\`` : tileBlockFix("name", after);
|
|
380979
380657
|
add(line, "notebook-block-opener-spacing", `\`${quoted(opener)}\` has no space after the route, so Malloy drops the note. Fix: ${fix}.${asTile || glueRoute === '"' ? "" : placement(sigil, line)}`);
|
|
@@ -380985,13 +380663,13 @@ function lintNotebookText(modelPath, text) {
|
|
|
380985
380663
|
add(line, "notebook-markdown-opener-text", `\`${quoted(markdown.rest)}\` is not a valid name for a \`${spelled}\` tile, which takes one bare word of letters, digits and underscores that does not start with a digit. Fix: ${tileEntryFix(asTileName(markdown.rest))}.`, "error");
|
|
380986
380664
|
} else if (opensText) {
|
|
380987
380665
|
add(line, "notebook-markdown-opener-text", `\`${quoted(markdown.rest)}\` follows \`${sigil}|${spelled}\` on its opener line, where only one bare word may go (a name), so the block would show it as its first line. Fix: ${dashboardTile ? tileBlockFix("name", markdown.rest) : "move it into the body, on the line below the opener"}.${placement(sigil, line)}`, "error");
|
|
380988
|
-
} else if (markdown.name &&
|
|
380989
|
-
add(line, "notebook-markdown-block-named", `the name \`${markdown.name}\` on this \`${spelled}\` block means nothing
|
|
380666
|
+
} else if (markdown.name && sigil === "#") {
|
|
380667
|
+
add(line, "notebook-markdown-block-named", `the name \`${markdown.name}\` on this \`${spelled}\` block means nothing on a block attached to a statement, and it is not shown. Fix: remove the name.`);
|
|
380990
380668
|
} else if (!markdown.name && dashboardTile) {
|
|
380991
380669
|
add(line, "notebook-markdown-block-unnamed", `an unnamed \`${spelled}\` block is not shown on a ${page}, whose text tiles are named blocks listed in \`tiles=[…]\`. Fix: ${tileEntryFix("name")}, or delete the block.`);
|
|
380992
380670
|
}
|
|
380993
380671
|
} else if (/^[ \t]*\(?markdown\)?(?=[ \t]|$)/i.test(rest)) {
|
|
380994
|
-
add(line, "notebook-markdown-opener", `\`${opener.trim()}\` opens a block that is not on the \`(markdown)\` route, so its body is not ${sigil === "#" ? "the statement's prose" :
|
|
380672
|
+
add(line, "notebook-markdown-opener", `\`${opener.trim()}\` opens a block that is not on the \`(markdown)\` route, so its body is not ${sigil === "#" ? "the statement's prose" : "a text tile"}. ${dashboardTile ? `Fix: ${tileEntryFix("name")}.` : `Did you mean \`${sigil}|(markdown)\`?${placement(sigil, line)}`}`);
|
|
380995
380673
|
}
|
|
380996
380674
|
let j3 = i2 + 1;
|
|
380997
380675
|
let firstRun;
|
|
@@ -381014,7 +380692,7 @@ function lintNotebookText(modelPath, text) {
|
|
|
381014
380692
|
}
|
|
381015
380693
|
const trailing = tokenText(end).replace(closer, "").trim();
|
|
381016
380694
|
if (tokenText(end).startsWith(closer) && trailing) {
|
|
381017
|
-
const own2 = sigil !== "##" ? "" : !belowTag(end.line) ? 'on its own `##"` line, or ' :
|
|
380695
|
+
const own2 = sigil !== "##" ? "" : !belowTag(end.line) ? 'on its own `##"` line, or ' : `in its own text tile (${tileEntryFix("name")}), or `;
|
|
381018
380696
|
add(end.line, "notebook-text-after-closer", `the text after the closing \`${closer}\` (\`${trailing}\`) is dropped, not shown. Fix: put it ${own2}inside the block.`);
|
|
381019
380697
|
}
|
|
381020
380698
|
}
|
|
@@ -381030,23 +380708,35 @@ function lintNotebookText(modelPath, text) {
|
|
|
381030
380708
|
}
|
|
381031
380709
|
function lintLayout(line, declaredLayout) {
|
|
381032
380710
|
if (declaredLayout !== undefined && declaredLayout !== "grid" && declaredLayout !== "linear") {
|
|
381033
|
-
add(line,
|
|
381034
|
-
}
|
|
381035
|
-
if (tag.status === "report" && tag.decidedBy === "default") {
|
|
381036
|
-
add(line, "report-layout-implicit", "this artifact tag names no `layout` and lists no tiles, so the report is linear only by default. Fix: write `## artifact { layout=linear }` (or `layout=grid` with `tiles=[…]`).");
|
|
380711
|
+
add(line, LAYOUT_UNKNOWN, `\`layout=${declaredLayout}\` is not a layout Publisher knows (grid, linear), so the tag's other properties decide it. Fix: write \`layout=grid\` or \`layout=linear\`.`, "error");
|
|
381037
380712
|
}
|
|
381038
380713
|
}
|
|
381039
380714
|
function tagFinding(tagNote) {
|
|
381040
|
-
const [
|
|
381041
|
-
const kind = tag.status === "kind-removed" ? tag.kind : undefined;
|
|
381042
|
-
const message = kind !== undefined ? `\`kind=${kind}\` is no longer read, so this file is not a report. Fix: remove \`kind=${kind}\` and write \`layout=grid\` or \`layout=linear\`.` : `the \`## artifact\` tag does not parse${parseError !== undefined ? ` (${parseError})` : ""}, so its properties are not read and this file is not a report. ${/\btiles\s*=/.test(tagNote.text) ? TILE_ENTRY_FIX : "Fix: correct the tag so it parses."}`;
|
|
380715
|
+
const [code, message] = tagFindingText(tagNote.text);
|
|
381043
380716
|
return {
|
|
381044
380717
|
line: tagNote.line,
|
|
381045
|
-
code
|
|
380718
|
+
code,
|
|
381046
380719
|
severity: "error",
|
|
381047
380720
|
message: `Line ${tagNote.line}: ${message}`
|
|
381048
380721
|
};
|
|
381049
380722
|
}
|
|
380723
|
+
function tagFindingText(tagText2) {
|
|
380724
|
+
if (tag.status === "kind-removed")
|
|
380725
|
+
return [
|
|
380726
|
+
"report-kind-removed",
|
|
380727
|
+
`\`kind=${tag.kind}\` is no longer read, so this file is not a report. Fix: remove \`kind=${tag.kind}\` and write \`layout=grid\` or \`layout=linear\`.`
|
|
380728
|
+
];
|
|
380729
|
+
if (tag.status === "cells-removed")
|
|
380730
|
+
return tag.unknownLayout !== undefined ? [
|
|
380731
|
+
LAYOUT_UNKNOWN,
|
|
380732
|
+
`\`layout=${tag.unknownLayout}\` is not a layout Publisher knows (grid, linear), and the tag lists no tiles, so this file is not a report. Fix: write \`layout=grid\` or \`layout=linear\`, and list the tiles in \`tiles=[…]\`.`
|
|
380733
|
+
] : [CELLS_REMOVED, CELLS_REMOVED_MESSAGE];
|
|
380734
|
+
const [parseError] = motlyParseErrors([tagText2]);
|
|
380735
|
+
return [
|
|
380736
|
+
"notebook-artifact-unparsed",
|
|
380737
|
+
`the \`## artifact\` tag does not parse${parseError !== undefined ? ` (${parseError})` : ""}, so its properties are not read and this file is not a report. ${/\btiles\s*=/.test(tagText2) ? TILE_ENTRY_FIX : "Fix: correct the tag so it parses."}`
|
|
380738
|
+
];
|
|
380739
|
+
}
|
|
381050
380740
|
function lintUnreferencedMarkdownBlocks(tagNote) {
|
|
381051
380741
|
if (motlyParseErrors([tagNote.text])[0] !== undefined)
|
|
381052
380742
|
return;
|
|
@@ -381070,9 +380760,7 @@ function lintNotebookText(modelPath, text) {
|
|
|
381070
380760
|
return;
|
|
381071
380761
|
const properties = Object.keys(tag2.dict ?? {});
|
|
381072
380762
|
lintLayout(tagNote.line, tagText(tag2, "layout"));
|
|
381073
|
-
if (
|
|
381074
|
-
return;
|
|
381075
|
-
if (layoutNotebook) {
|
|
380763
|
+
if (linear) {
|
|
381076
380764
|
for (const entry of tag2.array("tiles") ?? []) {
|
|
381077
380765
|
const ignored = ["colspan", "break"].filter((p2) => entry.has(p2));
|
|
381078
380766
|
if (ignored.length === 0)
|
|
@@ -381081,7 +380769,7 @@ function lintNotebookText(modelPath, text) {
|
|
|
381081
380769
|
}
|
|
381082
380770
|
}
|
|
381083
380771
|
const columns = motlyTag(modelNotes)?.tag("dashboard");
|
|
381084
|
-
if (
|
|
380772
|
+
if (linear && columns?.has("columns")) {
|
|
381085
380773
|
const width = tagNumeric(columns, "columns");
|
|
381086
380774
|
if (width !== 1) {
|
|
381087
380775
|
add(tagNote.line, "notebook-columns-ignored", "`dashboard { columns }` other than 1 is ignored on a notebook, which is always one column. Fix: remove `dashboard { columns }`.");
|
|
@@ -381120,6 +380808,286 @@ function notebookLintProblems(modelPath, text, url3) {
|
|
|
381120
380808
|
});
|
|
381121
380809
|
}
|
|
381122
380810
|
|
|
380811
|
+
// src/service/compile_document.ts
|
|
380812
|
+
var DECLARATION = /\b(?:source|query)\s*:\s*(`(?:[^`\\]|\\.)*`|[\p{L}_][\p{L}\p{N}_]*)\s+is\b/giu;
|
|
380813
|
+
function blankSpans(text, cells) {
|
|
380814
|
+
if (cells.length === 0)
|
|
380815
|
+
return text;
|
|
380816
|
+
const units = text.split("");
|
|
380817
|
+
for (const cell of cells) {
|
|
380818
|
+
const span = cellOffsets.get(cell);
|
|
380819
|
+
if (!span)
|
|
380820
|
+
throw new Error("restricted cell has no recorded span");
|
|
380821
|
+
const [start, end] = span;
|
|
380822
|
+
for (let i2 = start;i2 < end && i2 < units.length; i2++) {
|
|
380823
|
+
if (units[i2] !== `
|
|
380824
|
+
` && units[i2] !== "\r")
|
|
380825
|
+
units[i2] = " ";
|
|
380826
|
+
}
|
|
380827
|
+
}
|
|
380828
|
+
return units.join("");
|
|
380829
|
+
}
|
|
380830
|
+
function declaredNames(text) {
|
|
380831
|
+
const found = new Set(buildDerivationBaseMap(text).keys());
|
|
380832
|
+
for (const match of stripMalloyCommentsAndLiterals(text).matchAll(DECLARATION)) {
|
|
380833
|
+
const name = match[1];
|
|
380834
|
+
found.add(name.startsWith("`") ? name.slice(1, -1) : name);
|
|
380835
|
+
}
|
|
380836
|
+
return found;
|
|
380837
|
+
}
|
|
380838
|
+
function readsSource(prepared) {
|
|
380839
|
+
const ref = prepared._query?.structRef;
|
|
380840
|
+
if (typeof ref === "string")
|
|
380841
|
+
return ref;
|
|
380842
|
+
const named = ref;
|
|
380843
|
+
return named?.as || named?.name;
|
|
380844
|
+
}
|
|
380845
|
+
async function denied(gates, text) {
|
|
380846
|
+
try {
|
|
380847
|
+
await gates.text(text);
|
|
380848
|
+
return false;
|
|
380849
|
+
} catch (error2) {
|
|
380850
|
+
if (error2 instanceof AccessDeniedError)
|
|
380851
|
+
return true;
|
|
380852
|
+
throw error2;
|
|
380853
|
+
}
|
|
380854
|
+
}
|
|
380855
|
+
function tileProblem(expression, reason, cause) {
|
|
380856
|
+
return {
|
|
380857
|
+
code: "tile-does-not-compile",
|
|
380858
|
+
severity: "error",
|
|
380859
|
+
message: `Tile "${expression}" ${cause === "parse" ? "could not be parsed" : "does not compile"}: ${reason.replace(/\.?\s*$/, ".")} ` + `Fix: write the tile as one query over a source and a view, such as \`source -> view\` or \`source -> { group_by: field }\`.`
|
|
380860
|
+
};
|
|
380861
|
+
}
|
|
380862
|
+
function problemLine(problem) {
|
|
380863
|
+
const line = problem.at?.range?.start?.line;
|
|
380864
|
+
return line === undefined ? undefined : line + 1;
|
|
380865
|
+
}
|
|
380866
|
+
function cellsRemovedProblems(source) {
|
|
380867
|
+
const problems = notebookLintProblems("document.malloy", source, "").filter((problem) => problem.code === CELLS_REMOVED || problem.code === LAYOUT_UNKNOWN);
|
|
380868
|
+
return problems.length > 0 ? problems : [
|
|
380869
|
+
{
|
|
380870
|
+
code: CELLS_REMOVED,
|
|
380871
|
+
severity: "error",
|
|
380872
|
+
message: CELLS_REMOVED_MESSAGE
|
|
380873
|
+
}
|
|
380874
|
+
];
|
|
380875
|
+
}
|
|
380876
|
+
async function compileDocument(input) {
|
|
380877
|
+
const { base, source, modelName, gates } = input;
|
|
380878
|
+
const tag = readReportTagOfText(source);
|
|
380879
|
+
if (tag.status === "cells-removed")
|
|
380880
|
+
return { problems: cellsRemovedProblems(source) };
|
|
380881
|
+
if (tag.status !== "report")
|
|
380882
|
+
return;
|
|
380883
|
+
const parse5 = parseNotebookText(source);
|
|
380884
|
+
if (isNotebookReaderError(parse5))
|
|
380885
|
+
return;
|
|
380886
|
+
const read = readNotebookCells(parse5, undefined, source);
|
|
380887
|
+
if (read.error) {
|
|
380888
|
+
return { problems: [cellsUnreadableProblem(read.error, "")] };
|
|
380889
|
+
}
|
|
380890
|
+
const code = read.cells.filter((cell) => cell.type === "code");
|
|
380891
|
+
const restrictedCells = new Set;
|
|
380892
|
+
const tainted = new Set;
|
|
380893
|
+
const mentionsTainted = (text) => tainted.size > 0 && [...scanIdentifiers(text)].some((name) => tainted.has(name));
|
|
380894
|
+
for (const cell of code) {
|
|
380895
|
+
if (mentionsTainted(cell.text) || await denied(gates, cell.text)) {
|
|
380896
|
+
restrictedCells.add(cell);
|
|
380897
|
+
if (cell.kind === "definition") {
|
|
380898
|
+
for (const name of declaredNames(cell.text))
|
|
380899
|
+
tainted.add(name);
|
|
380900
|
+
}
|
|
380901
|
+
}
|
|
380902
|
+
}
|
|
380903
|
+
const tileExpressions = readDocumentTileExpressions(read.annotations);
|
|
380904
|
+
const restrictedTiles = new Set;
|
|
380905
|
+
for (const expression of tileExpressions) {
|
|
380906
|
+
const text = `run: ${expression}`;
|
|
380907
|
+
if (mentionsTainted(text) || await denied(gates, text)) {
|
|
380908
|
+
restrictedTiles.add(normalizeTileExpression(expression));
|
|
380909
|
+
}
|
|
380910
|
+
}
|
|
380911
|
+
const definitions = code.filter((cell) => cell.kind === "definition" && !restrictedCells.has(cell)).map((cell) => cell.text).join(`
|
|
380912
|
+
`);
|
|
380913
|
+
const unparsedTiles = new Set;
|
|
380914
|
+
const tileProblems = [];
|
|
380915
|
+
for (const expression of tileExpressions) {
|
|
380916
|
+
const key = normalizeTileExpression(expression);
|
|
380917
|
+
if (restrictedTiles.has(key))
|
|
380918
|
+
continue;
|
|
380919
|
+
try {
|
|
380920
|
+
await gates.constructs(`${definitions}
|
|
380921
|
+
run: ${expression}`);
|
|
380922
|
+
} catch (error2) {
|
|
380923
|
+
if (!(error2 instanceof UnparseableTextError))
|
|
380924
|
+
throw error2;
|
|
380925
|
+
unparsedTiles.add(key);
|
|
380926
|
+
tileProblems.push(tileProblem(expression, error2.detail, "parse"));
|
|
380927
|
+
}
|
|
380928
|
+
}
|
|
380929
|
+
const boundaryProblems = [];
|
|
380930
|
+
const checkBoundary = async (query, line) => {
|
|
380931
|
+
try {
|
|
380932
|
+
await gates.boundary(query, definitions);
|
|
380933
|
+
} catch (error2) {
|
|
380934
|
+
if (!(error2 instanceof OffSurfaceError))
|
|
380935
|
+
throw error2;
|
|
380936
|
+
boundaryProblems.push({
|
|
380937
|
+
code: "query-not-queryable",
|
|
380938
|
+
severity: "error",
|
|
380939
|
+
message: error2.message,
|
|
380940
|
+
...line !== undefined && {
|
|
380941
|
+
at: {
|
|
380942
|
+
url: "",
|
|
380943
|
+
range: {
|
|
380944
|
+
start: { line: line - 1, character: 0 },
|
|
380945
|
+
end: { line: line - 1, character: 0 }
|
|
380946
|
+
}
|
|
380947
|
+
}
|
|
380948
|
+
}
|
|
380949
|
+
});
|
|
380950
|
+
}
|
|
380951
|
+
};
|
|
380952
|
+
for (const cell of code) {
|
|
380953
|
+
if (cell.kind === "query" && !restrictedCells.has(cell)) {
|
|
380954
|
+
await checkBoundary(cell.text, cell.startLine);
|
|
380955
|
+
}
|
|
380956
|
+
}
|
|
380957
|
+
for (const expression of tileExpressions) {
|
|
380958
|
+
const key = normalizeTileExpression(expression);
|
|
380959
|
+
if (!restrictedTiles.has(key) && !unparsedTiles.has(key)) {
|
|
380960
|
+
await checkBoundary(`run: ${expression}`, undefined);
|
|
380961
|
+
}
|
|
380962
|
+
}
|
|
380963
|
+
if (boundaryProblems.length > 0)
|
|
380964
|
+
return { problems: boundaryProblems };
|
|
380965
|
+
const readable = [
|
|
380966
|
+
...code.filter((cell) => cell.kind === "query" && !restrictedCells.has(cell)).map((cell) => cell.text),
|
|
380967
|
+
...tileExpressions.filter((expression) => {
|
|
380968
|
+
const key = normalizeTileExpression(expression);
|
|
380969
|
+
return !restrictedTiles.has(key) && !unparsedTiles.has(key);
|
|
380970
|
+
}).map((expression) => `run: ${expression}`)
|
|
380971
|
+
];
|
|
380972
|
+
for (const query of readable.length > 0 ? readable : [""]) {
|
|
380973
|
+
gates.nameVisible(query, definitions);
|
|
380974
|
+
}
|
|
380975
|
+
const compiledText = blankSpans(source, [...restrictedCells]);
|
|
380976
|
+
await gates.document(compiledText);
|
|
380977
|
+
const extended = base.extendModel(compiledText);
|
|
380978
|
+
let model;
|
|
380979
|
+
try {
|
|
380980
|
+
model = await extended.getModel();
|
|
380981
|
+
} catch (error2) {
|
|
380982
|
+
if (error2 instanceof MalloyError4)
|
|
380983
|
+
return { problems: [...tileProblems, ...error2.problems] };
|
|
380984
|
+
throw error2;
|
|
380985
|
+
}
|
|
380986
|
+
const modelDef = model._modelDef;
|
|
380987
|
+
const registry2 = modelDef.givens ?? {};
|
|
380988
|
+
const sources = extractSourcesFromModelDef(modelDef, []).sources;
|
|
380989
|
+
const settle2 = async (runnable, onDenied) => {
|
|
380990
|
+
try {
|
|
380991
|
+
await gates.compiled(runnable);
|
|
380992
|
+
} catch (error2) {
|
|
380993
|
+
if (error2 instanceof AccessDeniedError)
|
|
380994
|
+
onDenied();
|
|
380995
|
+
else
|
|
380996
|
+
throw error2;
|
|
380997
|
+
}
|
|
380998
|
+
};
|
|
380999
|
+
const compiledBoundary = async (runnable, prepared, query) => {
|
|
381000
|
+
try {
|
|
381001
|
+
await gates.boundaryCompiled(runnable, readsSource(prepared), query, definitions);
|
|
381002
|
+
} catch (error2) {
|
|
381003
|
+
if (!(error2 instanceof OffSurfaceError))
|
|
381004
|
+
throw error2;
|
|
381005
|
+
boundaryProblems.push({
|
|
381006
|
+
code: "query-not-queryable",
|
|
381007
|
+
severity: "error",
|
|
381008
|
+
message: error2.message
|
|
381009
|
+
});
|
|
381010
|
+
}
|
|
381011
|
+
};
|
|
381012
|
+
for (const cell of code) {
|
|
381013
|
+
if (cell.kind !== "query" || restrictedCells.has(cell))
|
|
381014
|
+
continue;
|
|
381015
|
+
const runnable = extended.loadRestrictedQuery(cell.text);
|
|
381016
|
+
let denied2 = false;
|
|
381017
|
+
await settle2(runnable, () => {
|
|
381018
|
+
denied2 = true;
|
|
381019
|
+
restrictedCells.add(cell);
|
|
381020
|
+
});
|
|
381021
|
+
if (denied2)
|
|
381022
|
+
continue;
|
|
381023
|
+
try {
|
|
381024
|
+
await compiledBoundary(runnable, await runnable.getPreparedQuery(), cell.text);
|
|
381025
|
+
} catch (error2) {
|
|
381026
|
+
if (!(error2 instanceof MalloyError4))
|
|
381027
|
+
throw error2;
|
|
381028
|
+
}
|
|
381029
|
+
}
|
|
381030
|
+
const compiledTiles = await compileTileGivens(tileExpressions, { loadQuery: (text) => extended.loadRestrictedQuery(text) }, registry2, (sourceName) => gateGivenSource(sources, sourceName), new Set([...restrictedTiles, ...unparsedTiles]), async (tile, prepared) => {
|
|
381031
|
+
let denied2 = false;
|
|
381032
|
+
await settle2({ getPreparedQuery: async () => prepared }, () => {
|
|
381033
|
+
denied2 = true;
|
|
381034
|
+
restrictedTiles.add(normalizeTileExpression(tile));
|
|
381035
|
+
});
|
|
381036
|
+
if (!denied2) {
|
|
381037
|
+
await compiledBoundary({ getPreparedQuery: async () => prepared }, prepared, `run: ${tile}`);
|
|
381038
|
+
}
|
|
381039
|
+
}, (tile, error2) => {
|
|
381040
|
+
if (!(error2 instanceof MalloyError4))
|
|
381041
|
+
return;
|
|
381042
|
+
gates.nameVisible(`run: ${tile}`, definitions);
|
|
381043
|
+
tileProblems.push(tileProblem(tile, error2.problems.map((problem) => problem.message).join("; "), "compile"));
|
|
381044
|
+
});
|
|
381045
|
+
if (boundaryProblems.length > 0)
|
|
381046
|
+
return { problems: boundaryProblems };
|
|
381047
|
+
const spans = [...restrictedCells];
|
|
381048
|
+
const problems = [...tileProblems, ...model.problems].filter((problem) => {
|
|
381049
|
+
const line = problemLine(problem);
|
|
381050
|
+
return line === undefined || !spans.some((cell) => line >= cell.startLine && line <= cell.endLine);
|
|
381051
|
+
});
|
|
381052
|
+
const slug = modelName.replace(/^.*\//, "").replace(/\.malloy$/, "");
|
|
381053
|
+
const path13 = `${slug}.malloy`;
|
|
381054
|
+
const facts = readReportModelFacts(path13, modelDef, Object.values(registry2).map((given) => given.name), new Map(sources.flatMap((entry) => entry.name ? [
|
|
381055
|
+
[
|
|
381056
|
+
entry.name,
|
|
381057
|
+
gateGivenSource(sources, entry.name) ?? []
|
|
381058
|
+
]
|
|
381059
|
+
] : [])));
|
|
381060
|
+
const manifest = buildReportManifest({ ...facts, compiledTileGivens: compiledTiles }, { restrictedTiles });
|
|
381061
|
+
const cells = read.cells.filter((cell) => cell.kind === "definition").map((cell) => {
|
|
381062
|
+
const restricted = restrictedCells.has(cell);
|
|
381063
|
+
if (cell.kind === "markdown") {
|
|
381064
|
+
return { type: "markdown", kind: "markdown", text: cell.text };
|
|
381065
|
+
}
|
|
381066
|
+
return {
|
|
381067
|
+
type: "code",
|
|
381068
|
+
kind: cell.kind,
|
|
381069
|
+
text: cell.text,
|
|
381070
|
+
...cell.markdown !== undefined && { markdown: cell.markdown },
|
|
381071
|
+
...cell.proseLines && { proseLines: cell.proseLines },
|
|
381072
|
+
...cell.codeLine !== undefined && { codeLine: cell.codeLine },
|
|
381073
|
+
...cell.caption !== undefined && { caption: cell.caption },
|
|
381074
|
+
...restricted && { restricted: true }
|
|
381075
|
+
};
|
|
381076
|
+
});
|
|
381077
|
+
if (!manifest) {
|
|
381078
|
+
return { problems, document: { layout: tag.layout, cells } };
|
|
381079
|
+
}
|
|
381080
|
+
const { entryFile: _entry, ...rest } = manifest;
|
|
381081
|
+
return {
|
|
381082
|
+
problems,
|
|
381083
|
+
document: {
|
|
381084
|
+
layout: manifest.layout,
|
|
381085
|
+
manifest: { ...rest, name: slug },
|
|
381086
|
+
cells
|
|
381087
|
+
}
|
|
381088
|
+
};
|
|
381089
|
+
}
|
|
381090
|
+
|
|
381123
381091
|
// src/service/environment.ts
|
|
381124
381092
|
init_constants();
|
|
381125
381093
|
init_errors();
|
|
@@ -385482,7 +385450,7 @@ function assertEveryCallerJoinPlaced(prepared, found, isCaller) {
|
|
|
385482
385450
|
}
|
|
385483
385451
|
}
|
|
385484
385452
|
}
|
|
385485
|
-
function derivationTerminals(start, basesOf, isModelSource, maxNames = DERIVATION_CHAIN_MAX_NAMES) {
|
|
385453
|
+
function derivationTerminals(start, basesOf, isModelSource, maxNames = DERIVATION_CHAIN_MAX_NAMES, modelQuerySource) {
|
|
385486
385454
|
const seen = new Set;
|
|
385487
385455
|
const terminals = [];
|
|
385488
385456
|
const worklist = [start];
|
|
@@ -385498,8 +385466,13 @@ function derivationTerminals(start, basesOf, isModelSource, maxNames = DERIVATIO
|
|
|
385498
385466
|
continue;
|
|
385499
385467
|
}
|
|
385500
385468
|
const bases = basesOf.get(name);
|
|
385501
|
-
if (!bases || bases.size === 0)
|
|
385502
|
-
|
|
385469
|
+
if (!bases || bases.size === 0) {
|
|
385470
|
+
const querySource = modelQuerySource?.(name);
|
|
385471
|
+
if (!querySource)
|
|
385472
|
+
return { proven: false, at: name };
|
|
385473
|
+
worklist.push(querySource);
|
|
385474
|
+
continue;
|
|
385475
|
+
}
|
|
385503
385476
|
for (const base of bases)
|
|
385504
385477
|
worklist.push(base);
|
|
385505
385478
|
}
|
|
@@ -385524,7 +385497,7 @@ function resolveCallerJoin(callerJoin, compiledModelDef, region, context) {
|
|
|
385524
385497
|
if (forBoundary)
|
|
385525
385498
|
boundaryNames.add(name);
|
|
385526
385499
|
};
|
|
385527
|
-
const chainOf = (start) => derivationTerminals(start, text.derivations(), (name) => context.isModelSource(name));
|
|
385500
|
+
const chainOf = (start) => derivationTerminals(start, text.derivations(), (name) => context.isModelSource(name), undefined, (name) => context.modelQuerySource(name));
|
|
385528
385501
|
const byContentsKey = (key, forBoundary) => {
|
|
385529
385502
|
const entry = contentsStruct(compiledModelDef, key);
|
|
385530
385503
|
if (entry && isCaller(entry.location)) {
|
|
@@ -385538,12 +385511,12 @@ function resolveCallerJoin(callerJoin, compiledModelDef, region, context) {
|
|
|
385538
385511
|
boundaryUnprovenAt ??= chain2.at;
|
|
385539
385512
|
}
|
|
385540
385513
|
}
|
|
385541
|
-
return
|
|
385514
|
+
return "caller";
|
|
385542
385515
|
}
|
|
385543
385516
|
const name = entry ? aliasOf(entry) : key;
|
|
385544
385517
|
if (context.isModelSource(name)) {
|
|
385545
385518
|
add(name, forBoundary);
|
|
385546
|
-
return
|
|
385519
|
+
return "model";
|
|
385547
385520
|
}
|
|
385548
385521
|
if (entry)
|
|
385549
385522
|
return fail2(name);
|
|
@@ -385552,7 +385525,7 @@ function resolveCallerJoin(callerJoin, compiledModelDef, region, context) {
|
|
|
385552
385525
|
return fail2(chain.at);
|
|
385553
385526
|
for (const terminal of chain.terminals)
|
|
385554
385527
|
add(terminal, forBoundary);
|
|
385555
|
-
return
|
|
385528
|
+
return "chain";
|
|
385556
385529
|
};
|
|
385557
385530
|
const isUnmodifiedModelQuery = (q3) => {
|
|
385558
385531
|
if (!q3.name || !context.isModelQuery(q3.name))
|
|
@@ -385564,16 +385537,40 @@ function resolveCallerJoin(callerJoin, compiledModelDef, region, context) {
|
|
|
385564
385537
|
const keys = Object.entries(compiledModelDef.contents).filter(([, value]) => isSourceDef7(value) && value.sourceID === sourceID).map(([key]) => key);
|
|
385565
385538
|
if (keys.length === 0)
|
|
385566
385539
|
return fail2(sourceID);
|
|
385567
|
-
|
|
385540
|
+
let anchor = "model";
|
|
385541
|
+
for (const key of keys) {
|
|
385542
|
+
const got = byContentsKey(key, forBoundary);
|
|
385543
|
+
if (!got)
|
|
385544
|
+
return false;
|
|
385545
|
+
if (got !== "model")
|
|
385546
|
+
anchor = got;
|
|
385547
|
+
}
|
|
385548
|
+
return anchor;
|
|
385549
|
+
};
|
|
385550
|
+
const modelDeclaresSourceID = (sourceID) => {
|
|
385551
|
+
const entries = Object.entries(compiledModelDef.contents).filter(([, value]) => isSourceDef7(value) && value.sourceID === sourceID);
|
|
385552
|
+
return entries.length > 0 && entries.every(([key, value]) => !isCaller(value.location) && context.isModelSource(aliasOf(value) ?? key));
|
|
385568
385553
|
};
|
|
385569
385554
|
const resolveStruct = (struct, isJoinItself, forBoundary, depth) => {
|
|
385570
385555
|
if (depth > RESOLVE_MAX_DEPTH)
|
|
385571
385556
|
return fail2(callerJoin.alias);
|
|
385572
385557
|
let anchored = false;
|
|
385558
|
+
let declaredByModel = false;
|
|
385573
385559
|
if (struct.sourceID) {
|
|
385574
|
-
|
|
385560
|
+
const anchor = bySourceID(struct.sourceID, forBoundary);
|
|
385561
|
+
if (!anchor)
|
|
385575
385562
|
return false;
|
|
385576
385563
|
anchored = true;
|
|
385564
|
+
declaredByModel = anchor === "model";
|
|
385565
|
+
} else {
|
|
385566
|
+
const extendsID = struct.extends;
|
|
385567
|
+
if (extendsID && modelDeclaresSourceID(extendsID)) {
|
|
385568
|
+
const anchor = bySourceID(extendsID, forBoundary);
|
|
385569
|
+
if (!anchor)
|
|
385570
|
+
return false;
|
|
385571
|
+
anchored = true;
|
|
385572
|
+
declaredByModel = anchor === "model";
|
|
385573
|
+
}
|
|
385577
385574
|
}
|
|
385578
385575
|
if (struct.type === "composite") {
|
|
385579
385576
|
composite = true;
|
|
@@ -385594,15 +385591,17 @@ function resolveCallerJoin(callerJoin, compiledModelDef, region, context) {
|
|
|
385594
385591
|
if (isJoinItself && q3 && isUnmodifiedModelQuery(q3)) {
|
|
385595
385592
|
curatedQuery = q3.name;
|
|
385596
385593
|
}
|
|
385597
|
-
|
|
385598
|
-
|
|
385599
|
-
if (
|
|
385600
|
-
|
|
385601
|
-
|
|
385602
|
-
if (
|
|
385603
|
-
|
|
385604
|
-
|
|
385605
|
-
|
|
385594
|
+
if (!declaredByModel) {
|
|
385595
|
+
const ref = q3?.structRef;
|
|
385596
|
+
if (typeof ref === "string") {
|
|
385597
|
+
if (!byContentsKey(ref, forBoundary))
|
|
385598
|
+
return false;
|
|
385599
|
+
} else if (ref && typeof ref === "object") {
|
|
385600
|
+
if (!resolveStruct(ref, false, forBoundary, depth + 1))
|
|
385601
|
+
return false;
|
|
385602
|
+
} else {
|
|
385603
|
+
return fail2(callerJoin.alias);
|
|
385604
|
+
}
|
|
385606
385605
|
}
|
|
385607
385606
|
anchored = true;
|
|
385608
385607
|
}
|
|
@@ -386128,7 +386127,47 @@ function fieldUsageClosure(declaring, refSummary) {
|
|
|
386128
386127
|
}
|
|
386129
386128
|
return { paths, truncated: false };
|
|
386130
386129
|
}
|
|
386131
|
-
function
|
|
386130
|
+
function isCompositePlaceholder(field) {
|
|
386131
|
+
return field.e?.node === "compositeField";
|
|
386132
|
+
}
|
|
386133
|
+
function isComposite(struct) {
|
|
386134
|
+
return struct?.type === "composite";
|
|
386135
|
+
}
|
|
386136
|
+
function compositeMembers(composite, out = [], depth = 0) {
|
|
386137
|
+
if (depth > MAX_JOIN_RECURSION_DEPTH)
|
|
386138
|
+
return out;
|
|
386139
|
+
const members = composite.sources;
|
|
386140
|
+
for (const member of members ?? []) {
|
|
386141
|
+
out.push(member);
|
|
386142
|
+
if (isComposite(member))
|
|
386143
|
+
compositeMembers(member, out, depth + 1);
|
|
386144
|
+
}
|
|
386145
|
+
return out;
|
|
386146
|
+
}
|
|
386147
|
+
function withCompositeMembersResolved(declaring, executed, composite) {
|
|
386148
|
+
if (!composite || !isComposite(declaring) || !isComposite(composite)) {
|
|
386149
|
+
return declaring;
|
|
386150
|
+
}
|
|
386151
|
+
const leaf = (member) => !isComposite(member);
|
|
386152
|
+
const declared = new Set(compositeMembers(declaring).filter(leaf).map((member) => JSON.stringify(strip(member))));
|
|
386153
|
+
const memberFields = new Set(compositeMembers(composite).filter((member) => leaf(member) && declared.has(JSON.stringify(strip(member)))).flatMap((member) => member.fields ?? []));
|
|
386154
|
+
let resolved = false;
|
|
386155
|
+
const fields = (declaring.fields ?? []).map((field) => {
|
|
386156
|
+
if (!isCompositePlaceholder(field))
|
|
386157
|
+
return field;
|
|
386158
|
+
const executedField = executed.fields?.find((f5) => activeName2(f5) === activeName2(field));
|
|
386159
|
+
if (!executedField || !memberFields.has(executedField))
|
|
386160
|
+
return field;
|
|
386161
|
+
resolved = true;
|
|
386162
|
+
return executedField;
|
|
386163
|
+
});
|
|
386164
|
+
return resolved ? { ...declaring, fields } : declaring;
|
|
386165
|
+
}
|
|
386166
|
+
function membersDeclaringCondition(composite, condition) {
|
|
386167
|
+
return compositeMembers(composite).filter((member) => member.filterList?.includes(condition));
|
|
386168
|
+
}
|
|
386169
|
+
function assertFilterConditionBindsToDeclaringSource(declaringStructAsWritten, executedStruct, condition, executedComposite = undefined) {
|
|
386170
|
+
const declaringStruct = withCompositeMembersResolved(declaringStructAsWritten, executedStruct, executedComposite);
|
|
386132
386171
|
const { paths, truncated, unrecordedJoin } = fieldUsageClosure(declaringStruct, condition.refSummary);
|
|
386133
386172
|
if (truncated) {
|
|
386134
386173
|
throw new Error("a row-security filter's field-usage closure exceeded the resolution bound");
|
|
@@ -386283,9 +386322,6 @@ function assertGraftedGateBindsToDeclaringSource(entryPointStruct, executedStruc
|
|
|
386283
386322
|
}
|
|
386284
386323
|
assertFilterConditionBindsToDeclaringSource(declaring, executedStruct, condition);
|
|
386285
386324
|
}
|
|
386286
|
-
function rangesEqual(a, b3) {
|
|
386287
|
-
return a.start.line === b3.start.line && a.start.character === b3.start.character && a.end.line === b3.end.line && a.end.character === b3.end.character;
|
|
386288
|
-
}
|
|
386289
386325
|
function locationContains(outer, inner) {
|
|
386290
386326
|
if (outer.url !== inner.url)
|
|
386291
386327
|
return false;
|
|
@@ -386333,7 +386369,7 @@ function findSiblingAnnotationDeclaringSource(note, resolveSibling) {
|
|
|
386333
386369
|
if (!isSourceDef8(value))
|
|
386334
386370
|
continue;
|
|
386335
386371
|
for (const candidate of ownLevelNotes2(value.annotations)) {
|
|
386336
|
-
if (candidate.at &&
|
|
386372
|
+
if (candidate.at && sameNotePosition(candidate.at, note.at)) {
|
|
386337
386373
|
return value;
|
|
386338
386374
|
}
|
|
386339
386375
|
}
|
|
@@ -386381,7 +386417,7 @@ function siblingCandidateOwnsCondition(candidate, condition) {
|
|
|
386381
386417
|
if (c3.code !== condition.code)
|
|
386382
386418
|
return false;
|
|
386383
386419
|
const candidateAt = firstFieldUsageLocation(c3);
|
|
386384
|
-
return !!candidateAt &&
|
|
386420
|
+
return !!candidateAt && sameNotePosition(candidateAt, at2);
|
|
386385
386421
|
});
|
|
386386
386422
|
}
|
|
386387
386423
|
function conditionReadsNoField(condition) {
|
|
@@ -386400,13 +386436,14 @@ function isOwnFreshFilter(struct, condition, freshness) {
|
|
|
386400
386436
|
}
|
|
386401
386437
|
return !!freshness.compiledUrl && at2.url === freshness.compiledUrl;
|
|
386402
386438
|
}
|
|
386403
|
-
function assertInheritedSourceFiltersBind(struct, modelDef, freshness = {}, resolveSibling = undefined, depth = 0, visited = new Set, alreadyProven = new Set) {
|
|
386439
|
+
function assertInheritedSourceFiltersBind(struct, modelDef, freshness = {}, resolveSibling = undefined, depth = 0, visited = new Set, alreadyProven = new Set, unresolved = undefined) {
|
|
386404
386440
|
if (visited.has(struct))
|
|
386405
386441
|
return;
|
|
386406
386442
|
if (depth > MAX_JOIN_RECURSION_DEPTH) {
|
|
386407
386443
|
throw new Error("row-security filter recursion exceeded the join-depth resolution bound");
|
|
386408
386444
|
}
|
|
386409
386445
|
visited.add(struct);
|
|
386446
|
+
const composite = isComposite(unresolved) ? unresolved : undefined;
|
|
386410
386447
|
for (const condition of struct.filterList ?? []) {
|
|
386411
386448
|
if (alreadyProven.has(condition))
|
|
386412
386449
|
continue;
|
|
@@ -386414,16 +386451,24 @@ function assertInheritedSourceFiltersBind(struct, modelDef, freshness = {}, reso
|
|
|
386414
386451
|
continue;
|
|
386415
386452
|
const origin2 = findFilterOrigin(struct, condition, modelDef, resolveSibling);
|
|
386416
386453
|
if (origin2 !== struct) {
|
|
386417
|
-
assertFilterConditionBindsToDeclaringSource(origin2, struct, condition);
|
|
386454
|
+
assertFilterConditionBindsToDeclaringSource(origin2, struct, condition, composite);
|
|
386418
386455
|
continue;
|
|
386419
386456
|
}
|
|
386420
386457
|
if (isOwnFreshFilter(struct, condition, freshness))
|
|
386421
386458
|
continue;
|
|
386459
|
+
const members = composite ? membersDeclaringCondition(composite, condition) : [];
|
|
386460
|
+
if (members.length > 0) {
|
|
386461
|
+
for (const member of members) {
|
|
386462
|
+
assertFilterConditionBindsToDeclaringSource(member, struct, condition);
|
|
386463
|
+
}
|
|
386464
|
+
continue;
|
|
386465
|
+
}
|
|
386422
386466
|
throw new Error("a row-security filter's declaring source could not be resolved");
|
|
386423
386467
|
}
|
|
386424
386468
|
for (const field of struct.fields ?? []) {
|
|
386425
386469
|
if (isJoined6(field) && isSourceDef8(field)) {
|
|
386426
|
-
|
|
386470
|
+
const unresolvedJoin = unresolved?.fields?.find((f5) => isJoined6(f5) && activeName2(f5) === activeName2(field));
|
|
386471
|
+
assertInheritedSourceFiltersBind(field, modelDef, freshness, resolveSibling, depth + 1, visited, alreadyProven, unresolvedJoin);
|
|
386427
386472
|
}
|
|
386428
386473
|
}
|
|
386429
386474
|
}
|
|
@@ -386641,6 +386686,9 @@ class Model {
|
|
|
386641
386686
|
preaggregateRollupPlans = [];
|
|
386642
386687
|
preaggregatePlansVersion = 0;
|
|
386643
386688
|
compiledSourceText;
|
|
386689
|
+
reportModelFacts;
|
|
386690
|
+
reportTagCarried;
|
|
386691
|
+
sharedQueryModels = new WeakMap;
|
|
386644
386692
|
compiledReportFacts;
|
|
386645
386693
|
settledCompiledReportFacts;
|
|
386646
386694
|
sources;
|
|
@@ -386991,13 +387039,15 @@ class Model {
|
|
|
386991
387039
|
await this.authorizeAndBindRunnable(runnable, givens, {
|
|
386992
387040
|
bypassAuthorize,
|
|
386993
387041
|
checkOnly: options?.checkOnly,
|
|
387042
|
+
compileCallerText: options?.callerRegion?.text,
|
|
386994
387043
|
callerJoins: options?.callerRegion ? {
|
|
386995
387044
|
runnable,
|
|
386996
387045
|
region: options.callerRegion
|
|
386997
387046
|
} : undefined
|
|
386998
387047
|
});
|
|
386999
387048
|
}
|
|
387000
|
-
async collectAuthorizeEntryPointGates(runnable, givens, graftScope,
|
|
387049
|
+
async collectAuthorizeEntryPointGates(runnable, givens, graftScope, options = {}) {
|
|
387050
|
+
const { skipOwnSourceGate = false, callerText } = options;
|
|
387001
387051
|
const ownSourceName = await this.resolveAuthorizeSourceFromRunnable(runnable);
|
|
387002
387052
|
if (!skipOwnSourceGate) {
|
|
387003
387053
|
await this.assertAuthorized(ownSourceName, givens, false, graftScope);
|
|
@@ -387020,8 +387070,29 @@ class Model {
|
|
|
387020
387070
|
entryPointGates = Array.from(byKey.values());
|
|
387021
387071
|
}
|
|
387022
387072
|
}
|
|
387073
|
+
if (callerText && !skipOwnSourceGate && ownSourceName) {
|
|
387074
|
+
entryPointGates = this.foldRequestDeclaredTwins(ownSourceName, callerText, entryPointGates);
|
|
387075
|
+
}
|
|
387023
387076
|
return { entryPointGates, modelDef, struct };
|
|
387024
387077
|
}
|
|
387078
|
+
foldRequestDeclaredTwins(ownSourceName, callerText, walked) {
|
|
387079
|
+
if (this.entryPointGatesBySource.has(ownSourceName))
|
|
387080
|
+
return walked;
|
|
387081
|
+
const proof = this.requestChainGates(ownSourceName, callerText);
|
|
387082
|
+
if (!proof.proven)
|
|
387083
|
+
return walked;
|
|
387084
|
+
return this.swapForOnDiskTwins(walked, proof.gatesByBase.flatMap(([, gates]) => gates));
|
|
387085
|
+
}
|
|
387086
|
+
swapForOnDiskTwins(walked, twins) {
|
|
387087
|
+
const used = new Set;
|
|
387088
|
+
return walked.map((entry) => {
|
|
387089
|
+
const twin = twins.find((t3) => !used.has(t3) && t3.route === entry.route && t3.exprs.length === entry.exprs.length && t3.exprs.every((expr, i2) => expr === entry.exprs[i2]));
|
|
387090
|
+
if (!twin)
|
|
387091
|
+
return entry;
|
|
387092
|
+
used.add(twin);
|
|
387093
|
+
return twin;
|
|
387094
|
+
});
|
|
387095
|
+
}
|
|
387025
387096
|
async assertAuthorizedFromCompiledRunnable(runnable, givens) {
|
|
387026
387097
|
await this.authorizeAndBindRunnable(runnable, givens, {
|
|
387027
387098
|
skipOwnSourceGate: true,
|
|
@@ -387039,8 +387110,8 @@ class Model {
|
|
|
387039
387110
|
queryHadRowLevelFilterAttached(runnable) {
|
|
387040
387111
|
return this.rowLevelFilteredRunnables.has(runnable);
|
|
387041
387112
|
}
|
|
387042
|
-
async probeEntryPointGates(runnable, givens, graftScope,
|
|
387043
|
-
const { entryPointGates, modelDef } = await this.collectAuthorizeEntryPointGates(runnable, givens, graftScope,
|
|
387113
|
+
async probeEntryPointGates(runnable, givens, graftScope, options = {}) {
|
|
387114
|
+
const { entryPointGates, modelDef } = await this.collectAuthorizeEntryPointGates(runnable, givens, graftScope, options);
|
|
387044
387115
|
const rowLevel = [];
|
|
387045
387116
|
for (const entry of entryPointGates) {
|
|
387046
387117
|
const resolution = modelDef ? await this.resolveGateShape(entry, modelDef, graftScope) : { shape: "rejected", cause: undefined };
|
|
@@ -387089,10 +387160,10 @@ class Model {
|
|
|
387089
387160
|
return;
|
|
387090
387161
|
if (this.entryPointGatesBySource.has(entryPoint))
|
|
387091
387162
|
return;
|
|
387092
|
-
const proof = this.
|
|
387163
|
+
const proof = this.requestChainGates(entryPoint, query);
|
|
387093
387164
|
if (proof.proven) {
|
|
387094
|
-
for (const base of proof.
|
|
387095
|
-
for (const entry of
|
|
387165
|
+
for (const [base, gates] of proof.gatesByBase) {
|
|
387166
|
+
for (const entry of gates) {
|
|
387096
387167
|
const resolution = this.modelDef ? await this.resolveGateShape(entry, this.modelDef, graftScope) : { shape: "rejected", cause: undefined };
|
|
387097
387168
|
if (resolution.shape === "row_level") {
|
|
387098
387169
|
this.denyLaunderedEntryPoint(entryPoint, "row_filter_not_graftable", base);
|
|
@@ -387104,6 +387175,15 @@ class Model {
|
|
|
387104
387175
|
}
|
|
387105
387176
|
this.denyLaunderedEntryPoint(entryPoint, proof.reason, proof.at);
|
|
387106
387177
|
}
|
|
387178
|
+
requestChainGates(entryPoint, text) {
|
|
387179
|
+
const proof = this.requestChainProvesUngated(entryPoint, buildDerivationBaseMap(text));
|
|
387180
|
+
if (!proof.proven)
|
|
387181
|
+
return proof;
|
|
387182
|
+
return {
|
|
387183
|
+
proven: true,
|
|
387184
|
+
gatesByBase: proof.gatedBases.map((base) => [base, this.entryPointGatesBySource.get(base) ?? []])
|
|
387185
|
+
};
|
|
387186
|
+
}
|
|
387107
387187
|
async assertLocksOnRequestDeclaredBases(entryPoint, query, givens, bypassAuthorize) {
|
|
387108
387188
|
if (!this.declaresAnyGate())
|
|
387109
387189
|
return;
|
|
@@ -387141,7 +387221,7 @@ class Model {
|
|
|
387141
387221
|
return this.declaresAnyGateMemo;
|
|
387142
387222
|
}
|
|
387143
387223
|
requestChainProvesUngated(entryPoint, basesOf) {
|
|
387144
|
-
const chain = derivationTerminals(entryPoint, basesOf, (name) => this.entryPointGatesBySource.has(name), REQUEST_CHAIN_MAX_NAMES);
|
|
387224
|
+
const chain = derivationTerminals(entryPoint, basesOf, (name) => this.entryPointGatesBySource.has(name), REQUEST_CHAIN_MAX_NAMES, (name) => this.modelQuerySource(name));
|
|
387145
387225
|
if (!chain.proven) {
|
|
387146
387226
|
return {
|
|
387147
387227
|
proven: false,
|
|
@@ -387154,6 +387234,9 @@ class Model {
|
|
|
387154
387234
|
gatedBases: chain.terminals.filter((name) => (this.entryPointGatesBySource.get(name)?.length ?? 0) > 0)
|
|
387155
387235
|
};
|
|
387156
387236
|
}
|
|
387237
|
+
modelQuerySource(name) {
|
|
387238
|
+
return this.queries?.find((q3) => q3.name === name)?.sourceName;
|
|
387239
|
+
}
|
|
387157
387240
|
resolvedCallerJoins(runnable, region) {
|
|
387158
387241
|
const cached4 = this.callerJoinResolutions.get(runnable);
|
|
387159
387242
|
if (cached4 && cached4.region === region)
|
|
@@ -387178,6 +387261,7 @@ class Model {
|
|
|
387178
387261
|
const context = {
|
|
387179
387262
|
isModelSource: (name) => this.entryPointGatesBySource.has(name),
|
|
387180
387263
|
isModelQuery: (name) => this.modelDef?.contents[name]?.type === "query",
|
|
387264
|
+
modelQuerySource: (name) => this.modelQuerySource(name),
|
|
387181
387265
|
authorUrls
|
|
387182
387266
|
};
|
|
387183
387267
|
return collectCallerJoins(prepared, region, authorUrls).map((callerJoin) => ({
|
|
@@ -387219,7 +387303,9 @@ class Model {
|
|
|
387219
387303
|
async decideCallerSource(source, compiledModelDef, alias, givens, graftScope) {
|
|
387220
387304
|
const gates = this.collectEntryPointGates(source.struct, compiledModelDef, new Set, true);
|
|
387221
387305
|
if (gates.length > 0) {
|
|
387222
|
-
|
|
387306
|
+
const twins = source.chain.proven ? source.chain.terminals.flatMap((base) => this.entryPointGatesBySource.get(base) ?? []) : [];
|
|
387307
|
+
const swapped = this.swapForOnDiskTwins(gates, twins);
|
|
387308
|
+
return Promise.all(swapped.map((entry, i2) => entry === gates[i2] ? this.resolveGateShape(entry, compiledModelDef, graftScope) : this.resolveGateShape(entry, this.modelDef ?? compiledModelDef, graftScope)));
|
|
387223
387309
|
}
|
|
387224
387310
|
if (!source.chain.proven) {
|
|
387225
387311
|
this.denyCallerJoin(alias, "chain_not_established", source.chain.at);
|
|
@@ -387421,7 +387507,10 @@ ${query}`;
|
|
|
387421
387507
|
return runnable;
|
|
387422
387508
|
}
|
|
387423
387509
|
const graftScope = options?.graftScope ?? this.defaultGraftScope();
|
|
387424
|
-
const entryPointRowLevel = await this.probeEntryPointGates(runnable, givens, graftScope,
|
|
387510
|
+
const entryPointRowLevel = await this.probeEntryPointGates(runnable, givens, graftScope, {
|
|
387511
|
+
skipOwnSourceGate: options?.skipOwnSourceGate,
|
|
387512
|
+
callerText: options?.compileCallerText ?? options?.callerQueryText
|
|
387513
|
+
});
|
|
387425
387514
|
if (entryPointRowLevel.length === 0) {
|
|
387426
387515
|
if (options?.callerQueryText && !options?.skipOwnSourceGate) {
|
|
387427
387516
|
await this.assertRequestDeclaredEntryPointIsNotLaundered(runnable, options.callerQueryText, givens, graftScope);
|
|
@@ -387451,6 +387540,7 @@ ${query}`;
|
|
|
387451
387540
|
g2
|
|
387452
387541
|
])).values());
|
|
387453
387542
|
const graftedMaterializer = this.getOrBuildGraftedMaterializer(grafts, graftScope);
|
|
387543
|
+
await this.ensureSharedQueryModel(graftedMaterializer);
|
|
387454
387544
|
const recompiled = options.recompile(graftedMaterializer, grafts);
|
|
387455
387545
|
if (options.proveGraft) {
|
|
387456
387546
|
if (callerJoinRowLevel.length > 0) {
|
|
@@ -387615,14 +387705,15 @@ ${query}`;
|
|
|
387615
387705
|
if (!target)
|
|
387616
387706
|
return;
|
|
387617
387707
|
try {
|
|
387618
|
-
assertInheritedSourceFiltersBind(target, modelDef, { compiledUrl, queryLocation }, this.siblingModelDefResolver, undefined, undefined, alreadyProven);
|
|
387708
|
+
assertInheritedSourceFiltersBind(target, modelDef, { compiledUrl, queryLocation }, this.siblingModelDefResolver, undefined, undefined, alreadyProven, struct);
|
|
387619
387709
|
} catch (err) {
|
|
387620
387710
|
recordRowLevelGateDecision("denied_by_gate", site);
|
|
387621
387711
|
logger.debug("Inherited source filter binding check failed; denying", {
|
|
387622
387712
|
modelPath: this.modelPath,
|
|
387623
387713
|
error: err instanceof Error ? err.message : String(err)
|
|
387624
387714
|
});
|
|
387625
|
-
const
|
|
387715
|
+
const named = struct ?? target;
|
|
387716
|
+
const label = named.as || named.name;
|
|
387626
387717
|
throw new AccessDeniedError(`Access denied for source "${label}".`);
|
|
387627
387718
|
}
|
|
387628
387719
|
}
|
|
@@ -387917,6 +388008,27 @@ ${query}`;
|
|
|
387917
388008
|
model.preparedPreaggregateCompanion = data.preaggregateCompanion;
|
|
387918
388009
|
return model;
|
|
387919
388010
|
}
|
|
388011
|
+
ensureSharedQueryModel(materializer) {
|
|
388012
|
+
if (!materializer)
|
|
388013
|
+
return Promise.resolve();
|
|
388014
|
+
let built = this.sharedQueryModels.get(materializer);
|
|
388015
|
+
if (!built) {
|
|
388016
|
+
built = (async () => {
|
|
388017
|
+
try {
|
|
388018
|
+
const base = await materializer.getModel();
|
|
388019
|
+
base.queryModel;
|
|
388020
|
+
} catch (error2) {
|
|
388021
|
+
logger.warn("Could not build a shared QueryModel; each query builds its own", {
|
|
388022
|
+
packageName: this.packageName,
|
|
388023
|
+
modelPath: this.modelPath,
|
|
388024
|
+
error: error2 instanceof Error ? error2.message : String(error2)
|
|
388025
|
+
});
|
|
388026
|
+
}
|
|
388027
|
+
})();
|
|
388028
|
+
this.sharedQueryModels.set(materializer, built);
|
|
388029
|
+
}
|
|
388030
|
+
return built;
|
|
388031
|
+
}
|
|
387920
388032
|
static fromCompilationError(packageName, modelPath, modelType, error2, sourceText) {
|
|
387921
388033
|
const model = new Model(packageName, modelPath, {}, modelType, undefined, undefined, undefined, undefined, undefined, undefined, error2);
|
|
387922
388034
|
model.compiledSourceText = sourceText;
|
|
@@ -388000,6 +388112,11 @@ ${query}`;
|
|
|
388000
388112
|
return this.compiledSourceText;
|
|
388001
388113
|
}
|
|
388002
388114
|
getReportModelFacts() {
|
|
388115
|
+
return (this.reportModelFacts ??= {
|
|
388116
|
+
value: this.deriveReportModelFacts()
|
|
388117
|
+
}).value;
|
|
388118
|
+
}
|
|
388119
|
+
deriveReportModelFacts() {
|
|
388003
388120
|
if (!this.modelDef)
|
|
388004
388121
|
return;
|
|
388005
388122
|
return readReportModelFacts(this.modelPath, this.modelDef, (this.givens ?? []).map((given) => given.name).filter((name) => name !== undefined), new Map((this.sources ?? []).flatMap((source) => source.name ? [
|
|
@@ -388235,6 +388352,9 @@ ${query}`;
|
|
|
388235
388352
|
return this.modelPath.endsWith(NOTEBOOK_FILE_SUFFIX) ? "malloynb" : "malloy";
|
|
388236
388353
|
}
|
|
388237
388354
|
carriesReportTag() {
|
|
388355
|
+
return this.reportTagCarried ??= this.deriveCarriesReportTag();
|
|
388356
|
+
}
|
|
388357
|
+
deriveCarriesReportTag() {
|
|
388238
388358
|
if (!isReportEligiblePath(this.modelPath))
|
|
388239
388359
|
return false;
|
|
388240
388360
|
const facts = this.getReportModelFacts();
|
|
@@ -388336,7 +388456,9 @@ ${query}`;
|
|
|
388336
388456
|
];
|
|
388337
388457
|
const findings = [];
|
|
388338
388458
|
const carriesReportTag = this.carriesReportTag();
|
|
388459
|
+
const yielder = createEventLoopYielder();
|
|
388339
388460
|
for (const { label, result } of prepared) {
|
|
388461
|
+
await yielder.maybeYield();
|
|
388340
388462
|
const logs = filterPublisherOwnedRenderLogs(validateRenderTags2(result), carriesReportTag);
|
|
388341
388463
|
if (logs.length > 0) {
|
|
388342
388464
|
logger.warn(`Render tag findings on '${label}': ${logs.map((e2) => `[${e2.severity}] ${e2.message}`).join("; ")}`);
|
|
@@ -388353,8 +388475,11 @@ ${query}`;
|
|
|
388353
388475
|
}
|
|
388354
388476
|
preparedRenderTagResults;
|
|
388355
388477
|
async prepareRenderTagTargets(mm, targets) {
|
|
388478
|
+
await this.ensureSharedQueryModel(mm);
|
|
388479
|
+
const yielder = createEventLoopYielder();
|
|
388356
388480
|
const prepared = [];
|
|
388357
388481
|
for (const target of targets) {
|
|
388482
|
+
await yielder.maybeYield();
|
|
388358
388483
|
try {
|
|
388359
388484
|
const result = await mm.loadQuery(target.queryString).getPreparedResult();
|
|
388360
388485
|
prepared.push({
|
|
@@ -388430,48 +388555,19 @@ ${query}`;
|
|
|
388430
388555
|
const read = readNotebookCells(parse5, modelDef, text);
|
|
388431
388556
|
if (read.error)
|
|
388432
388557
|
return refuse(read.error);
|
|
388433
|
-
const anonymousQueries = this.modelInfo?.anonymous_queries ?? [];
|
|
388434
388558
|
this.notebookAnnotations = read.annotations;
|
|
388435
388559
|
const layout = this.readNotebookLayout();
|
|
388436
388560
|
this.notebookLayout = layout;
|
|
388437
|
-
const
|
|
388438
|
-
|
|
388439
|
-
|
|
388440
|
-
|
|
388441
|
-
|
|
388442
|
-
|
|
388443
|
-
|
|
388444
|
-
|
|
388445
|
-
|
|
388446
|
-
|
|
388447
|
-
markdown: cell.markdown,
|
|
388448
|
-
proseLines: cell.proseLines,
|
|
388449
|
-
codeLine: cell.codeLine,
|
|
388450
|
-
caption: cell.caption,
|
|
388451
|
-
modelDef
|
|
388452
|
-
};
|
|
388453
|
-
}
|
|
388454
|
-
const k2 = cell.queryIndex ?? -1;
|
|
388455
|
-
const anonymous = anonymousQueries[k2];
|
|
388456
|
-
const compiled = modelDef.queryList[k2];
|
|
388457
|
-
return {
|
|
388458
|
-
type: "code",
|
|
388459
|
-
kind: "query",
|
|
388460
|
-
text: cell.text,
|
|
388461
|
-
markdown: cell.markdown,
|
|
388462
|
-
proseLines: cell.proseLines,
|
|
388463
|
-
codeLine: cell.codeLine,
|
|
388464
|
-
caption: cell.caption,
|
|
388465
|
-
queryIndex: cell.queryIndex,
|
|
388466
|
-
runnable: materializer.loadQuery(cell.text),
|
|
388467
|
-
modelMaterializer: materializer,
|
|
388468
|
-
modelDef,
|
|
388469
|
-
queryInfo: anonymous && {
|
|
388470
|
-
...anonymous,
|
|
388471
|
-
name: compiled?.as || compiled?.name || ""
|
|
388472
|
-
}
|
|
388473
|
-
};
|
|
388474
|
-
});
|
|
388561
|
+
const cells = read.cells.filter((cell) => cell.kind === "definition").map((cell) => ({
|
|
388562
|
+
type: "code",
|
|
388563
|
+
kind: "definition",
|
|
388564
|
+
text: cell.text,
|
|
388565
|
+
markdown: cell.markdown,
|
|
388566
|
+
proseLines: cell.proseLines,
|
|
388567
|
+
codeLine: cell.codeLine,
|
|
388568
|
+
caption: cell.caption,
|
|
388569
|
+
modelDef
|
|
388570
|
+
}));
|
|
388475
388571
|
if (layout) {
|
|
388476
388572
|
for (const tile of layout.tiles ?? []) {
|
|
388477
388573
|
if (tile.kind === "text") {
|
|
@@ -388538,7 +388634,7 @@ ${query}`;
|
|
|
388538
388634
|
throw readerError;
|
|
388539
388635
|
return this.getNotebookModel();
|
|
388540
388636
|
} else {
|
|
388541
|
-
throw new ModelNotFoundError(`${this.modelPath} is not a valid notebook name. Notebook files must end in .malloynb, or be a .malloy with a model-level ## artifact { layout=linear } tag.`);
|
|
388637
|
+
throw new ModelNotFoundError(`${this.modelPath} is not a valid notebook name. Notebook files must end in .malloynb, or be a .malloy report with a model-level ## artifact { layout=linear tiles=[…] } tag.`);
|
|
388542
388638
|
}
|
|
388543
388639
|
}
|
|
388544
388640
|
setServeDestinationConfig(provider) {
|
|
@@ -388586,6 +388682,7 @@ ${query}`;
|
|
|
388586
388682
|
...ordinary,
|
|
388587
388683
|
...groups.flatMap((g2) => g2.members)
|
|
388588
388684
|
]);
|
|
388685
|
+
await this.ensureSharedQueryModel(this.serveShapeCache.materializer);
|
|
388589
388686
|
const runnable = this.serveShapeCache.materializer.loadRestrictedQuery(queryString);
|
|
388590
388687
|
const probeSQL = await runnable.getSQL({ virtualMap, givens });
|
|
388591
388688
|
const rollupPaths = [
|
|
@@ -388871,6 +388968,7 @@ run: ${sourceName ? `${quoteMalloyIdentifier2(sourceName)} -> ` : ""}${quoteMall
|
|
|
388871
388968
|
}
|
|
388872
388969
|
}
|
|
388873
388970
|
}
|
|
388971
|
+
await this.ensureSharedQueryModel(this.modelMaterializer);
|
|
388874
388972
|
runnable = this.modelMaterializer.loadRestrictedQuery(queryString);
|
|
388875
388973
|
liveRunnable = runnable;
|
|
388876
388974
|
querySurfaceGivens = this.filterGivensToModelSurface(givens, await this.givenNamesReadByQuery(runnable, givens));
|
|
@@ -389222,6 +389320,7 @@ run: ${sourceName ? `${quoteMalloyIdentifier2(sourceName)} -> ` : ""}${quoteMall
|
|
|
389222
389320
|
return {
|
|
389223
389321
|
type: "source",
|
|
389224
389322
|
packageName: this.packageName,
|
|
389323
|
+
path: this.modelPath,
|
|
389225
389324
|
modelPath: this.modelPath,
|
|
389226
389325
|
malloyVersion: MALLOY_VERSION,
|
|
389227
389326
|
dataStyles: JSON.stringify(this.dataStyles),
|
|
@@ -389891,9 +389990,11 @@ init_constants();
|
|
|
389891
389990
|
init_logger();
|
|
389892
389991
|
async function compilePackageBuildPlan(pkg, signal) {
|
|
389893
389992
|
const parts = emptyBuildPlanParts();
|
|
389993
|
+
const yielder = createEventLoopYielder();
|
|
389894
389994
|
for (const modelPath of pkg.getModelPaths()) {
|
|
389895
389995
|
if (!modelPath.endsWith(MODEL_FILE_SUFFIX))
|
|
389896
389996
|
continue;
|
|
389997
|
+
await yielder.maybeYield();
|
|
389897
389998
|
if (signal?.aborted)
|
|
389898
389999
|
throw new Error("Build cancelled");
|
|
389899
390000
|
const { runtime, modelURL, importBaseURL } = await Model.getModelRuntime(pkg.getPackagePath(), modelPath, pkg.getMalloyConfig());
|
|
@@ -391037,8 +391138,11 @@ class Package {
|
|
|
391037
391138
|
const { packageName, packagePath, malloyConfig, buildManifest } = ctx;
|
|
391038
391139
|
const models = new Map;
|
|
391039
391140
|
const renderTagWarnings = [];
|
|
391141
|
+
const timings = { hydrateMs: 0, renderValidationMs: 0 };
|
|
391040
391142
|
const placeholder = (sm, err) => Model.fromCompilationError(packageName, sm.modelPath, sm.modelType, err, sm.modelPath === ctx.replacement?.modelPath ? ctx.replacement.source : undefined);
|
|
391143
|
+
const yielder = createEventLoopYielder();
|
|
391041
391144
|
for (const sm of outcome.models) {
|
|
391145
|
+
await yielder.maybeYield();
|
|
391042
391146
|
if (sm.compilationError) {
|
|
391043
391147
|
const err = Model.deserializeCompilationError(sm.compilationError);
|
|
391044
391148
|
if (onCompileError === "throw") {
|
|
@@ -391060,6 +391164,7 @@ class Package {
|
|
|
391060
391164
|
continue;
|
|
391061
391165
|
}
|
|
391062
391166
|
let model;
|
|
391167
|
+
const hydrateStart = performance.now();
|
|
391063
391168
|
try {
|
|
391064
391169
|
model = Model.fromSerialized(packageName, packagePath, malloyConfig, sm, {
|
|
391065
391170
|
buildManifest,
|
|
@@ -391080,8 +391185,11 @@ class Package {
|
|
|
391080
391185
|
});
|
|
391081
391186
|
models.set(sm.modelPath, placeholder(sm, hydrateErr instanceof Error ? hydrateErr : new Error(String(hydrateErr))));
|
|
391082
391187
|
continue;
|
|
391188
|
+
} finally {
|
|
391189
|
+
timings.hydrateMs += performance.now() - hydrateStart;
|
|
391083
391190
|
}
|
|
391084
391191
|
let warnings;
|
|
391192
|
+
const validationStart = performance.now();
|
|
391085
391193
|
try {
|
|
391086
391194
|
warnings = await model.validateRenderTags();
|
|
391087
391195
|
} catch (renderErr) {
|
|
@@ -391102,6 +391210,8 @@ class Package {
|
|
|
391102
391210
|
}
|
|
391103
391211
|
models.set(sm.modelPath, placeholder(sm, err));
|
|
391104
391212
|
continue;
|
|
391213
|
+
} finally {
|
|
391214
|
+
timings.renderValidationMs += performance.now() - validationStart;
|
|
391105
391215
|
}
|
|
391106
391216
|
for (const w2 of warnings) {
|
|
391107
391217
|
renderTagWarnings.push({
|
|
@@ -391111,10 +391221,12 @@ class Package {
|
|
|
391111
391221
|
severity: w2.severity
|
|
391112
391222
|
});
|
|
391113
391223
|
}
|
|
391224
|
+
const afterHydrateStart = performance.now();
|
|
391114
391225
|
await afterHydrate?.(sm);
|
|
391226
|
+
timings.hydrateMs += performance.now() - afterHydrateStart;
|
|
391115
391227
|
models.set(sm.modelPath, model);
|
|
391116
391228
|
}
|
|
391117
|
-
return { models, renderTagWarnings };
|
|
391229
|
+
return { models, renderTagWarnings, timings };
|
|
391118
391230
|
}
|
|
391119
391231
|
static async lintWorkerOutcome(environmentName, packageName, packagePath, malloyConfig, outcome, buildManifest, replacement) {
|
|
391120
391232
|
const { models, renderTagWarnings } = await Package.hydrateWorkerModels(outcome, {
|
|
@@ -391184,8 +391296,13 @@ class Package {
|
|
|
391184
391296
|
const packageConfig = Package.packageConfigFromOutcome(environmentName, packageName, outcome);
|
|
391185
391297
|
let models;
|
|
391186
391298
|
let renderTagWarnings;
|
|
391299
|
+
let hydrationTimings;
|
|
391187
391300
|
try {
|
|
391188
|
-
({
|
|
391301
|
+
({
|
|
391302
|
+
models,
|
|
391303
|
+
renderTagWarnings,
|
|
391304
|
+
timings: hydrationTimings
|
|
391305
|
+
} = await Package.hydrateWorkerModels(outcome, { packageName, packagePath, malloyConfig }, "throw", async (sm) => {
|
|
391189
391306
|
if (sm.modelPath.endsWith(MODEL_FILE_SUFFIX)) {
|
|
391190
391307
|
const modelSource = await fs14.readFile(path16.join(packagePath, sm.modelPath), "utf-8");
|
|
391191
391308
|
assertPersistNamesQuoted(modelSource, sm.modelPath);
|
|
@@ -391208,15 +391325,16 @@ class Package {
|
|
|
391208
391325
|
});
|
|
391209
391326
|
const pkg = new Package(environmentName, packageName, packagePath, packageConfig, databases, models, malloyConfig);
|
|
391210
391327
|
pkg.renderTagWarnings = renderTagWarnings;
|
|
391211
|
-
await pkg.discoverReports("load");
|
|
391328
|
+
const reportTimings = await pkg.discoverReports("load");
|
|
391212
391329
|
pkg.manifestWarnings = outcome.packageMetadata.manifestWarnings ?? [];
|
|
391213
391330
|
pkg.recordDeprecatedUsage(outcome);
|
|
391214
391331
|
pkg.retrievalSettings = outcome.packageMetadata.retrieval ?? DEFAULT_PACKAGE_RETRIEVAL;
|
|
391215
391332
|
pkg.rawAgents = outcome.packageMetadata.agents;
|
|
391216
391333
|
pkg.wireFreshnessResolvers();
|
|
391217
391334
|
pkg.refreshServingIdentity();
|
|
391335
|
+
let buildPlanMs;
|
|
391336
|
+
const buildPlanPhaseStart = performance.now();
|
|
391218
391337
|
try {
|
|
391219
|
-
const buildPlanStart = Date.now();
|
|
391220
391338
|
const workerPlan = outcome.buildPlan;
|
|
391221
391339
|
for (const [reason, count4] of Object.entries(workerPlan?.eligibilityRefused ?? {})) {
|
|
391222
391340
|
for (let i2 = 0;i2 < (count4 ?? 0); i2++) {
|
|
@@ -391249,7 +391367,8 @@ class Package {
|
|
|
391249
391367
|
sourceEntityId: source.sourceEntityId,
|
|
391250
391368
|
declaration: incrementalDeclarations[sourceID]
|
|
391251
391369
|
}));
|
|
391252
|
-
recordBuildPlanComputeDuration(workerPlan ? workerPlan.durationMs :
|
|
391370
|
+
recordBuildPlanComputeDuration(workerPlan ? workerPlan.durationMs : performance.now() - buildPlanPhaseStart, workerPlan ? "worker" : "main_thread");
|
|
391371
|
+
buildPlanMs = performance.now() - buildPlanPhaseStart;
|
|
391253
391372
|
} catch (err) {
|
|
391254
391373
|
recordBuildPlanComputeFailed();
|
|
391255
391374
|
logger.warn(`Failed to compute build plan for package ${packageName}`, {
|
|
@@ -391305,6 +391424,15 @@ class Package {
|
|
|
391305
391424
|
}
|
|
391306
391425
|
await pkg.pushPreaggregateServeModels();
|
|
391307
391426
|
pkg.logEmptyDiscoveryWarnings();
|
|
391427
|
+
recordPackageLoadPhaseDurations({
|
|
391428
|
+
worker: workerDoneTime - dispatchTime,
|
|
391429
|
+
hydrate: hydrationTimings.hydrateMs,
|
|
391430
|
+
render_validation: hydrationTimings.renderValidationMs,
|
|
391431
|
+
discovery: reportTimings.discoveryMs,
|
|
391432
|
+
lint: reportTimings.lintMs,
|
|
391433
|
+
build_plan: buildPlanMs,
|
|
391434
|
+
total: performance.now() - startTime
|
|
391435
|
+
});
|
|
391308
391436
|
return pkg;
|
|
391309
391437
|
}
|
|
391310
391438
|
getPackageName() {
|
|
@@ -391958,8 +392086,11 @@ class Package {
|
|
|
391958
392086
|
const started = performance.now();
|
|
391959
392087
|
const discovery = await this.discoverReportsIn(this.models, during);
|
|
391960
392088
|
this.installReports(discovery);
|
|
392089
|
+
const linting = performance.now();
|
|
391961
392090
|
await this.relintReports({ textLint: during !== "compile" });
|
|
391962
|
-
|
|
392091
|
+
const finished = performance.now();
|
|
392092
|
+
this.reportDiscovered(during, discovery, finished - started);
|
|
392093
|
+
return { discoveryMs: linting - started, lintMs: finished - linting };
|
|
391963
392094
|
}
|
|
391964
392095
|
reportDiscovered(during, discovery, durationMs) {
|
|
391965
392096
|
recordReportDiscoveryDuration(during, durationMs);
|
|
@@ -392060,19 +392191,17 @@ class Package {
|
|
|
392060
392191
|
model: modelPath,
|
|
392061
392192
|
error: errMessage(err)
|
|
392062
392193
|
});
|
|
392063
|
-
|
|
392064
|
-
|
|
392065
|
-
continue;
|
|
392066
|
-
}
|
|
392194
|
+
files.set(modelPath, { status: "dropped", name });
|
|
392195
|
+
continue;
|
|
392067
392196
|
}
|
|
392068
|
-
if (
|
|
392197
|
+
if (facts && !manifest) {
|
|
392069
392198
|
files.set(modelPath, { status: "tileless", name, facts });
|
|
392070
392199
|
continue;
|
|
392071
392200
|
}
|
|
392072
|
-
if (
|
|
392201
|
+
if (!facts) {
|
|
392073
392202
|
manifest = {
|
|
392074
392203
|
name,
|
|
392075
|
-
layout:
|
|
392204
|
+
layout: tag.layout,
|
|
392076
392205
|
title: name,
|
|
392077
392206
|
autorun: true,
|
|
392078
392207
|
entryFile: modelPath,
|
|
@@ -392368,7 +392497,7 @@ class Package {
|
|
|
392368
392497
|
layout: report.layout,
|
|
392369
392498
|
title: manifest?.title ?? listing.title ?? report.name,
|
|
392370
392499
|
description: manifest?.description ?? listing.description,
|
|
392371
|
-
editable: report.path.endsWith(MODEL_FILE_SUFFIX)
|
|
392500
|
+
editable: report.path.endsWith(MODEL_FILE_SUFFIX),
|
|
392372
392501
|
...error2 !== undefined && { error: error2 }
|
|
392373
392502
|
};
|
|
392374
392503
|
}
|
|
@@ -394137,7 +394266,7 @@ ${query}`);
|
|
|
394137
394266
|
}
|
|
394138
394267
|
}
|
|
394139
394268
|
// package.json
|
|
394140
|
-
var version2 = "0.9.
|
|
394269
|
+
var version2 = "0.9.11";
|
|
394141
394270
|
|
|
394142
394271
|
// src/version.ts
|
|
394143
394272
|
var SERVER_VERSION = version2;
|