@malloy-publisher/server 0.7.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. package/dist/app/api-doc.yaml +187 -85
  2. package/dist/app/assets/EnvironmentPage-dgZIJhMw.js +1 -0
  3. package/dist/app/assets/HomePage-D2FGM4cV.js +1 -0
  4. package/dist/app/assets/{LightMode-BFbC4W45.js → LightMode-emBuHCdz.js} +1 -1
  5. package/dist/app/assets/{MainPage-BcCyzakx.js → MainPage-BVqdH0TB.js} +2 -2
  6. package/dist/app/assets/{ModelPage-wHHnxoih.js → ModelPage-DAJxkTmk.js} +2 -2
  7. package/dist/app/assets/PackagePage-BGGcyDeJ.js +1 -0
  8. package/dist/app/assets/{RenderedResult-CYyhQekT.es-DBHF8EwO.js → RenderedResult-CDA9mN_6.es-C4f_ZzvB.js} +3 -3
  9. package/dist/app/assets/{RouteError-Blt6a2Za.js → RouteError-CtRFTa_q.js} +1 -1
  10. package/dist/app/assets/{ThemeEditorPage-Cb-2YbfG.js → ThemeEditorPage-Bnd5dgI0.js} +1 -1
  11. package/dist/app/assets/{ToggleButtonGroup-BuhURn8v.js → ToggleButtonGroup-CxU1OIGN.js} +1 -1
  12. package/dist/app/assets/{index-DCizS2BO.js → index-BWGMOIxQ.js} +1 -1
  13. package/dist/app/assets/{index-Dz4y_2PE.js → index-CNQ_NxFx.js} +105 -105
  14. package/dist/app/assets/{index-kt4oTEGz.js → index-OsUlLBi_.js} +1 -1
  15. package/dist/app/assets/{index-BELjGKhI.js → index-aXk5ykLp.js} +1 -1
  16. package/dist/app/assets/{index-BJyLY8XQ.js → index-xgxo45Xn.js} +1 -1
  17. package/dist/app/assets/{index.es-u5PRRTzT.js → index.es-BCvB2VgL.js} +22 -22
  18. package/dist/app/index.html +1 -1
  19. package/dist/package_load_worker.mjs +1328 -776
  20. package/dist/server.mjs +3375 -694
  21. package/package.json +1 -1
  22. package/dist/app/assets/EnvironmentPage-ufeWfqnK.js +0 -1
  23. package/dist/app/assets/HomePage-CN_FgmtD.js +0 -1
  24. package/dist/app/assets/PackagePage-DwdO-2dp.js +0 -1
@@ -12905,6 +12905,8 @@ function internalErrorToHttpError(error) {
12905
12905
  return httpError(422, error.message);
12906
12906
  } else if (error instanceof ModelCompilationError) {
12907
12907
  return httpError(424, error.message);
12908
+ } else if (error instanceof PackageManifestError) {
12909
+ return httpError(424, error.message);
12908
12910
  } else if (error instanceof ConnectionError) {
12909
12911
  if (error.callerSafe) {
12910
12912
  return httpError(502, error.message);
@@ -12945,7 +12947,7 @@ function httpError(code, message, reason) {
12945
12947
  }
12946
12948
  };
12947
12949
  }
12948
- var GENERIC_INTERNAL_MESSAGE = "Internal server error.", GENERIC_UPSTREAM_MESSAGE = "Upstream connection error.", MAX_LOGGED_DETAIL_CHARS = 2000, NotImplementedError, BadRequestError, InvalidArgumentError, CompileRefusedError, EnvironmentNotFoundError, PackageNotFoundError, ModelNotFoundError, DashboardNotFoundError, ConnectionNotFoundError, TableNotFoundError, ConnectionError, DestinationNotFoundError, ConnectionAuthError, UnsupportedCatalogFormatError, ModelCompilationError, MaterializationEligibilityError, PublisherConfigError, FrozenConfigError, AccessDeniedError, NotQueryableError, MaterializationNotFoundError, MaterializationConflictError, WriteConflictError, WriteRolledBackError, InvalidStateTransitionError, ServiceUnavailableError, PayloadTooLargeError, ResponseUnserializableError, QueryTimeoutError;
12950
+ var GENERIC_INTERNAL_MESSAGE = "Internal server error.", GENERIC_UPSTREAM_MESSAGE = "Upstream connection error.", MAX_LOGGED_DETAIL_CHARS = 2000, NotImplementedError, BadRequestError, InvalidArgumentError, CompileRefusedError, EnvironmentNotFoundError, PackageNotFoundError, ModelNotFoundError, DashboardNotFoundError, ConnectionNotFoundError, TableNotFoundError, ConnectionError, DestinationNotFoundError, ConnectionAuthError, UnsupportedCatalogFormatError, ModelCompilationError, PackageManifestError, MaterializationEligibilityError, PublisherConfigError, FrozenConfigError, AccessDeniedError, NotQueryableError, OffSurfaceError, MaterializationNotFoundError, MaterializationConflictError, WriteConflictError, WriteRolledBackError, InvalidStateTransitionError, ServiceUnavailableError, PayloadTooLargeError, ResponseUnserializableError, QueryTimeoutError;
12949
12951
  var init_errors = __esm(() => {
12950
12952
  init_constants();
12951
12953
  init_logger();
@@ -13020,6 +13022,12 @@ var init_errors = __esm(() => {
13020
13022
  super(error.message);
13021
13023
  }
13022
13024
  };
13025
+ PackageManifestError = class PackageManifestError extends Error {
13026
+ constructor(message) {
13027
+ super(message);
13028
+ this.name = "PackageManifestError";
13029
+ }
13030
+ };
13023
13031
  MaterializationEligibilityError = class MaterializationEligibilityError extends Error {
13024
13032
  reason;
13025
13033
  constructor(error) {
@@ -13052,6 +13060,12 @@ var init_errors = __esm(() => {
13052
13060
  this.name = "NotQueryableError";
13053
13061
  }
13054
13062
  };
13063
+ OffSurfaceError = class OffSurfaceError extends NotQueryableError {
13064
+ constructor(message) {
13065
+ super(message);
13066
+ this.name = "OffSurfaceError";
13067
+ }
13068
+ };
13055
13069
  MaterializationNotFoundError = class MaterializationNotFoundError extends Error {
13056
13070
  constructor(message) {
13057
13071
  super(message);
@@ -13419,11 +13433,11 @@ function recordAuthorizeBypass(entryPoint) {
13419
13433
  });
13420
13434
  bypassCounter.add(1, { entry_point: entryPoint });
13421
13435
  }
13422
- function recordRowLevelGateDecision(decision) {
13436
+ function recordRowLevelGateDecision(decision, site = "entry_point") {
13423
13437
  rowLevelDecisionCounter ??= publisherMeter().createCounter("publisher_authorize_row_level_total", {
13424
- description: "How a row-level `#(access_filter)` gate resolved a request. Label: decision ('denied_by_gate'|'empty_after_filter'). 'denied_by_gate' is the fail-closed refusal when the gate could not be applied; 'empty_after_filter' is a successful response with zero rows after the filter matched none, which is NOT an error."
13438
+ description: "How a row-level `#(access_filter)` gate resolved a request. Labels: decision ('denied_by_gate'|'empty_after_filter'), site ('entry_point'|'caller_join'). 'denied_by_gate' is the fail-closed refusal when the gate could not be applied; 'empty_after_filter' is a successful response with zero rows after the filter matched none, which is NOT an error. site says whether the gate was the query's own run target or a source the caller's text joined."
13425
13439
  });
13426
- rowLevelDecisionCounter.add(1, { decision });
13440
+ rowLevelDecisionCounter.add(1, { decision, site });
13427
13441
  }
13428
13442
  function recordRowLevelGateRejected(cause) {
13429
13443
  rowLevelRejectionCounter ??= publisherMeter().createCounter("publisher_authorize_row_level_rejected_total", {
@@ -13437,11 +13451,11 @@ function recordAuthorizeAdmitAllGate(route) {
13437
13451
  });
13438
13452
  admitAllCounter.add(1, { route });
13439
13453
  }
13440
- function recordLockDecision(decision) {
13454
+ function recordLockDecision(decision, site = "entry_point") {
13441
13455
  lockDecisionCounter ??= publisherMeter().createCounter("publisher_authorize_lock_total", {
13442
- description: "How a `#(authorize)` lock resolved a request. Label: decision ('admitted'|'denied_by_lock'|'denied_unresolvable'). Both denials are a 403; 'denied_by_lock' is the gate refusing a caller it does not admit (routine), 'denied_unresolvable' is the fail-closed refusal when the gate could not be decided at all."
13456
+ description: "How a `#(authorize)` lock resolved a request. Labels: decision ('admitted'|'denied_by_lock'|'denied_unresolvable'), site ('entry_point'|'caller_join'). Both denials are a 403; 'denied_by_lock' is the gate refusing a caller it does not admit (routine), 'denied_unresolvable' is the fail-closed refusal when the gate could not be decided at all. site says whether the lock was on the query's own run target or on a source the caller's text joined."
13443
13457
  });
13444
- lockDecisionCounter.add(1, { decision });
13458
+ lockDecisionCounter.add(1, { decision, site });
13445
13459
  }
13446
13460
  var guardRejectionCounter = null, bypassCounter = null, rowLevelDecisionCounter = null, rowLevelRejectionCounter = null, admitAllCounter = null, lockDecisionCounter = null;
13447
13461
  var init_authorize_metrics = __esm(() => {
@@ -14465,858 +14479,1370 @@ var init_gate_classification = __esm(() => {
14465
14479
  init_gate_registry_walk();
14466
14480
  });
14467
14481
 
14468
- // src/service/filter.ts
14469
- function parseFilterAnnotation(annotation) {
14470
- const trimmed = annotation.trim();
14471
- if (!trimmed.startsWith(ANNOTATION_PREFIX)) {
14472
- return null;
14473
- }
14474
- const body = trimmed.slice(ANNOTATION_PREFIX.length).trim();
14475
- const tokens = tokenize(body);
14476
- let name;
14477
- let dimension;
14478
- let type;
14479
- let implicit = false;
14480
- let required = false;
14481
- for (const token of tokens) {
14482
- if (token.includes("=")) {
14483
- const eqIndex = token.indexOf("=");
14484
- const key = token.slice(0, eqIndex).toLowerCase();
14485
- const value = token.slice(eqIndex + 1);
14486
- switch (key) {
14487
- case "name":
14488
- name = value;
14489
- break;
14490
- case "dimension":
14491
- dimension = value;
14492
- break;
14493
- case "type":
14494
- if (!VALID_FILTER_TYPES.has(value)) {
14495
- throw new Error(`Invalid filter type "${value}". Must be one of: ${[...VALID_FILTER_TYPES].join(", ")}`);
14496
- }
14497
- type = value;
14498
- break;
14499
- default:
14500
- throw new Error(`Unknown filter parameter "${key}"`);
14501
- }
14482
+ // src/service/motly.ts
14483
+ import { parseAnnotation } from "@malloydata/malloy-tag";
14484
+ function onMotlyRoute(text) {
14485
+ const afterSigil = text.replace(/^##?\|?/, "");
14486
+ return afterSigil === "" || /^[ \t\r\n]/.test(afterSigil);
14487
+ }
14488
+ function hasEnvReference(annotation) {
14489
+ return annotation.includes("@env.");
14490
+ }
14491
+ function pollutionTargets() {
14492
+ const targets = [Object.prototype];
14493
+ for (const key of Object.getOwnPropertyNames(Object.prototype)) {
14494
+ const descriptor = Object.getOwnPropertyDescriptor(Object.prototype, key);
14495
+ if (!descriptor)
14496
+ continue;
14497
+ let value;
14498
+ if ("value" in descriptor) {
14499
+ value = descriptor.value;
14502
14500
  } else {
14503
- const flag = token.toLowerCase();
14504
- if (flag === "implicit") {
14505
- implicit = true;
14506
- } else if (flag === "required") {
14507
- required = true;
14508
- } else {
14509
- throw new Error(`Unknown filter flag "${token}"`);
14501
+ if (BUILT_IN_PROTO_GETTER === undefined || descriptor.get !== BUILT_IN_PROTO_GETTER) {
14502
+ return;
14510
14503
  }
14504
+ continue;
14505
+ }
14506
+ if ((typeof value === "object" || typeof value === "function") && value !== null && !targets.includes(value)) {
14507
+ targets.push(value);
14511
14508
  }
14512
14509
  }
14513
- if (!dimension) {
14514
- throw new Error("filter annotation missing required 'dimension' parameter");
14515
- }
14516
- if (!type) {
14517
- throw new Error("filter annotation missing required 'type' parameter");
14518
- }
14519
- return {
14520
- name: name ?? dimension,
14521
- dimension,
14522
- type,
14523
- implicit,
14524
- required
14525
- };
14510
+ return targets;
14526
14511
  }
14527
- function parseFilters(annotations) {
14528
- const byName = new Map;
14529
- for (const annotation of annotations) {
14530
- const parsed = parseFilterAnnotation(annotation);
14531
- if (parsed) {
14532
- byName.set(parsed.name, parsed);
14512
+ function parseGuarded(texts) {
14513
+ const targets = pollutionTargets();
14514
+ if (targets === undefined) {
14515
+ return { tag: undefined, messages: [UNSAFE_TO_PARSE] };
14516
+ }
14517
+ let unreadable = false;
14518
+ const ownNames = (target) => {
14519
+ try {
14520
+ return Object.getOwnPropertyNames(target);
14521
+ } catch {
14522
+ unreadable = true;
14523
+ return [];
14533
14524
  }
14525
+ };
14526
+ const before = targets.map(ownNames);
14527
+ if (unreadable)
14528
+ return { tag: undefined, messages: [UNSAFE_TO_PARSE] };
14529
+ const undoPollution = () => {
14530
+ let polluted = false;
14531
+ targets.forEach((target, index) => {
14532
+ for (const key of ownNames(target)) {
14533
+ if (before[index].includes(key))
14534
+ continue;
14535
+ polluted = true;
14536
+ try {
14537
+ delete target[key];
14538
+ } catch {}
14539
+ }
14540
+ });
14541
+ return polluted || unreadable;
14542
+ };
14543
+ if (texts.some((text) => text.length > MAX_ANNOTATION_CHARS)) {
14544
+ return { tag: undefined, messages: [ANNOTATION_TOO_LONG] };
14545
+ }
14546
+ try {
14547
+ const result = parseAnnotation([...texts]);
14548
+ if (undoPollution())
14549
+ return { tag: undefined, messages: [UNSAFE_TO_PARSE] };
14550
+ return {
14551
+ tag: result.tag,
14552
+ messages: result.log.map((error) => error.message)
14553
+ };
14554
+ } catch {
14555
+ undoPollution();
14556
+ return { tag: undefined, messages: [UNSAFE_TO_PARSE] };
14534
14557
  }
14535
- return [...byName.values()];
14536
- }
14537
- function escapeMalloyString(value) {
14538
- return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
14539
14558
  }
14540
- function isBooleanLiteral(v) {
14541
- const lower = v.toLowerCase();
14542
- return lower === "true" || lower === "false";
14559
+ function docCommentText(texts) {
14560
+ const lines = texts.filter((text) => /^##?\|?"([ \t\r\n]|$)/.test(text)).map((text) => text.replace(/^##?\|?"[ \t\r\n]?/, "").trimEnd());
14561
+ return lines.some((text) => text.trim().length > 0) ? lines.join(`
14562
+ `) : undefined;
14543
14563
  }
14544
- function isDateLiteral(v) {
14545
- return ISO_DATE_RE.test(v) || ISO_TIMESTAMP_RE.test(v);
14564
+ function splitDocComment(texts) {
14565
+ const text = docCommentText(texts);
14566
+ if (text === undefined)
14567
+ return {};
14568
+ const lines = text.split(`
14569
+ `);
14570
+ const titleLine = lines.findIndex((line) => line.trim().length > 0);
14571
+ if (titleLine === -1)
14572
+ return {};
14573
+ const body = lines.slice(titleLine + 1).join(`
14574
+ `).replace(/^[ \t\r\n]+/, "").trimEnd();
14575
+ return {
14576
+ title: lines[titleLine].trim(),
14577
+ body: body.length > 0 ? body : undefined
14578
+ };
14546
14579
  }
14547
- function malloyLiteral(v) {
14548
- if (isBooleanLiteral(v)) {
14549
- return v.toLowerCase();
14550
- }
14551
- if (isDateLiteral(v)) {
14552
- return `@${v.slice(0, 10)}`;
14580
+ function docCommentTitleAndDescription(texts, explicitTitle) {
14581
+ if (explicitTitle !== undefined) {
14582
+ return { title: explicitTitle, description: docCommentText(texts) };
14553
14583
  }
14554
- return `'${escapeMalloyString(v)}'`;
14584
+ const { title, body } = splitDocComment(texts);
14585
+ return { title, description: body };
14555
14586
  }
14556
- function buildPredicate(filter, value) {
14557
- const dim = `\`${filter.dimension}\``;
14558
- switch (filter.type) {
14559
- case "equal": {
14560
- const v = Array.isArray(value) ? value[0] : value;
14561
- return `${dim} = ${malloyLiteral(v)}`;
14587
+ function quoteFilterLiterals(annotation) {
14588
+ const VALUE_START = /[=[,]/;
14589
+ const BARE_FILTER_LITERAL = /^([ \t]*)f(['"])((?:\\.|(?!\2)[^\\])*)\2/;
14590
+ let out = "";
14591
+ let i = 0;
14592
+ while (i < annotation.length) {
14593
+ const char = annotation[i];
14594
+ const delimited = endOfDelimited(annotation, i);
14595
+ if (delimited > i) {
14596
+ out += annotation.slice(i, delimited);
14597
+ i = delimited;
14598
+ continue;
14562
14599
  }
14563
- case "in": {
14564
- const values = Array.isArray(value) ? value : [value];
14565
- if (values.length === 1) {
14566
- return `${dim} = ${malloyLiteral(values[0])}`;
14600
+ if (VALUE_START.test(char)) {
14601
+ const match = BARE_FILTER_LITERAL.exec(annotation.slice(i + 1));
14602
+ if (match) {
14603
+ const body = match[3].replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
14604
+ out += `${char}${match[1]}"f'${body}'"`;
14605
+ i += 1 + match[0].length;
14606
+ continue;
14567
14607
  }
14568
- const conditions = values.map((v) => `${dim} = ${malloyLiteral(v)}`);
14569
- return `(${conditions.join(" or ")})`;
14570
- }
14571
- case "like": {
14572
- const v = Array.isArray(value) ? value[0] : value;
14573
- const escaped = escapeMalloyString(v.toLowerCase());
14574
- const pattern = escaped.startsWith("%") || escaped.endsWith("%") ? escaped : `%${escaped}%`;
14575
- return `lower(${dim}) ~ '${pattern}'`;
14576
- }
14577
- case "greater_than": {
14578
- const v = Array.isArray(value) ? value[0] : value;
14579
- return `${dim} > ${malloyLiteral(v)}`;
14580
- }
14581
- case "less_than": {
14582
- const v = Array.isArray(value) ? value[0] : value;
14583
- return `${dim} < ${malloyLiteral(v)}`;
14584
14608
  }
14609
+ out += char;
14610
+ i += 1;
14585
14611
  }
14612
+ return out;
14586
14613
  }
14587
- function buildFilterClause(filters, params) {
14588
- const predicates = [];
14589
- for (const filter of filters) {
14590
- const value = params[filter.name];
14591
- const hasValue = value !== undefined && value !== null && (Array.isArray(value) ? value.length > 0 : value !== "");
14592
- if (!hasValue) {
14593
- if (filter.required) {
14594
- throw new FilterValidationError(`Required filter "${filter.name}" (dimension: ${filter.dimension}) was not provided`);
14614
+ function endOfDelimited(text, start) {
14615
+ const char = text[start];
14616
+ if (char === "`") {
14617
+ let i = start + 1;
14618
+ while (i < text.length) {
14619
+ if (text[i] === "\\") {
14620
+ i += 2;
14621
+ continue;
14595
14622
  }
14596
- continue;
14623
+ if (text[i] === "`")
14624
+ return i + 1;
14625
+ if (text[i] === `
14626
+ `)
14627
+ return text.length;
14628
+ i += 1;
14597
14629
  }
14598
- predicates.push(buildPredicate(filter, value));
14599
- }
14600
- if (predicates.length === 0) {
14601
- return "";
14602
- }
14603
- return predicates.join(" and ");
14604
- }
14605
- function injectFilterRefinement(query, filterClause) {
14606
- if (!filterClause) {
14607
- return query;
14630
+ return text.length;
14608
14631
  }
14609
- return `${query.trimEnd()}
14610
- + {where: ${filterClause}}`;
14611
- }
14612
- function tokenize(input) {
14613
- const tokens = [];
14614
- let current = "";
14615
- let inQuote = false;
14616
- let quoteChar = "";
14617
- for (const ch of input) {
14618
- if (inQuote) {
14619
- if (ch === quoteChar) {
14620
- inQuote = false;
14621
- } else {
14622
- current += ch;
14632
+ if (text.startsWith("<<<", start)) {
14633
+ let lineStart = start + 3;
14634
+ for (;; ) {
14635
+ const newline = text.indexOf(`
14636
+ `, lineStart);
14637
+ const lineEnd = newline === -1 ? text.length : newline;
14638
+ if (text.slice(lineStart, lineEnd).trim() === ">>>")
14639
+ return lineEnd;
14640
+ if (newline === -1)
14641
+ return text.length;
14642
+ lineStart = newline + 1;
14643
+ }
14644
+ }
14645
+ if (char === '"' || char === "'") {
14646
+ const triple = char.repeat(3);
14647
+ if (text.startsWith(triple, start)) {
14648
+ let i2 = start + triple.length;
14649
+ while (i2 < text.length) {
14650
+ if (text[i2] === "\\") {
14651
+ i2 += 2;
14652
+ continue;
14653
+ }
14654
+ if (text.startsWith(triple, i2))
14655
+ return i2 + triple.length;
14656
+ i2 += 1;
14623
14657
  }
14624
- } else if (ch === '"' || ch === "'") {
14625
- inQuote = true;
14626
- quoteChar = ch;
14627
- } else if (ch === " " || ch === "\t") {
14628
- if (current) {
14629
- tokens.push(current);
14630
- current = "";
14658
+ return text.length;
14659
+ }
14660
+ let i = start + 1;
14661
+ while (i < text.length) {
14662
+ if (text[i] === "\\") {
14663
+ i += 2;
14664
+ continue;
14631
14665
  }
14632
- } else {
14633
- current += ch;
14666
+ if (text[i] === char)
14667
+ return i + 1;
14668
+ if (text[i] === `
14669
+ `)
14670
+ return i;
14671
+ i += 1;
14634
14672
  }
14673
+ return text.length;
14635
14674
  }
14636
- if (current) {
14637
- tokens.push(current);
14675
+ return start;
14676
+ }
14677
+ function unwrapFilterLiteral(value) {
14678
+ const match = /^f(['"])([\s\S]*)\1$/.exec(value);
14679
+ return match ? match[2] : value;
14680
+ }
14681
+ function parseMotly(texts) {
14682
+ const onRoute = texts.filter(onMotlyRoute);
14683
+ const motly = onRoute.filter((text) => !hasEnvReference(text));
14684
+ const envDropped = onRoute.length - motly.length;
14685
+ const envErrors = envDropped > 0 ? [ENV_REFERENCE_DROPPED] : [];
14686
+ if (motly.length === 0)
14687
+ return { tag: undefined, errors: envErrors };
14688
+ const direct = parseGuarded(motly);
14689
+ if (direct.messages.length === 0)
14690
+ return { tag: direct.tag, errors: envErrors };
14691
+ const rescued = motly.map((text) => {
14692
+ if (parseGuarded([text]).messages.length === 0)
14693
+ return text;
14694
+ const rewritten = quoteFilterLiterals(text);
14695
+ return parseGuarded([rewritten]).messages.length === 0 ? rewritten : text;
14696
+ });
14697
+ const after = parseGuarded(rescued);
14698
+ return { tag: after.tag, errors: [...envErrors, ...after.messages] };
14699
+ }
14700
+ function tagText(tag, ...path) {
14701
+ try {
14702
+ return tag?.text(...path);
14703
+ } catch {
14704
+ return;
14638
14705
  }
14639
- return tokens;
14640
14706
  }
14641
- var VALID_FILTER_TYPES, ANNOTATION_PREFIX = "#(filter)", ISO_DATE_RE, ISO_TIMESTAMP_RE, FilterValidationError;
14642
- var init_filter = __esm(() => {
14643
- VALID_FILTER_TYPES = new Set([
14644
- "equal",
14645
- "in",
14646
- "like",
14647
- "greater_than",
14648
- "less_than"
14649
- ]);
14650
- ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
14651
- ISO_TIMESTAMP_RE = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}/;
14652
- FilterValidationError = class FilterValidationError extends Error {
14653
- constructor(message) {
14654
- super(message);
14655
- this.name = "FilterValidationError";
14656
- }
14657
- };
14707
+ function tagNumeric(tag, ...path) {
14708
+ try {
14709
+ const raw = tagText(tag, ...path);
14710
+ if (raw === undefined)
14711
+ return;
14712
+ const DECIMAL = /^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?$/;
14713
+ const text = raw.trim();
14714
+ if (!DECIMAL.test(text))
14715
+ return;
14716
+ const value = Number(text);
14717
+ return Number.isFinite(value) ? value : undefined;
14718
+ } catch {
14719
+ return;
14720
+ }
14721
+ }
14722
+ function motlyTag(texts) {
14723
+ return parseMotly(texts).tag;
14724
+ }
14725
+ function motlyParseErrors(texts) {
14726
+ return parseMotly(texts).errors;
14727
+ }
14728
+ function readAutorun(tag) {
14729
+ return tagText(tag, "autorun") !== "false";
14730
+ }
14731
+ function scalarTypeOf(tag) {
14732
+ try {
14733
+ return tag?.scalarType();
14734
+ } catch {
14735
+ return;
14736
+ }
14737
+ }
14738
+ function authoredDay(isoText) {
14739
+ const value = new Date(isoText);
14740
+ if (Number.isNaN(value.getTime()))
14741
+ return isoText.slice(0, 10);
14742
+ const atUtcMidnight = value.getUTCHours() === 0 && value.getUTCMinutes() === 0 && value.getUTCSeconds() === 0 && value.getUTCMilliseconds() === 0;
14743
+ const year = atUtcMidnight ? value.getUTCFullYear() : value.getFullYear();
14744
+ const month = atUtcMidnight ? value.getUTCMonth() : value.getMonth();
14745
+ const day = atUtcMidnight ? value.getUTCDate() : value.getDate();
14746
+ return `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
14747
+ }
14748
+ function readStartingGivens(tag, declaredType) {
14749
+ const entries = tag?.tag("givens");
14750
+ if (!entries)
14751
+ return;
14752
+ const collected = {};
14753
+ for (const [name, value] of entries.entries()) {
14754
+ const tag2 = value;
14755
+ const text = tagText(tag2);
14756
+ if (text === undefined)
14757
+ continue;
14758
+ const declared = declaredType(name);
14759
+ collected[name] = scalarTypeOf(tag2) === "date" && declared !== "timestamp" && declared !== "timestamptz" ? authoredDay(text) : declared?.startsWith("filter<") ? unwrapFilterLiteral(text) : text;
14760
+ }
14761
+ return Object.keys(collected).length > 0 ? collected : undefined;
14762
+ }
14763
+ var BUILT_IN_PROTO_GETTER, MAX_ANNOTATION_CHARS = 8192, ANNOTATION_TOO_LONG, ENV_REFERENCE_DROPPED = "annotation dropped for carrying an @env. reference", UNSAFE_TO_PARSE = "annotation could not be parsed safely";
14764
+ var init_motly = __esm(() => {
14765
+ BUILT_IN_PROTO_GETTER = Object.getOwnPropertyDescriptor(Object.prototype, "__proto__")?.get;
14766
+ ANNOTATION_TOO_LONG = `annotation exceeds ${MAX_ANNOTATION_CHARS} characters and was not parsed`;
14658
14767
  });
14659
14768
 
14660
- // src/service/source_extraction.ts
14769
+ // src/service/given.ts
14661
14770
  import {
14662
- isJoined as isJoined3,
14663
- isSourceDef as isSourceDef4,
14664
- modelDefToModelInfo
14771
+ isSourceDef as isSourceDef4
14665
14772
  } from "@malloydata/malloy";
14666
- function joinFieldNamesUnresolvableDeclaration(field, modelDef) {
14667
- const ids = [field.referenceID, field.sourceID].filter((id) => !!id);
14668
- if (ids.length === 0)
14773
+ function presentText(tag, ...path) {
14774
+ const raw = tagText(tag, ...path);
14775
+ return raw !== undefined && raw.trim() !== "" ? raw : undefined;
14776
+ }
14777
+ function readGivenControlSpec(annotationTexts2) {
14778
+ const spec = {};
14779
+ const tag = motlyTag(annotationTexts2);
14780
+ if (!tag)
14781
+ return spec;
14782
+ const label = presentText(tag, "label");
14783
+ if (label !== undefined)
14784
+ spec.label = label;
14785
+ const description = presentText(tag, "description");
14786
+ if (description !== undefined)
14787
+ spec.description = description;
14788
+ const control = tagText(tag, "control");
14789
+ if (control === "select" || control === "multiselect") {
14790
+ spec.control = control;
14791
+ }
14792
+ const rangeMin = tagNumeric(tag, "range_min");
14793
+ if (rangeMin !== undefined)
14794
+ spec.rangeMin = rangeMin;
14795
+ const rangeMax = tagNumeric(tag, "range_max");
14796
+ if (rangeMax !== undefined)
14797
+ spec.rangeMax = rangeMax;
14798
+ const suggest = tag.tag("suggest");
14799
+ if (suggest) {
14800
+ const parsed = {};
14801
+ const query = presentText(suggest, "query");
14802
+ if (query !== undefined)
14803
+ parsed.query = query;
14804
+ const source = presentText(suggest, "source");
14805
+ if (source !== undefined)
14806
+ parsed.source = source;
14807
+ const dimension = presentText(suggest, "dimension");
14808
+ if (dimension !== undefined)
14809
+ parsed.dimension = dimension;
14810
+ const runnable = parsed.query !== undefined || parsed.source !== undefined && parsed.dimension !== undefined;
14811
+ if (runnable)
14812
+ spec.suggest = parsed;
14813
+ }
14814
+ return spec;
14815
+ }
14816
+ function malloyGivenToApi(given) {
14817
+ const type = given.type;
14818
+ const renderedType = type.type === "filter expression" ? `filter<${type.filterType}>` : type.type === "array" && type.elementTypeDef?.type ? `${type.elementTypeDef.type}[]` : type.type;
14819
+ const allNotes = given.annotations.forRoute(undefined);
14820
+ return {
14821
+ name: given.name,
14822
+ type: renderedType,
14823
+ annotations: allNotes.filter((note) => !isReservedRoute(note.route)).map((note) => note.text),
14824
+ ...readGivenControlSpec(allNotes.map((note) => note.text)),
14825
+ default: given._internal?.defaultText
14826
+ };
14827
+ }
14828
+ function collectGivenRefs(value, into) {
14829
+ if (Array.isArray(value)) {
14830
+ for (const item of value)
14831
+ collectGivenRefs(item, into);
14832
+ return;
14833
+ }
14834
+ if (value === null || typeof value !== "object")
14835
+ return;
14836
+ const node = value;
14837
+ if (node.node === "given" && typeof node.refName === "string") {
14838
+ into.add(node.refName);
14839
+ }
14840
+ for (const child of Object.values(node))
14841
+ collectGivenRefs(child, into);
14842
+ }
14843
+ function gateGivenSource(sources, name) {
14844
+ const source = sources.find((candidate) => candidate.name === name);
14845
+ if (!source)
14846
+ return;
14847
+ return [...source.authorize ?? [], ...source.accessFilter ?? []];
14848
+ }
14849
+ function suggestGivenLookup(modelDef, gatesBySource, surfaced) {
14850
+ const registry = modelDef.givens ?? {};
14851
+ const bySource = new Map;
14852
+ const byQuery = new Map;
14853
+ for (const obj of Object.values(modelDef.contents)) {
14854
+ if (isSourceDef4(obj)) {
14855
+ const name = obj.as || obj.name;
14856
+ const refs = new Set;
14857
+ collectGivenRefs(obj.filterList, refs);
14858
+ for (const expr of gatesBySource(name) ?? []) {
14859
+ for (const given of referencedGivenNames(expr))
14860
+ refs.add(given);
14861
+ }
14862
+ bySource.set(name, Array.from(refs));
14863
+ } else if (obj.type === "query") {
14864
+ const query = obj;
14865
+ byQuery.set(query.as || query.name, {
14866
+ own: (query.givenUsage ?? []).map((usage) => registry[usage.id]?.name).filter((n) => n !== undefined),
14867
+ source: typeof query.structRef === "string" ? query.structRef : undefined
14868
+ });
14869
+ }
14870
+ }
14871
+ const narrow = (names) => Array.from(new Set(names)).filter((name) => surfaced === undefined || surfaced.has(name));
14872
+ return {
14873
+ forSource: (name) => {
14874
+ const found = bySource.get(name);
14875
+ return found && narrow(found);
14876
+ },
14877
+ forQuery: (name) => {
14878
+ const found = byQuery.get(name);
14879
+ if (!found)
14880
+ return;
14881
+ return narrow([
14882
+ ...found.own,
14883
+ ...found.source ? bySource.get(found.source) ?? [] : []
14884
+ ]);
14885
+ }
14886
+ };
14887
+ }
14888
+ function suggestGivenNames(suggest, lookup) {
14889
+ const names = suggest.query !== undefined ? lookup.forQuery(suggest.query) : suggest.source !== undefined ? lookup.forSource(suggest.source) : undefined;
14890
+ return names && names.length > 0 ? names : undefined;
14891
+ }
14892
+ function attachSuggestGivenNames(givens, lookup) {
14893
+ for (const given of givens ?? []) {
14894
+ if (!given.suggest)
14895
+ continue;
14896
+ const names = suggestGivenNames(given.suggest, lookup);
14897
+ if (names)
14898
+ given.suggest.givenNames = names;
14899
+ else
14900
+ delete given.suggest.givenNames;
14901
+ }
14902
+ }
14903
+ var init_given = __esm(() => {
14904
+ init_annotations();
14905
+ init_authorize();
14906
+ init_motly();
14907
+ });
14908
+
14909
+ // src/service/dashboard.ts
14910
+ import { isSourceDef as isSourceDef5 } from "@malloydata/malloy";
14911
+ function filterPublisherOwnedRenderLogs(logs, modelPath) {
14912
+ const couldBeATile = isDashboardModelPath(modelPath);
14913
+ return logs.filter((log) => {
14914
+ const match = UNKNOWN_RENDER_TAG.exec(log.message ?? "");
14915
+ if (!match)
14916
+ return true;
14917
+ const [, tag, field] = match;
14918
+ const root = tag.split(".")[0];
14919
+ if (PUBLISHER_OWNED_TAGS.includes(root))
14920
+ return false;
14921
+ return !(couldBeATile && RENDERER_CHILD_TAGS.includes(root) && field === "root");
14922
+ });
14923
+ }
14924
+ function isDashboardModelPath(modelPath) {
14925
+ if (!modelPath.endsWith(MODEL_FILE_SUFFIX2))
14669
14926
  return false;
14670
- for (const id of ids) {
14671
- const entry = modelDef.sourceRegistry?.[id]?.entry;
14672
- if (!entry)
14927
+ const segments = modelPath.split("/");
14928
+ return segments.length === 2 && segments[0] === DASHBOARDS_DIR;
14929
+ }
14930
+ function dashboardSlug(modelPath) {
14931
+ const basename = modelPath.slice(modelPath.lastIndexOf("/") + 1);
14932
+ return basename.slice(0, -MODEL_FILE_SUFFIX2.length);
14933
+ }
14934
+ function matchesDocumentedDashboardName(slug) {
14935
+ return /^[a-zA-Z0-9_-]+$/.test(slug);
14936
+ }
14937
+ function readArtifactTag(tag, declaredType) {
14938
+ const artifact = tag.tag("artifact");
14939
+ if (!artifact)
14940
+ return;
14941
+ const givens = readStartingGivens(artifact, declaredType);
14942
+ const autorun = readAutorun(artifact);
14943
+ return {
14944
+ title: tagText(artifact, "title"),
14945
+ tiles: artifact.array("tiles")?.map((tile) => tagText(tile)).filter((tile) => tile !== undefined),
14946
+ dashboardColumns: positiveInteger(tagNumeric(tag.tag("dashboard"), "columns")),
14947
+ givens,
14948
+ autorun
14949
+ };
14950
+ }
14951
+ function positiveInteger(value) {
14952
+ return value !== undefined && Number.isInteger(value) && value >= 1 ? value : undefined;
14953
+ }
14954
+ function normalizeTileExpression(tile) {
14955
+ return tile.split("->").map((part) => part.trim().replace(/\s+/g, " ")).join(" -> ").replace(/\s+/g, " ");
14956
+ }
14957
+ function readDashboardModelFacts(modelPath, modelDef, surfacedGivenNames, gatesBySource) {
14958
+ const registry = modelDef.givens ?? {};
14959
+ const surfaced = new Set(surfacedGivenNames);
14960
+ const givens = new Map;
14961
+ for (const given of Object.values(registry)) {
14962
+ if (!surfaced.has(given.name))
14963
+ continue;
14964
+ const type = given.type;
14965
+ const isFilter = type.type === "filter expression";
14966
+ givens.set(given.name, {
14967
+ name: given.name,
14968
+ type: isFilter ? `filter<${type.filterType}>` : type.type,
14969
+ default: given.defaultText,
14970
+ annotations: (given.annotations?.blockNotes ?? []).concat(given.annotations?.notes ?? []).map((note) => note.text)
14971
+ });
14972
+ }
14973
+ const isNamedQuery = (obj) => obj.type === "query";
14974
+ const queries = Object.values(modelDef.contents).filter(isNamedQuery).map((query) => ({
14975
+ name: query.as || query.name,
14976
+ annotations: [
14977
+ ...query.annotations?.blockNotes ?? [],
14978
+ ...query.annotations?.notes ?? []
14979
+ ].map((note) => note.text),
14980
+ givens: (query.givenUsage ?? []).map((usage) => registry[usage.id]?.name).filter((name) => name !== undefined),
14981
+ sourceName: typeof query.structRef === "string" ? query.structRef : undefined
14982
+ }));
14983
+ const viewGivens = new Map;
14984
+ const viewAnnotations = new Map;
14985
+ const sourceFields = new Map;
14986
+ const drills = [];
14987
+ for (const obj of Object.values(modelDef.contents)) {
14988
+ if (!isSourceDef5(obj))
14989
+ continue;
14990
+ const sourceName = obj.as || obj.name;
14991
+ const fieldNames = new Set;
14992
+ sourceFields.set(sourceName, fieldNames);
14993
+ const sourceGivens = new Set;
14994
+ collectGivenRefs(obj.filterList, sourceGivens);
14995
+ for (const field of obj.fields) {
14996
+ const fieldName = field.as || field.name;
14997
+ fieldNames.add(fieldName);
14998
+ if (field.type === "turtle") {
14999
+ const refs = new Set(sourceGivens);
15000
+ collectGivenRefs(field.pipeline, refs);
15001
+ const key = normalizeTileExpression(`${sourceName} -> ${fieldName}`);
15002
+ viewGivens.set(key, Array.from(refs));
15003
+ viewAnnotations.set(key, [
15004
+ ...field.annotations?.blockNotes ?? [],
15005
+ ...field.annotations?.notes ?? []
15006
+ ].map((note) => note.text));
15007
+ continue;
15008
+ }
15009
+ const drill = readDrillTag([
15010
+ ...field.annotations?.blockNotes ?? [],
15011
+ ...field.annotations?.notes ?? []
15012
+ ].map((note) => note.text));
15013
+ if (drill) {
15014
+ drills.push({ ...drill, source: sourceName, dimension: fieldName });
15015
+ }
15016
+ }
15017
+ }
15018
+ return {
15019
+ modelPath,
15020
+ modelAnnotations: ownModelNotes(modelDef),
15021
+ queries,
15022
+ givens,
15023
+ viewGivens,
15024
+ viewAnnotations,
15025
+ sourceFields,
15026
+ drills,
15027
+ suggestGivens: suggestGivenLookup(modelDef, (source) => gatesBySource?.get(source), surfaced)
15028
+ };
15029
+ }
15030
+ function readTileLayout(annotations) {
15031
+ const tag = motlyTag(annotations);
15032
+ if (!tag)
15033
+ return;
15034
+ const layout = {};
15035
+ const label = tagText(tag, "label");
15036
+ if (label !== undefined)
15037
+ layout.label = label;
15038
+ const subtitle = tagText(tag, "subtitle");
15039
+ if (subtitle !== undefined)
15040
+ layout.subtitle = subtitle;
15041
+ const colspan = positiveInteger(tagNumeric(tag, "colspan"));
15042
+ if (colspan !== undefined)
15043
+ layout.colspan = colspan;
15044
+ if (tag.has("break"))
15045
+ layout.break = true;
15046
+ if (tag.has("borderless"))
15047
+ layout.borderless = true;
15048
+ return Object.keys(layout).length > 0 ? layout : undefined;
15049
+ }
15050
+ function readDrillTag(annotations) {
15051
+ const drill = motlyTag(annotations)?.tag("drill");
15052
+ if (!drill)
15053
+ return;
15054
+ const to = tagTextArray(drill, "to") ?? [tagText(drill, "to")].filter(isString);
15055
+ const given = tagText(drill, "given");
15056
+ return given === undefined ? { to } : { to, given };
15057
+ }
15058
+ function isString(value) {
15059
+ return value !== undefined;
15060
+ }
15061
+ function tagTextArray(tag, ...path) {
15062
+ const items = tag?.array(...path);
15063
+ return items?.map((item) => tagText(item)).filter(isString);
15064
+ }
15065
+ function resolveTileGivens(tile, facts) {
15066
+ return referencedTileGivens(tile, facts)?.filter((name) => facts.givens.has(name));
15067
+ }
15068
+ function resolveTileLayout(tile, facts) {
15069
+ const annotations = tileAnnotations(tile, facts);
15070
+ return annotations && readTileLayout(annotations);
15071
+ }
15072
+ function tileAnnotations(tile, facts) {
15073
+ const normalized = normalizeTileExpression(tile);
15074
+ return facts.viewAnnotations.get(normalized) ?? facts.queries.find((query) => query.name === normalized)?.annotations;
15075
+ }
15076
+ function referencedTileGivens(tile, facts) {
15077
+ const normalized = normalizeTileExpression(tile);
15078
+ return facts.viewGivens.get(normalized) ?? facts.queries.find((query) => query.name === normalized)?.givens;
15079
+ }
15080
+ function buildGivenSpecs(names, facts) {
15081
+ const declarations = facts.givens;
15082
+ const specs = [];
15083
+ for (const name of new Set(names)) {
15084
+ const declaration = declarations.get(name);
15085
+ if (!declaration)
14673
15086
  continue;
14674
- const declared = entry.type === "source_registry_reference" ? modelDef.contents[entry.name] : entry;
14675
- if (declared && isSourceDef4(declared))
14676
- return false;
15087
+ specs.push(givenSpec(declaration, facts));
14677
15088
  }
14678
- return true;
15089
+ return specs;
14679
15090
  }
14680
- function isEarlierPosition(a, b) {
14681
- return a.line < b.line || a.line === b.line && a.character < b.character;
15091
+ function givenSpec(declaration, facts) {
15092
+ const control = readGivenControlSpec(declaration.annotations);
15093
+ if (control.suggest) {
15094
+ const names = suggestGivenNames(control.suggest, facts.suggestGivens);
15095
+ if (names)
15096
+ control.suggest.givenNames = names;
15097
+ }
15098
+ return {
15099
+ name: declaration.name,
15100
+ type: declaration.type,
15101
+ default: declaration.type.startsWith("filter<") && declaration.default !== undefined ? unwrapFilterLiteral(declaration.default) : declaration.default,
15102
+ annotations: declaration.annotations.filter((text) => /^##?\(/.test(text)),
15103
+ ...control
15104
+ };
14682
15105
  }
14683
- function considerNoteOwner(declaredBy, note, candidate) {
14684
- const location = candidate.location;
14685
- if (!location || location.url !== note.at.url)
14686
- return;
14687
- const currentLocation = declaredBy.get(note)?.location;
14688
- if (!currentLocation || isEarlierPosition(location.range.start, currentLocation.range.start)) {
14689
- declaredBy.set(note, candidate);
15106
+ function factsCarryArtifactTag(facts) {
15107
+ if (motlyTag(facts.modelAnnotations)?.tag("artifact"))
15108
+ return true;
15109
+ return facts.queries.some((query) => Boolean(motlyTag(query.annotations)?.tag("artifact")));
15110
+ }
15111
+ function buildDashboardManifest(facts) {
15112
+ const name = dashboardSlug(facts.modelPath);
15113
+ const base = {
15114
+ name,
15115
+ entryFile: facts.modelPath
15116
+ };
15117
+ const modelTag = motlyTag(facts.modelAnnotations);
15118
+ const declaredType = (name2) => facts.givens.get(name2)?.type;
15119
+ const composite = modelTag ? readArtifactTag(modelTag, declaredType) : undefined;
15120
+ if (composite?.tiles?.length) {
15121
+ const tiles = composite.tiles.map((query) => {
15122
+ const givenNames = resolveTileGivens(query, facts);
15123
+ const layout = resolveTileLayout(query, facts);
15124
+ return {
15125
+ query,
15126
+ ...givenNames ? { givenNames } : {},
15127
+ ...layout
15128
+ };
15129
+ });
15130
+ const doc = docCommentTitleAndDescription(facts.modelAnnotations, composite.title);
15131
+ return {
15132
+ ...base,
15133
+ title: doc.title ?? name,
15134
+ description: doc.description,
15135
+ tiles,
15136
+ dashboardColumns: composite.dashboardColumns,
15137
+ startingGivens: composite.givens,
15138
+ autorun: composite.autorun,
15139
+ givens: buildGivenSpecs(tiles.some((tile) => tile.givenNames === undefined) ? Array.from(facts.givens.keys()) : tiles.flatMap((tile) => tile.givenNames ?? []), facts)
15140
+ };
15141
+ }
15142
+ for (const query of facts.queries) {
15143
+ const tag = motlyTag(query.annotations);
15144
+ const artifact = tag ? readArtifactTag(tag, declaredType) : undefined;
15145
+ if (!artifact)
15146
+ continue;
15147
+ const doc = docCommentTitleAndDescription(query.annotations, artifact.title);
15148
+ return {
15149
+ ...base,
15150
+ title: doc.title ?? name,
15151
+ description: doc.description,
15152
+ query: query.name,
15153
+ dashboardColumns: artifact.dashboardColumns,
15154
+ startingGivens: artifact.givens,
15155
+ autorun: artifact.autorun,
15156
+ givens: buildGivenSpecs(query.givens, facts)
15157
+ };
14690
15158
  }
15159
+ return;
14691
15160
  }
14692
- function collectSourceInfos(modelDef) {
14693
- const names = Object.keys(modelDef.contents).filter((name) => isSourceDef4(modelDef.contents[name]));
14694
- const exports = [...new Set([...modelDef.exports, ...names])];
14695
- return modelDefToModelInfo({ ...modelDef, exports }).entries.filter((entry) => entry.kind === "source");
15161
+ function plainTileParts(tile) {
15162
+ const piped = /^(\w+)\s*->\s*(\w+)$/.exec(tile.trim());
15163
+ if (piped)
15164
+ return { source: piped[1], view: piped[2] };
15165
+ const bare = /^\w+$/.exec(tile.trim());
15166
+ return bare ? { name: tile.trim() } : undefined;
14696
15167
  }
14697
- function extractSourcesFromModelDef(modelDef, givens, onParseError) {
14698
- const filterMap = new Map;
14699
- const authorizeMap = new Map;
14700
- const misplacedAuthorize = [];
14701
- const gatedSourceOwnAuthorizeNotes = new Set;
14702
- const authorizeNoteDeclaredBy = new Map;
14703
- const nearMissAuthorize = [];
14704
- const sweptStructs = [];
14705
- for (const obj of Object.values(modelDef.contents)) {
14706
- if (!isSourceDef4(obj))
15168
+ function lintDashboard(facts, manifest) {
15169
+ const findings = [];
15170
+ const slug = manifest.name;
15171
+ const add = (message, severity = "warn") => findings.push({ subject: slug, message, severity });
15172
+ const ownAnnotations = manifest.tiles ? facts.modelAnnotations : facts.queries.find((query) => query.name === manifest.query)?.annotations ?? [];
15173
+ const ownTag = motlyTag(ownAnnotations);
15174
+ const artifactTag = ownTag?.tag("artifact");
15175
+ const lintTargets = new Set([...facts.modelAnnotations, ...ownAnnotations]);
15176
+ for (const message of new Set(motlyParseErrors([...lintTargets]))) {
15177
+ add(`Annotation ${describeParseFailure(message)}, so that line had no ` + `effect on this dashboard.`);
15178
+ }
15179
+ if (!manifest.tiles) {
15180
+ const modelArtifact = motlyTag(facts.modelAnnotations)?.tag("artifact");
15181
+ if (modelArtifact) {
15182
+ add(`${describeTilelessModelArtifact(modelArtifact)}, so it builds ` + `nothing and this dashboard is the query's own '# artifact' ` + `instead. ${NAME_THE_TILES}`);
15183
+ }
15184
+ }
15185
+ const knownProperties = manifest.tiles ? COMPOSITE_ARTIFACT_PROPERTIES : QUERY_ARTIFACT_PROPERTIES;
15186
+ for (const property of Object.keys(artifactTag?.dict ?? {})) {
15187
+ if (knownProperties.includes(property))
14707
15188
  continue;
14708
- const struct = obj;
14709
- sweptStructs.push(obj);
14710
- for (const note of ownLevelNotes(struct.annotations)) {
14711
- if (containsAuthorizeAnnotationTag([note.text])) {
14712
- gatedSourceOwnAuthorizeNotes.add(note);
14713
- considerNoteOwner(authorizeNoteDeclaredBy, note, struct);
14714
- }
14715
- }
14716
- nearMissAuthorize.push(...collectAuthorizeNearMissesAllRoutes([
14717
- ...ownLevelNotes(struct.annotations),
14718
- ...struct.fields.flatMap((field) => ownLevelNotes(field.annotations))
14719
- ].map((note) => note.text)));
15189
+ add(`\`${property}\` in the artifact tag does nothing in Publisher` + (ARTIFACT_PROPERTY_REPLACEMENTS[property] ? `. ${ARTIFACT_PROPERTY_REPLACEMENTS[property]}` : property === "tiles" ? `: a dashboard whose tag sits on a query is that one query's ` + `result. Move the tag to model level (\`## artifact\`) to ` + `combine separate queries into a grid.` : `.`));
14720
15190
  }
14721
- for (const value of Object.values(modelDef.sourceRegistry ?? {})) {
14722
- const entry = value.entry;
14723
- if (entry.type === "source_registry_reference")
15191
+ for (const entry of artifactTag?.array("tiles") ?? []) {
15192
+ const carried = Object.keys(entry.dict ?? {});
15193
+ if (carried.length === 0)
14724
15194
  continue;
14725
- if (!isSourceDef4(entry))
15195
+ const named = carried.map((property) => `\`${property}\``).join(", ");
15196
+ add(`\`${tagText(entry) ?? "a tile"}\` in \`tiles=[…]\` carries ${named}, ` + `which Publisher does not read: a tile entry is the run expression ` + `alone. Per-tile presentation goes on the view the tile names ` + `(\`# colspan\`, \`# break\`, \`# label\`, \`# subtitle\`, ` + `\`# borderless\`).`);
15197
+ }
15198
+ for (const { query: tile } of manifest.tiles ?? []) {
15199
+ const tag = motlyTag(tileAnnotations(tile, facts) ?? []);
15200
+ if (!tag?.has("colspan"))
15201
+ continue;
15202
+ const colspan = positiveInteger(tagNumeric(tag, "colspan"));
15203
+ if (colspan === undefined) {
15204
+ const raw = tagText(tag, "colspan");
15205
+ add(`# colspan on "${tile}" must be a positive integer, got ` + `${raw === undefined ? "a value that could not be read" : JSON.stringify(raw)}. ` + `The tile spans one column.`);
14726
15206
  continue;
14727
- sweptStructs.push(entry);
14728
- for (const note of ownLevelNotes(entry.annotations)) {
14729
- if (containsAuthorizeAnnotationTag([note.text])) {
14730
- gatedSourceOwnAuthorizeNotes.add(note);
14731
- considerNoteOwner(authorizeNoteDeclaredBy, note, entry);
14732
- }
14733
15207
  }
14734
- nearMissAuthorize.push(...collectAuthorizeNearMissesAllRoutes(ownLevelNotes(entry.annotations).map((note) => note.text)));
14735
- }
14736
- for (const struct of derivedStructsReachable(sweptStructs, modelDef)) {
14737
- nearMissAuthorize.push(...collectAuthorizeNearMissesAllRoutes([
14738
- ...ownLevelNotes(struct.annotations),
14739
- ...struct.fields.flatMap((field) => ownLevelNotes(field.annotations))
14740
- ].map((note) => note.text)));
14741
- }
14742
- {
14743
- const folded = modelAnnotations(modelDef);
14744
- nearMissAuthorize.push(...collectAuthorizeNearMissesAllRoutes([...folded.notes ?? [], ...folded.blockNotes ?? []].map((note) => note.text)));
14745
- }
14746
- assertNoAuthorizeNearMisses(nearMissAuthorize);
14747
- const authorizeOwnNotes = new Map;
14748
- const attributedAuthorizeOwnNotes = new Map;
14749
- {
14750
- const fileNoteTexts = ownLevelNoteTexts(modelAnnotations(modelDef));
14751
- if (containsAuthorizeAnnotationTag(fileNoteTexts)) {
14752
- misplacedAuthorize.push({
14753
- kind: "file",
14754
- route: fileNoteTexts.map((text) => authorizeAnnotationRoute(text)).find((r) => r !== undefined) ?? ACCESS_FILTER_ROUTE
14755
- });
15208
+ const columns = manifest.dashboardColumns;
15209
+ if (columns === undefined) {
15210
+ add(`# colspan=${colspan} on "${tile}" needs a grid to span. This ` + `dashboard declares no # dashboard { columns=N }, so it lays out ` + `at the viewer's default width and every colspan is clamped to it.`);
15211
+ } else if (colspan > columns) {
15212
+ add(`# colspan=${colspan} on "${tile}" exceeds the ${columns} columns ` + `of this dashboard and is clamped to ${columns}. Widen ` + `# dashboard { columns=… } or lower the colspan.`);
14756
15213
  }
14757
15214
  }
14758
- const sources = Object.values(modelDef.contents).filter((obj) => isSourceDef4(obj)).map((sourceObj) => {
14759
- const struct = sourceObj;
14760
- const sourceName = struct.as || struct.name;
14761
- const annotations = annotationTexts(struct.annotations);
14762
- const collected = [];
14763
- let cur = struct.annotations;
14764
- while (cur) {
14765
- if (cur.blockNotes) {
14766
- collected.push(cur.blockNotes.map((note) => note.text));
14767
- }
14768
- cur = cur.inherits;
14769
- }
14770
- const allAnnotations = collected.reverse().flat();
14771
- let filters;
14772
- if (allAnnotations.length > 0) {
14773
- try {
14774
- const parsed = parseFilters(allAnnotations);
14775
- if (parsed.length > 0) {
14776
- filterMap.set(sourceName, parsed);
14777
- const fields = struct.fields;
14778
- filters = parsed.map((f) => {
14779
- const field = fields.find((fd) => (fd.as || fd.name) === f.dimension);
14780
- return {
14781
- name: f.name,
14782
- dimension: f.dimension,
14783
- type: f.type,
14784
- implicit: f.implicit,
14785
- required: f.required,
14786
- dimensionType: field?.type
14787
- };
14788
- });
14789
- }
14790
- } catch (err) {
14791
- onParseError?.(sourceName, err);
14792
- }
14793
- }
14794
- const ownNotes = ownLevelNoteTexts(struct.annotations);
14795
- const ownAuthorizeNotes = ownLevelNotes(struct.annotations).filter((note) => containsAuthorizeAnnotationTag([note.text]));
14796
- const ownNotesByRoute = new Map;
14797
- const attributedOwnNotesByRoute = new Map;
14798
- for (const route of CANONICAL_AUTHORIZE_ROUTES) {
14799
- const notesForRoute = ownAuthorizeNotes.filter((note) => authorizeAnnotationRoute(note.text) === route);
14800
- ownNotesByRoute.set(route, notesForRoute);
14801
- attributedOwnNotesByRoute.set(route, notesForRoute.filter((note) => authorizeNoteDeclaredBy.get(note) === struct));
15215
+ const columnsTag = ownTag?.tag("dashboard");
15216
+ if (columnsTag?.has("columns")) {
15217
+ if (positiveInteger(tagNumeric(columnsTag, "columns")) === undefined) {
15218
+ const raw = tagText(columnsTag, "columns");
15219
+ add(`# dashboard { columns=… } must be a positive integer, got ` + `${raw === undefined ? "a value that could not be read" : JSON.stringify(raw)}. ` + `The grid falls back to the default.`);
14802
15220
  }
14803
- authorizeOwnNotes.set(sourceName, ownNotesByRoute);
14804
- attributedAuthorizeOwnNotes.set(sourceName, attributedOwnNotesByRoute);
14805
- const groupsForSource = [];
14806
- const routeGroupsByRoute = new Map;
14807
- for (const route of CANONICAL_AUTHORIZE_ROUTES) {
14808
- const ownExprsForRoute = collectAuthorizeExprsForRoute(ownNotes, route);
14809
- const groupsForRoute = ownExprsForRoute.length > 0 ? [ownExprsForRoute] : effectiveAncestorGateExprs(struct, modelDef, route);
14810
- for (const exprs of groupsForRoute) {
14811
- groupsForSource.push({ route, exprs });
14812
- }
14813
- routeGroupsByRoute.set(route, groupsForRoute);
15221
+ }
15222
+ for (const { query: tile } of manifest.tiles ?? []) {
15223
+ const parts = plainTileParts(tile);
15224
+ if (!parts)
15225
+ continue;
15226
+ const resolved = referencedTileGivens(tile, facts) !== undefined || "name" in parts && facts.queries.some((query) => query.name === parts.name);
15227
+ if (!resolved) {
15228
+ const detail = "source" in parts ? facts.sourceFields.has(parts.source) ? `source "${parts.source}" has no view "${parts.view}"` : `no source "${parts.source}" in this file` : `no query named "${parts.name}" in this file`;
15229
+ add(`tile "${tile}" does not resolve: ${detail}.`, "error");
14814
15230
  }
14815
- if (groupsForSource.length > 0) {
14816
- authorizeMap.set(sourceName, groupsForSource);
15231
+ const unbindable = (referencedTileGivens(tile, facts) ?? []).filter((name) => !facts.givens.has(name));
15232
+ for (const name of unbindable) {
15233
+ add(`tile "${tile}" filters by given "${name}", which this file does ` + `not import, so no control is shown for it and it stays at its ` + `default. Add it to an import in ${facts.modelPath}.`);
14817
15234
  }
14818
- const routeGroupsToFlatWire = (groups) => groups && groups.length > 0 ? groups.flat() : undefined;
14819
- const authorize = routeGroupsToFlatWire(routeGroupsByRoute.get(AUTHORIZE_ROUTE));
14820
- const accessFilter = routeGroupsToFlatWire(routeGroupsByRoute.get(ACCESS_FILTER_ROUTE));
14821
- const views = struct.fields.filter((field) => field.type === "turtle").filter((turtle) => turtle.pipeline.map((stage) => stage.type).every((type) => type === "reduce")).map((turtle) => ({
14822
- name: turtle.as || turtle.name,
14823
- annotations: annotationTexts(turtle.annotations)
14824
- }));
14825
- for (const field of struct.fields) {
14826
- const fieldAuthorizeNotes = ownLevelNotes(field.annotations).filter((note) => containsAuthorizeAnnotationTag([note.text]));
14827
- if (fieldAuthorizeNotes.length === 0)
14828
- continue;
14829
- if (fieldAuthorizeNotes.every((note) => gatedSourceOwnAuthorizeNotes.has(note))) {
14830
- continue;
14831
- }
14832
- const fieldName = field.as || field.name;
14833
- if (isJoined3(field) && isSourceDef4(field)) {
14834
- const joinedStruct = field;
14835
- if (joinFieldNamesUnresolvableDeclaration(joinedStruct, modelDef)) {
14836
- continue;
14837
- }
14838
- misplacedAuthorize.push({
14839
- kind: "field",
14840
- name: sourceName,
14841
- fieldName,
14842
- route: fieldAuthorizeNotes.map((note) => authorizeAnnotationRoute(note.text)).find((r) => r !== undefined) ?? ACCESS_FILTER_ROUTE
14843
- });
15235
+ }
15236
+ if (manifest.query !== undefined) {
15237
+ const query = facts.queries.find((q) => q.name === manifest.query);
15238
+ for (const name of query?.givens ?? []) {
15239
+ if (facts.givens.has(name))
14844
15240
  continue;
14845
- }
14846
- misplacedAuthorize.push({
14847
- kind: "field",
14848
- name: sourceName,
14849
- fieldName,
14850
- route: fieldAuthorizeNotes.map((note) => authorizeAnnotationRoute(note.text)).find((r) => r !== undefined) ?? ACCESS_FILTER_ROUTE
14851
- });
15241
+ add(`query "${manifest.query}" filters by given "${name}", which this ` + `file does not import, so no control is shown for it and it ` + `stays at its default. Add it to an import in ` + `${facts.modelPath}.`);
14852
15242
  }
14853
- return {
14854
- name: sourceName,
14855
- annotations,
14856
- views,
14857
- filters,
14858
- givens,
14859
- authorize,
14860
- accessFilter
14861
- };
14862
- });
14863
- return {
14864
- sources,
14865
- filterMap,
14866
- authorizeMap,
14867
- misplacedAuthorize,
14868
- authorizeOwnNotes,
14869
- attributedAuthorizeOwnNotes
14870
- };
14871
- }
14872
- function extractQueriesFromModelDef(modelDef) {
14873
- const isNamedQuery = (obj) => obj.type === "query";
14874
- const namedQueries = Object.values(modelDef.contents).filter(isNamedQuery);
14875
- const misplacedAuthorize = namedQueries.filter((queryObj) => containsAuthorizeAnnotationTag(ownLevelNoteTexts(queryObj.annotations))).map((queryObj) => ({
14876
- kind: "query",
14877
- name: queryObj.as || queryObj.name,
14878
- route: ownLevelNoteTexts(queryObj.annotations).map((text) => authorizeAnnotationRoute(text)).find((r) => r !== undefined) ?? ACCESS_FILTER_ROUTE
14879
- }));
14880
- const queries = namedQueries.map((queryObj) => ({
14881
- name: queryObj.as || queryObj.name,
14882
- sourceName: typeof queryObj.structRef === "string" ? queryObj.structRef : undefined,
14883
- annotations: annotationTexts(queryObj.annotations)
14884
- }));
14885
- return { queries, misplacedAuthorize };
14886
- }
14887
- var init_source_extraction = __esm(() => {
14888
- init_annotations();
14889
- init_authorize();
14890
- init_authorize_routes();
14891
- init_filter();
14892
- init_gate_registry_walk();
14893
- });
14894
-
14895
- // src/service/motly.ts
14896
- import { parseAnnotation } from "@malloydata/malloy-tag";
14897
- function onMotlyRoute(text) {
14898
- const afterSigil = text.replace(/^##?\|?/, "");
14899
- return afterSigil === "" || /^[ \t\r\n]/.test(afterSigil);
14900
- }
14901
- function hasEnvReference(annotation) {
14902
- return annotation.includes("@env.");
14903
- }
14904
- function pollutionTargets() {
14905
- const targets = [Object.prototype];
14906
- for (const key of Object.getOwnPropertyNames(Object.prototype)) {
14907
- const descriptor = Object.getOwnPropertyDescriptor(Object.prototype, key);
14908
- if (!descriptor)
14909
- continue;
14910
- let value;
14911
- if ("value" in descriptor) {
14912
- value = descriptor.value;
14913
- } else {
14914
- if (BUILT_IN_PROTO_GETTER === undefined || descriptor.get !== BUILT_IN_PROTO_GETTER) {
14915
- return;
15243
+ }
15244
+ for (const name of Object.keys(manifest.startingGivens ?? {})) {
15245
+ if (facts.givens.has(name))
15246
+ continue;
15247
+ add(`'artifact { givens { ${name}=… } }' sets a starting value for given ` + `"${name}", which this file does not import, so the value is ` + `dropped at query time and the dashboard opens at the ` + `declaration's default. Add it to an import in ${facts.modelPath}.`);
15248
+ }
15249
+ for (const spec of manifest.givens) {
15250
+ const suggest = spec.suggest;
15251
+ if (!suggest) {
15252
+ const declared = motlyTag(facts.givens.get(spec.name)?.annotations ?? [])?.tag("suggest");
15253
+ if (declared) {
15254
+ const has = (key) => tagText(declared, key) !== undefined;
15255
+ const missing = has("dimension") ? `it names a dimension but no query= or source= to read it from` : has("source") ? `source= alone names no column to read options from` : `it names neither a query= nor a source=`;
15256
+ add(`given "${spec.name}" declares a suggest that cannot fetch ` + `options: ${missing}. Use query=, source= with dimension=, ` + `or query= with dimension=. No control options are offered ` + `for it.`, "error");
14916
15257
  }
14917
15258
  continue;
14918
15259
  }
14919
- if ((typeof value === "object" || typeof value === "function") && value !== null && !targets.includes(value)) {
14920
- targets.push(value);
15260
+ if (suggest.query !== undefined) {
15261
+ const named = facts.queries.find((query) => query.name === suggest.query);
15262
+ if (!named) {
15263
+ add(`given "${spec.name}" suggests options from query ` + `"${suggest.query}", which this file does not define.`, "error");
15264
+ } else if (named.sourceName !== undefined && !facts.sourceFields.has(named.sourceName)) {
15265
+ add(`given "${spec.name}" suggests options from query ` + `"${suggest.query}", which reads source ` + `"${named.sourceName}" that this file does not import. The ` + `query resolves by name, so the package loads, but the ` + `control answers "Undefined source '${named.sourceName}'" ` + `and offers no options. Import that source here too.`, "error");
15266
+ }
15267
+ }
15268
+ if (suggest.source !== undefined) {
15269
+ const fields = facts.sourceFields.get(suggest.source);
15270
+ if (!fields) {
15271
+ add(`given "${spec.name}" suggests options from source ` + `"${suggest.source}", which this file does not define.`, "error");
15272
+ } else if (suggest.dimension !== undefined && !fields.has(suggest.dimension)) {
15273
+ add(`given "${spec.name}" suggests options from ` + `"${suggest.source} -> ${suggest.dimension}", but that ` + `source has no field "${suggest.dimension}".`, "error");
15274
+ }
14921
15275
  }
14922
15276
  }
14923
- return targets;
15277
+ return findings;
14924
15278
  }
14925
- function parseGuarded(texts) {
14926
- const targets = pollutionTargets();
14927
- if (targets === undefined) {
14928
- return { tag: undefined, messages: [UNSAFE_TO_PARSE] };
15279
+ function describeParseFailure(message) {
15280
+ if (message === UNSAFE_TO_PARSE) {
15281
+ return `was refused rather than parsed (${message}), which can be caused by something outside this file`;
14929
15282
  }
14930
- let unreadable = false;
14931
- const ownNames = (target) => {
14932
- try {
14933
- return Object.getOwnPropertyNames(target);
14934
- } catch {
14935
- unreadable = true;
14936
- return [];
14937
- }
14938
- };
14939
- const before = targets.map(ownNames);
14940
- if (unreadable)
14941
- return { tag: undefined, messages: [UNSAFE_TO_PARSE] };
14942
- const undoPollution = () => {
14943
- let polluted = false;
14944
- targets.forEach((target, index) => {
14945
- for (const key of ownNames(target)) {
14946
- if (before[index].includes(key))
14947
- continue;
14948
- polluted = true;
14949
- try {
14950
- delete target[key];
14951
- } catch {}
14952
- }
14953
- });
14954
- return polluted || unreadable;
14955
- };
14956
- if (texts.some((text) => text.length > MAX_ANNOTATION_CHARS)) {
14957
- return { tag: undefined, messages: [ANNOTATION_TOO_LONG] };
15283
+ if (message === ENV_REFERENCE_DROPPED) {
15284
+ return `was dropped rather than parsed (${message}), so nothing on that line took effect`;
14958
15285
  }
14959
- try {
14960
- const result = parseAnnotation([...texts]);
14961
- if (undoPollution())
14962
- return { tag: undefined, messages: [UNSAFE_TO_PARSE] };
14963
- return {
14964
- tag: result.tag,
14965
- messages: result.log.map((error) => error.message)
14966
- };
14967
- } catch {
14968
- undoPollution();
14969
- return { tag: undefined, messages: [UNSAFE_TO_PARSE] };
15286
+ if (message === ANNOTATION_TOO_LONG) {
15287
+ return `was refused rather than parsed (${message})`;
14970
15288
  }
15289
+ return `does not parse (${message})`;
14971
15290
  }
14972
- function docCommentText(texts) {
14973
- const lines = texts.filter((text) => /^##?\|?"([ \t\r\n]|$)/.test(text)).map((text) => text.replace(/^##?\|?"[ \t\r\n]?/, "").trimEnd());
14974
- return lines.some((text) => text.trim().length > 0) ? lines.join(`
14975
- `) : undefined;
14976
- }
14977
- function splitDocComment(texts) {
14978
- const text = docCommentText(texts);
14979
- if (text === undefined)
14980
- return {};
14981
- const lines = text.split(`
14982
- `);
14983
- const titleLine = lines.findIndex((line) => line.trim().length > 0);
14984
- if (titleLine === -1)
14985
- return {};
14986
- const body = lines.slice(titleLine + 1).join(`
14987
- `).replace(/^[ \t\r\n]+/, "").trimEnd();
14988
- return {
14989
- title: lines[titleLine].trim(),
14990
- body: body.length > 0 ? body : undefined
14991
- };
14992
- }
14993
- function docCommentTitleAndDescription(texts, explicitTitle) {
14994
- if (explicitTitle !== undefined) {
14995
- return { title: explicitTitle, description: docCommentText(texts) };
15291
+ function lintUndiscoveredDashboard(facts) {
15292
+ const subject = dashboardSlug(facts.modelPath);
15293
+ const messages = [
15294
+ ...motlyParseErrors(facts.modelAnnotations),
15295
+ ...facts.queries.flatMap((query) => motlyParseErrors(query.annotations))
15296
+ ].map((message) => `Tag ${describeParseFailure(message)}, so the whole tag is discarded ` + `and this file is treated as a shared include rather than a dashboard.`);
15297
+ const findings = Array.from(new Set(messages), (message) => ({ subject, message, severity: "error" }));
15298
+ if (findings.length > 0)
15299
+ return findings;
15300
+ const modelArtifact = motlyTag(facts.modelAnnotations)?.tag("artifact");
15301
+ if (modelArtifact) {
15302
+ findings.push({
15303
+ subject,
15304
+ message: `${describeTilelessModelArtifact(modelArtifact)}, so this file ` + `produces no dashboard. ${NAME_THE_TILES}`,
15305
+ severity: "error"
15306
+ });
14996
15307
  }
14997
- const { title, body } = splitDocComment(texts);
14998
- return { title, description: body };
15308
+ findings.push(...lintArtifactOnView(facts));
15309
+ return findings;
14999
15310
  }
15000
- function quoteFilterLiterals(annotation) {
15001
- const VALUE_START = /[=[,]/;
15002
- const BARE_FILTER_LITERAL = /^([ \t]*)f(['"])((?:\\.|(?!\2)[^\\])*)\2/;
15003
- let out = "";
15004
- let i = 0;
15005
- while (i < annotation.length) {
15006
- const char = annotation[i];
15007
- const delimited = endOfDelimited(annotation, i);
15008
- if (delimited > i) {
15009
- out += annotation.slice(i, delimited);
15010
- i = delimited;
15311
+ function lintArtifactOnView(facts) {
15312
+ const subject = dashboardSlug(facts.modelPath);
15313
+ const findings = [];
15314
+ for (const [view, annotations] of facts.viewAnnotations) {
15315
+ if (!motlyTag(annotations)?.tag("artifact"))
15011
15316
  continue;
15012
- }
15013
- if (VALUE_START.test(char)) {
15014
- const match = BARE_FILTER_LITERAL.exec(annotation.slice(i + 1));
15015
- if (match) {
15016
- const body = match[3].replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
15017
- out += `${char}${match[1]}"f'${body}'"`;
15018
- i += 1 + match[0].length;
15019
- continue;
15317
+ const viewName = view.split("->").at(-1)?.trim() ?? view;
15318
+ findings.push({
15319
+ subject,
15320
+ message: `'# artifact' on view '${view}' is not read: Publisher reads the ` + `tag on a 'query:' or at model level, so this file produces no ` + `dashboard. Either name the view as a tile, ` + `## artifact { tiles=["${view}"] }, or tag a query that runs it, ` + `query: ${viewName} is ${view}.`,
15321
+ severity: "error"
15322
+ });
15323
+ }
15324
+ return findings;
15325
+ }
15326
+ function describeTilelessModelArtifact(modelArtifact) {
15327
+ return modelArtifact.array("tiles") === undefined ? `Model-level '## artifact' declares no tiles=` : `Model-level '## artifact' declares an empty tiles=`;
15328
+ }
15329
+ function lintGivenTags(facts) {
15330
+ const findings = new Map;
15331
+ for (const file of facts) {
15332
+ for (const [name, declaration] of file.givens) {
15333
+ for (const message of motlyParseErrors(declaration.annotations)) {
15334
+ findings.set(`${name}|${message}`, {
15335
+ subject: name,
15336
+ message: `given "${name}" has an annotation that ` + `${describeParseFailure(message)}, so the whole line is ` + `discarded and the given ` + `loses any label, control, range or suggest it declared. ` + `It still accepts values; only its presentation is lost.`,
15337
+ severity: "error"
15338
+ });
15020
15339
  }
15021
15340
  }
15022
- out += char;
15023
- i += 1;
15024
15341
  }
15025
- return out;
15342
+ return Array.from(findings.values());
15026
15343
  }
15027
- function endOfDelimited(text, start) {
15028
- const char = text[start];
15029
- if (char === "`") {
15030
- let i = start + 1;
15031
- while (i < text.length) {
15032
- if (text[i] === "\\") {
15033
- i += 2;
15344
+ function lintDrillTargets(facts, knownSlugs) {
15345
+ const findings = new Map;
15346
+ for (const file of facts) {
15347
+ for (const drill of file.drills) {
15348
+ const where = `${drill.source}.${drill.dimension}`;
15349
+ if (drill.to.length === 0) {
15350
+ findings.set(where, {
15351
+ subject: where,
15352
+ message: `# drill on ${where} names no destination (to=…).`,
15353
+ severity: "error"
15354
+ });
15034
15355
  continue;
15035
15356
  }
15036
- if (text[i] === "`")
15037
- return i + 1;
15038
- if (text[i] === `
15039
- `)
15040
- return text.length;
15041
- i += 1;
15357
+ for (const destination of drill.to) {
15358
+ if (destination === "self" || knownSlugs.has(destination))
15359
+ continue;
15360
+ findings.set(`${where}|${destination}`, {
15361
+ subject: where,
15362
+ message: `# drill on ${where} targets "${destination}", which is not ` + `a dashboard in this package.`,
15363
+ severity: "error"
15364
+ });
15365
+ }
15042
15366
  }
15043
- return text.length;
15044
15367
  }
15045
- if (text.startsWith("<<<", start)) {
15046
- let lineStart = start + 3;
15047
- for (;; ) {
15048
- const newline = text.indexOf(`
15049
- `, lineStart);
15050
- const lineEnd = newline === -1 ? text.length : newline;
15051
- if (text.slice(lineStart, lineEnd).trim() === ">>>")
15052
- return lineEnd;
15053
- if (newline === -1)
15054
- return text.length;
15055
- lineStart = newline + 1;
15368
+ return Array.from(findings.values());
15369
+ }
15370
+ function drillGivenName(drill) {
15371
+ return drill.given ?? drill.dimension;
15372
+ }
15373
+ function lintSelfDrills(facts) {
15374
+ const surfaced = new Set;
15375
+ for (const file of facts) {
15376
+ for (const name of file.givens.keys())
15377
+ surfaced.add(name.toLowerCase());
15378
+ }
15379
+ const findings = new Map;
15380
+ for (const file of facts) {
15381
+ for (const drill of file.drills) {
15382
+ if (!drill.to.includes("self"))
15383
+ continue;
15384
+ const given = drillGivenName(drill);
15385
+ if (surfaced.has(given.toLowerCase()))
15386
+ continue;
15387
+ const where = `${drill.source}.${drill.dimension}`;
15388
+ findings.set(where, {
15389
+ subject: where,
15390
+ message: `# drill on ${where} has to=self, but no model in this ` + `package declares a given "${given}" for the clicked value to ` + `filter on, so the drill cannot fire anywhere. Declare it, or ` + `point the tag at a given that exists with given=.`,
15391
+ severity: "error"
15392
+ });
15056
15393
  }
15057
15394
  }
15058
- if (char === '"' || char === "'") {
15059
- const triple = char.repeat(3);
15060
- if (text.startsWith(triple, start)) {
15061
- let i2 = start + triple.length;
15062
- while (i2 < text.length) {
15063
- if (text[i2] === "\\") {
15064
- i2 += 2;
15065
- continue;
15066
- }
15067
- if (text.startsWith(triple, i2))
15068
- return i2 + triple.length;
15069
- i2 += 1;
15395
+ return Array.from(findings.values());
15396
+ }
15397
+ var DASHBOARDS_DIR = "dashboards", COMPONENT_FILE_SUFFIXES, MODEL_FILE_SUFFIX2 = ".malloy", PUBLISHER_OWNED_TAGS, RENDERER_CHILD_TAGS, UNKNOWN_RENDER_TAG, COMPOSITE_ARTIFACT_PROPERTIES, QUERY_ARTIFACT_PROPERTIES, ARTIFACT_PROPERTY_REPLACEMENTS, NAME_THE_TILES = `Name the views to show: tiles=["source -> view", …].`;
15398
+ var init_dashboard = __esm(() => {
15399
+ init_annotations();
15400
+ init_given();
15401
+ init_motly();
15402
+ COMPONENT_FILE_SUFFIXES = [".jsx", ".tsx"];
15403
+ PUBLISHER_OWNED_TAGS = ["artifact", "drill"];
15404
+ RENDERER_CHILD_TAGS = ["colspan", "break", "subtitle", "borderless"];
15405
+ UNKNOWN_RENDER_TAG = /^Unknown render tag '([^']+)' on field '([^']*)'/;
15406
+ COMPOSITE_ARTIFACT_PROPERTIES = [
15407
+ "title",
15408
+ "tiles",
15409
+ "givens",
15410
+ "autorun"
15411
+ ];
15412
+ QUERY_ARTIFACT_PROPERTIES = COMPOSITE_ARTIFACT_PROPERTIES.filter((property) => property !== "tiles");
15413
+ ARTIFACT_PROPERTY_REPLACEMENTS = {
15414
+ dashboard_columns: "Write the grid width as `# dashboard { columns=N }` beside the artifact tag.",
15415
+ dashboard: "The grid width is a sibling of the artifact tag, not a property of it: " + "close the artifact braces first, as `## artifact { … } dashboard { columns=N }`."
15416
+ };
15417
+ });
15418
+
15419
+ // src/service/filter.ts
15420
+ function parseFilterAnnotation(annotation) {
15421
+ const trimmed = annotation.trim();
15422
+ if (!trimmed.startsWith(ANNOTATION_PREFIX)) {
15423
+ return null;
15424
+ }
15425
+ const body = trimmed.slice(ANNOTATION_PREFIX.length).trim();
15426
+ const tokens = tokenize(body);
15427
+ let name;
15428
+ let dimension;
15429
+ let type;
15430
+ let implicit = false;
15431
+ let required = false;
15432
+ for (const token of tokens) {
15433
+ if (token.includes("=")) {
15434
+ const eqIndex = token.indexOf("=");
15435
+ const key = token.slice(0, eqIndex).toLowerCase();
15436
+ const value = token.slice(eqIndex + 1);
15437
+ switch (key) {
15438
+ case "name":
15439
+ name = value;
15440
+ break;
15441
+ case "dimension":
15442
+ dimension = value;
15443
+ break;
15444
+ case "type":
15445
+ if (!VALID_FILTER_TYPES.has(value)) {
15446
+ throw new Error(`Invalid filter type "${value}". Must be one of: ${[...VALID_FILTER_TYPES].join(", ")}`);
15447
+ }
15448
+ type = value;
15449
+ break;
15450
+ default:
15451
+ throw new Error(`Unknown filter parameter "${key}"`);
15070
15452
  }
15071
- return text.length;
15072
- }
15073
- let i = start + 1;
15074
- while (i < text.length) {
15075
- if (text[i] === "\\") {
15076
- i += 2;
15077
- continue;
15453
+ } else {
15454
+ const flag = token.toLowerCase();
15455
+ if (flag === "implicit") {
15456
+ implicit = true;
15457
+ } else if (flag === "required") {
15458
+ required = true;
15459
+ } else {
15460
+ throw new Error(`Unknown filter flag "${token}"`);
15078
15461
  }
15079
- if (text[i] === char)
15080
- return i + 1;
15081
- if (text[i] === `
15082
- `)
15083
- return i;
15084
- i += 1;
15085
15462
  }
15086
- return text.length;
15087
15463
  }
15088
- return start;
15089
- }
15090
- function unwrapFilterLiteral(value) {
15091
- const match = /^f(['"])([\s\S]*)\1$/.exec(value);
15092
- return match ? match[2] : value;
15093
- }
15094
- function parseMotly(texts) {
15095
- const onRoute = texts.filter(onMotlyRoute);
15096
- const motly = onRoute.filter((text) => !hasEnvReference(text));
15097
- const envDropped = onRoute.length - motly.length;
15098
- const envErrors = envDropped > 0 ? [ENV_REFERENCE_DROPPED] : [];
15099
- if (motly.length === 0)
15100
- return { tag: undefined, errors: envErrors };
15101
- const direct = parseGuarded(motly);
15102
- if (direct.messages.length === 0)
15103
- return { tag: direct.tag, errors: envErrors };
15104
- const rescued = motly.map((text) => {
15105
- if (parseGuarded([text]).messages.length === 0)
15106
- return text;
15107
- const rewritten = quoteFilterLiterals(text);
15108
- return parseGuarded([rewritten]).messages.length === 0 ? rewritten : text;
15109
- });
15110
- const after = parseGuarded(rescued);
15111
- return { tag: after.tag, errors: [...envErrors, ...after.messages] };
15464
+ if (!dimension) {
15465
+ throw new Error("filter annotation missing required 'dimension' parameter");
15466
+ }
15467
+ if (!type) {
15468
+ throw new Error("filter annotation missing required 'type' parameter");
15469
+ }
15470
+ return {
15471
+ name: name ?? dimension,
15472
+ dimension,
15473
+ type,
15474
+ implicit,
15475
+ required
15476
+ };
15112
15477
  }
15113
- function tagText(tag, ...path) {
15114
- try {
15115
- return tag?.text(...path);
15116
- } catch {
15117
- return;
15478
+ function parseFilters(annotations) {
15479
+ const byName = new Map;
15480
+ for (const annotation of annotations) {
15481
+ const parsed = parseFilterAnnotation(annotation);
15482
+ if (parsed) {
15483
+ byName.set(parsed.name, parsed);
15484
+ }
15118
15485
  }
15486
+ return [...byName.values()];
15119
15487
  }
15120
- function tagNumeric(tag, ...path) {
15121
- try {
15122
- const raw = tagText(tag, ...path);
15123
- if (raw === undefined)
15124
- return;
15125
- const DECIMAL = /^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?$/;
15126
- const text = raw.trim();
15127
- if (!DECIMAL.test(text))
15128
- return;
15129
- const value = Number(text);
15130
- return Number.isFinite(value) ? value : undefined;
15131
- } catch {
15132
- return;
15133
- }
15488
+ function escapeMalloyString(value) {
15489
+ return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
15134
15490
  }
15135
- function motlyTag(texts) {
15136
- return parseMotly(texts).tag;
15491
+ function isBooleanLiteral(v) {
15492
+ const lower = v.toLowerCase();
15493
+ return lower === "true" || lower === "false";
15137
15494
  }
15138
- function motlyParseErrors(texts) {
15139
- return parseMotly(texts).errors;
15495
+ function isDateLiteral(v) {
15496
+ return ISO_DATE_RE.test(v) || ISO_TIMESTAMP_RE.test(v);
15140
15497
  }
15141
- function readAutorun(tag) {
15142
- return tagText(tag, "autorun") !== "false";
15498
+ function malloyLiteral(v) {
15499
+ if (isBooleanLiteral(v)) {
15500
+ return v.toLowerCase();
15501
+ }
15502
+ if (isDateLiteral(v)) {
15503
+ return `@${v.slice(0, 10)}`;
15504
+ }
15505
+ return `'${escapeMalloyString(v)}'`;
15143
15506
  }
15144
- function scalarTypeOf(tag) {
15145
- try {
15146
- return tag?.scalarType();
15147
- } catch {
15148
- return;
15507
+ function buildPredicate(filter, value) {
15508
+ const dim = `\`${filter.dimension}\``;
15509
+ switch (filter.type) {
15510
+ case "equal": {
15511
+ const v = Array.isArray(value) ? value[0] : value;
15512
+ return `${dim} = ${malloyLiteral(v)}`;
15513
+ }
15514
+ case "in": {
15515
+ const values = Array.isArray(value) ? value : [value];
15516
+ if (values.length === 1) {
15517
+ return `${dim} = ${malloyLiteral(values[0])}`;
15518
+ }
15519
+ const conditions = values.map((v) => `${dim} = ${malloyLiteral(v)}`);
15520
+ return `(${conditions.join(" or ")})`;
15521
+ }
15522
+ case "like": {
15523
+ const v = Array.isArray(value) ? value[0] : value;
15524
+ const escaped = escapeMalloyString(v.toLowerCase());
15525
+ const pattern = escaped.startsWith("%") || escaped.endsWith("%") ? escaped : `%${escaped}%`;
15526
+ return `lower(${dim}) ~ '${pattern}'`;
15527
+ }
15528
+ case "greater_than": {
15529
+ const v = Array.isArray(value) ? value[0] : value;
15530
+ return `${dim} > ${malloyLiteral(v)}`;
15531
+ }
15532
+ case "less_than": {
15533
+ const v = Array.isArray(value) ? value[0] : value;
15534
+ return `${dim} < ${malloyLiteral(v)}`;
15535
+ }
15149
15536
  }
15150
15537
  }
15151
- function authoredDay(isoText) {
15152
- const value = new Date(isoText);
15153
- if (Number.isNaN(value.getTime()))
15154
- return isoText.slice(0, 10);
15155
- const atUtcMidnight = value.getUTCHours() === 0 && value.getUTCMinutes() === 0 && value.getUTCSeconds() === 0 && value.getUTCMilliseconds() === 0;
15156
- const year = atUtcMidnight ? value.getUTCFullYear() : value.getFullYear();
15157
- const month = atUtcMidnight ? value.getUTCMonth() : value.getMonth();
15158
- const day = atUtcMidnight ? value.getUTCDate() : value.getDate();
15159
- return `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
15538
+ function filterHasValue(filter, params) {
15539
+ const value = params[filter.name];
15540
+ return value !== undefined && value !== null && (Array.isArray(value) ? value.length > 0 : value !== "");
15160
15541
  }
15161
- function readStartingGivens(tag, declaredType) {
15162
- const entries = tag?.tag("givens");
15163
- if (!entries)
15164
- return;
15165
- const collected = {};
15166
- for (const [name, value] of entries.entries()) {
15167
- const tag2 = value;
15168
- const text = tagText(tag2);
15169
- if (text === undefined)
15542
+ function buildFilterClause(filters, params) {
15543
+ const predicates = [];
15544
+ for (const filter of filters) {
15545
+ if (!filterHasValue(filter, params)) {
15546
+ if (filter.required) {
15547
+ throw new FilterValidationError(`Required filter "${filter.name}" (dimension: ${filter.dimension}) was not provided`);
15548
+ }
15170
15549
  continue;
15171
- const declared = declaredType(name);
15172
- collected[name] = scalarTypeOf(tag2) === "date" && declared !== "timestamp" && declared !== "timestamptz" ? authoredDay(text) : declared?.startsWith("filter<") ? unwrapFilterLiteral(text) : text;
15550
+ }
15551
+ predicates.push(buildPredicate(filter, params[filter.name]));
15173
15552
  }
15174
- return Object.keys(collected).length > 0 ? collected : undefined;
15553
+ if (predicates.length === 0) {
15554
+ return "";
15555
+ }
15556
+ return predicates.join(" and ");
15175
15557
  }
15176
- var BUILT_IN_PROTO_GETTER, MAX_ANNOTATION_CHARS = 8192, ANNOTATION_TOO_LONG, ENV_REFERENCE_DROPPED = "annotation dropped for carrying an @env. reference", UNSAFE_TO_PARSE = "annotation could not be parsed safely";
15177
- var init_motly = __esm(() => {
15178
- BUILT_IN_PROTO_GETTER = Object.getOwnPropertyDescriptor(Object.prototype, "__proto__")?.get;
15179
- ANNOTATION_TOO_LONG = `annotation exceeds ${MAX_ANNOTATION_CHARS} characters and was not parsed`;
15180
- });
15181
-
15182
- // src/service/given.ts
15183
- import {
15184
- isSourceDef as isSourceDef5
15185
- } from "@malloydata/malloy";
15186
- function presentText(tag, ...path) {
15187
- const raw = tagText(tag, ...path);
15188
- return raw !== undefined && raw.trim() !== "" ? raw : undefined;
15558
+ function injectFilterRefinement(query, filterClause) {
15559
+ if (!filterClause) {
15560
+ return query;
15561
+ }
15562
+ return `${query.trimEnd()}
15563
+ + {where: ${filterClause}}`;
15189
15564
  }
15190
- function readGivenControlSpec(annotationTexts2) {
15191
- const spec = {};
15192
- const tag = motlyTag(annotationTexts2);
15193
- if (!tag)
15194
- return spec;
15195
- const label = presentText(tag, "label");
15196
- if (label !== undefined)
15197
- spec.label = label;
15198
- const description = presentText(tag, "description");
15199
- if (description !== undefined)
15200
- spec.description = description;
15201
- const control = tagText(tag, "control");
15202
- if (control === "select" || control === "multiselect") {
15203
- spec.control = control;
15565
+ function tokenize(input) {
15566
+ const tokens = [];
15567
+ let current = "";
15568
+ let inQuote = false;
15569
+ let quoteChar = "";
15570
+ for (const ch of input) {
15571
+ if (inQuote) {
15572
+ if (ch === quoteChar) {
15573
+ inQuote = false;
15574
+ } else {
15575
+ current += ch;
15576
+ }
15577
+ } else if (ch === '"' || ch === "'") {
15578
+ inQuote = true;
15579
+ quoteChar = ch;
15580
+ } else if (ch === " " || ch === "\t") {
15581
+ if (current) {
15582
+ tokens.push(current);
15583
+ current = "";
15584
+ }
15585
+ } else {
15586
+ current += ch;
15587
+ }
15204
15588
  }
15205
- const rangeMin = tagNumeric(tag, "range_min");
15206
- if (rangeMin !== undefined)
15207
- spec.rangeMin = rangeMin;
15208
- const rangeMax = tagNumeric(tag, "range_max");
15209
- if (rangeMax !== undefined)
15210
- spec.rangeMax = rangeMax;
15211
- const suggest = tag.tag("suggest");
15212
- if (suggest) {
15213
- const parsed = {};
15214
- const query = presentText(suggest, "query");
15215
- if (query !== undefined)
15216
- parsed.query = query;
15217
- const source = presentText(suggest, "source");
15218
- if (source !== undefined)
15219
- parsed.source = source;
15220
- const dimension = presentText(suggest, "dimension");
15221
- if (dimension !== undefined)
15222
- parsed.dimension = dimension;
15223
- const runnable = parsed.query !== undefined || parsed.source !== undefined && parsed.dimension !== undefined;
15224
- if (runnable)
15225
- spec.suggest = parsed;
15589
+ if (current) {
15590
+ tokens.push(current);
15226
15591
  }
15227
- return spec;
15592
+ return tokens;
15228
15593
  }
15229
- function malloyGivenToApi(given) {
15230
- const type = given.type;
15231
- const renderedType = type.type === "filter expression" ? `filter<${type.filterType}>` : type.type === "array" && type.elementTypeDef?.type ? `${type.elementTypeDef.type}[]` : type.type;
15232
- const allNotes = given.annotations.forRoute(undefined);
15233
- return {
15234
- name: given.name,
15235
- type: renderedType,
15236
- annotations: allNotes.filter((note) => !isReservedRoute(note.route)).map((note) => note.text),
15237
- ...readGivenControlSpec(allNotes.map((note) => note.text)),
15238
- default: given._internal?.defaultText
15594
+ var VALID_FILTER_TYPES, ANNOTATION_PREFIX = "#(filter)", ISO_DATE_RE, ISO_TIMESTAMP_RE, FilterValidationError;
15595
+ var init_filter = __esm(() => {
15596
+ VALID_FILTER_TYPES = new Set([
15597
+ "equal",
15598
+ "in",
15599
+ "like",
15600
+ "greater_than",
15601
+ "less_than"
15602
+ ]);
15603
+ ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
15604
+ ISO_TIMESTAMP_RE = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}/;
15605
+ FilterValidationError = class FilterValidationError extends Error {
15606
+ constructor(message) {
15607
+ super(message);
15608
+ this.name = "FilterValidationError";
15609
+ }
15239
15610
  };
15240
- }
15241
- function collectGivenRefs(value, into) {
15242
- if (Array.isArray(value)) {
15243
- for (const item of value)
15244
- collectGivenRefs(item, into);
15245
- return;
15246
- }
15247
- if (value === null || typeof value !== "object")
15248
- return;
15249
- const node = value;
15250
- if (node.node === "given" && typeof node.refName === "string") {
15251
- into.add(node.refName);
15611
+ });
15612
+
15613
+ // src/service/source_extraction.ts
15614
+ import {
15615
+ isJoined as isJoined3,
15616
+ isSourceDef as isSourceDef6,
15617
+ modelDefToModelInfo
15618
+ } from "@malloydata/malloy";
15619
+ function joinFieldNamesUnresolvableDeclaration(field, modelDef) {
15620
+ const ids = [field.referenceID, field.sourceID].filter((id) => !!id);
15621
+ if (ids.length === 0)
15622
+ return false;
15623
+ for (const id of ids) {
15624
+ const entry = modelDef.sourceRegistry?.[id]?.entry;
15625
+ if (!entry)
15626
+ continue;
15627
+ const declared = entry.type === "source_registry_reference" ? modelDef.contents[entry.name] : entry;
15628
+ if (declared && isSourceDef6(declared))
15629
+ return false;
15252
15630
  }
15253
- for (const child of Object.values(node))
15254
- collectGivenRefs(child, into);
15631
+ return true;
15255
15632
  }
15256
- function gateGivenSource(sources, name) {
15257
- const source = sources.find((candidate) => candidate.name === name);
15258
- if (!source)
15633
+ function isEarlierPosition(a, b) {
15634
+ return a.line < b.line || a.line === b.line && a.character < b.character;
15635
+ }
15636
+ function considerNoteOwner(declaredBy, note, candidate) {
15637
+ const location = candidate.location;
15638
+ if (!location || location.url !== note.at.url)
15259
15639
  return;
15260
- return [...source.authorize ?? [], ...source.accessFilter ?? []];
15640
+ const currentLocation = declaredBy.get(note)?.location;
15641
+ if (!currentLocation || isEarlierPosition(location.range.start, currentLocation.range.start)) {
15642
+ declaredBy.set(note, candidate);
15643
+ }
15261
15644
  }
15262
- function suggestGivenLookup(modelDef, gatesBySource, surfaced) {
15263
- const registry = modelDef.givens ?? {};
15264
- const bySource = new Map;
15265
- const byQuery = new Map;
15645
+ function collectSourceInfos(modelDef) {
15646
+ const names = Object.keys(modelDef.contents).filter((name) => isSourceDef6(modelDef.contents[name]));
15647
+ const exports = [...new Set([...modelDef.exports, ...names])];
15648
+ return modelDefToModelInfo({ ...modelDef, exports }).entries.filter((entry) => entry.kind === "source");
15649
+ }
15650
+ function extractSourcesFromModelDef(modelDef, givens, onParseError) {
15651
+ const filterMap = new Map;
15652
+ const authorizeMap = new Map;
15653
+ const misplacedAuthorize = [];
15654
+ const gatedSourceOwnAuthorizeNotes = new Set;
15655
+ const authorizeNoteDeclaredBy = new Map;
15656
+ const nearMissAuthorize = [];
15657
+ const sweptStructs = [];
15266
15658
  for (const obj of Object.values(modelDef.contents)) {
15267
- if (isSourceDef5(obj)) {
15268
- const name = obj.as || obj.name;
15269
- const refs = new Set;
15270
- collectGivenRefs(obj.filterList, refs);
15271
- for (const expr of gatesBySource(name) ?? []) {
15272
- for (const given of referencedGivenNames(expr))
15273
- refs.add(given);
15659
+ if (!isSourceDef6(obj))
15660
+ continue;
15661
+ const struct = obj;
15662
+ sweptStructs.push(obj);
15663
+ for (const note of ownLevelNotes(struct.annotations)) {
15664
+ if (containsAuthorizeAnnotationTag([note.text])) {
15665
+ gatedSourceOwnAuthorizeNotes.add(note);
15666
+ considerNoteOwner(authorizeNoteDeclaredBy, note, struct);
15274
15667
  }
15275
- bySource.set(name, Array.from(refs));
15276
- } else if (obj.type === "query") {
15277
- const query = obj;
15278
- byQuery.set(query.as || query.name, {
15279
- own: (query.givenUsage ?? []).map((usage) => registry[usage.id]?.name).filter((n) => n !== undefined),
15280
- source: typeof query.structRef === "string" ? query.structRef : undefined
15668
+ }
15669
+ nearMissAuthorize.push(...collectAuthorizeNearMissesAllRoutes([
15670
+ ...ownLevelNotes(struct.annotations),
15671
+ ...struct.fields.flatMap((field) => ownLevelNotes(field.annotations))
15672
+ ].map((note) => note.text)));
15673
+ }
15674
+ for (const value of Object.values(modelDef.sourceRegistry ?? {})) {
15675
+ const entry = value.entry;
15676
+ if (entry.type === "source_registry_reference")
15677
+ continue;
15678
+ if (!isSourceDef6(entry))
15679
+ continue;
15680
+ sweptStructs.push(entry);
15681
+ for (const note of ownLevelNotes(entry.annotations)) {
15682
+ if (containsAuthorizeAnnotationTag([note.text])) {
15683
+ gatedSourceOwnAuthorizeNotes.add(note);
15684
+ considerNoteOwner(authorizeNoteDeclaredBy, note, entry);
15685
+ }
15686
+ }
15687
+ nearMissAuthorize.push(...collectAuthorizeNearMissesAllRoutes(ownLevelNotes(entry.annotations).map((note) => note.text)));
15688
+ }
15689
+ for (const struct of derivedStructsReachable(sweptStructs, modelDef)) {
15690
+ nearMissAuthorize.push(...collectAuthorizeNearMissesAllRoutes([
15691
+ ...ownLevelNotes(struct.annotations),
15692
+ ...struct.fields.flatMap((field) => ownLevelNotes(field.annotations))
15693
+ ].map((note) => note.text)));
15694
+ }
15695
+ {
15696
+ const folded = modelAnnotations(modelDef);
15697
+ nearMissAuthorize.push(...collectAuthorizeNearMissesAllRoutes([...folded.notes ?? [], ...folded.blockNotes ?? []].map((note) => note.text)));
15698
+ }
15699
+ assertNoAuthorizeNearMisses(nearMissAuthorize);
15700
+ const authorizeOwnNotes = new Map;
15701
+ const attributedAuthorizeOwnNotes = new Map;
15702
+ {
15703
+ const fileNoteTexts = ownLevelNoteTexts(modelAnnotations(modelDef));
15704
+ if (containsAuthorizeAnnotationTag(fileNoteTexts)) {
15705
+ misplacedAuthorize.push({
15706
+ kind: "file",
15707
+ route: fileNoteTexts.map((text) => authorizeAnnotationRoute(text)).find((r) => r !== undefined) ?? ACCESS_FILTER_ROUTE
15281
15708
  });
15282
15709
  }
15283
15710
  }
15284
- const narrow = (names) => Array.from(new Set(names)).filter((name) => surfaced === undefined || surfaced.has(name));
15285
- return {
15286
- forSource: (name) => {
15287
- const found = bySource.get(name);
15288
- return found && narrow(found);
15289
- },
15290
- forQuery: (name) => {
15291
- const found = byQuery.get(name);
15292
- if (!found)
15293
- return;
15294
- return narrow([
15295
- ...found.own,
15296
- ...found.source ? bySource.get(found.source) ?? [] : []
15297
- ]);
15711
+ const sources = Object.values(modelDef.contents).filter((obj) => isSourceDef6(obj)).map((sourceObj) => {
15712
+ const struct = sourceObj;
15713
+ const sourceName = struct.as || struct.name;
15714
+ const annotations = annotationTexts(struct.annotations);
15715
+ const collected = [];
15716
+ let cur = struct.annotations;
15717
+ while (cur) {
15718
+ if (cur.blockNotes) {
15719
+ collected.push(cur.blockNotes.map((note) => note.text));
15720
+ }
15721
+ cur = cur.inherits;
15722
+ }
15723
+ const allAnnotations = collected.reverse().flat();
15724
+ let filters;
15725
+ if (allAnnotations.length > 0) {
15726
+ try {
15727
+ const parsed = parseFilters(allAnnotations);
15728
+ if (parsed.length > 0) {
15729
+ filterMap.set(sourceName, parsed);
15730
+ const fields = struct.fields;
15731
+ filters = parsed.map((f) => {
15732
+ const field = fields.find((fd) => (fd.as || fd.name) === f.dimension);
15733
+ return {
15734
+ name: f.name,
15735
+ dimension: f.dimension,
15736
+ type: f.type,
15737
+ implicit: f.implicit,
15738
+ required: f.required,
15739
+ dimensionType: field?.type
15740
+ };
15741
+ });
15742
+ }
15743
+ } catch (err) {
15744
+ onParseError?.(sourceName, err);
15745
+ }
15746
+ }
15747
+ const ownNotes = ownLevelNoteTexts(struct.annotations);
15748
+ const ownAuthorizeNotes = ownLevelNotes(struct.annotations).filter((note) => containsAuthorizeAnnotationTag([note.text]));
15749
+ const ownNotesByRoute = new Map;
15750
+ const attributedOwnNotesByRoute = new Map;
15751
+ for (const route of CANONICAL_AUTHORIZE_ROUTES) {
15752
+ const notesForRoute = ownAuthorizeNotes.filter((note) => authorizeAnnotationRoute(note.text) === route);
15753
+ ownNotesByRoute.set(route, notesForRoute);
15754
+ attributedOwnNotesByRoute.set(route, notesForRoute.filter((note) => authorizeNoteDeclaredBy.get(note) === struct));
15755
+ }
15756
+ authorizeOwnNotes.set(sourceName, ownNotesByRoute);
15757
+ attributedAuthorizeOwnNotes.set(sourceName, attributedOwnNotesByRoute);
15758
+ const groupsForSource = [];
15759
+ const routeGroupsByRoute = new Map;
15760
+ for (const route of CANONICAL_AUTHORIZE_ROUTES) {
15761
+ const ownExprsForRoute = collectAuthorizeExprsForRoute(ownNotes, route);
15762
+ const groupsForRoute = ownExprsForRoute.length > 0 ? [ownExprsForRoute] : effectiveAncestorGateExprs(struct, modelDef, route);
15763
+ for (const exprs of groupsForRoute) {
15764
+ groupsForSource.push({ route, exprs });
15765
+ }
15766
+ routeGroupsByRoute.set(route, groupsForRoute);
15767
+ }
15768
+ if (groupsForSource.length > 0) {
15769
+ authorizeMap.set(sourceName, groupsForSource);
15770
+ }
15771
+ const routeGroupsToFlatWire = (groups) => groups && groups.length > 0 ? groups.flat() : undefined;
15772
+ const authorize = routeGroupsToFlatWire(routeGroupsByRoute.get(AUTHORIZE_ROUTE));
15773
+ const accessFilter = routeGroupsToFlatWire(routeGroupsByRoute.get(ACCESS_FILTER_ROUTE));
15774
+ const views = struct.fields.filter((field) => field.type === "turtle").filter((turtle) => turtle.pipeline.map((stage) => stage.type).every((type) => type === "reduce")).map((turtle) => ({
15775
+ name: turtle.as || turtle.name,
15776
+ annotations: annotationTexts(turtle.annotations)
15777
+ }));
15778
+ for (const field of struct.fields) {
15779
+ const fieldAuthorizeNotes = ownLevelNotes(field.annotations).filter((note) => containsAuthorizeAnnotationTag([note.text]));
15780
+ if (fieldAuthorizeNotes.length === 0)
15781
+ continue;
15782
+ if (fieldAuthorizeNotes.every((note) => gatedSourceOwnAuthorizeNotes.has(note))) {
15783
+ continue;
15784
+ }
15785
+ const fieldName = field.as || field.name;
15786
+ if (isJoined3(field) && isSourceDef6(field)) {
15787
+ const joinedStruct = field;
15788
+ if (joinFieldNamesUnresolvableDeclaration(joinedStruct, modelDef)) {
15789
+ continue;
15790
+ }
15791
+ misplacedAuthorize.push({
15792
+ kind: "field",
15793
+ name: sourceName,
15794
+ fieldName,
15795
+ route: fieldAuthorizeNotes.map((note) => authorizeAnnotationRoute(note.text)).find((r) => r !== undefined) ?? ACCESS_FILTER_ROUTE
15796
+ });
15797
+ continue;
15798
+ }
15799
+ misplacedAuthorize.push({
15800
+ kind: "field",
15801
+ name: sourceName,
15802
+ fieldName,
15803
+ route: fieldAuthorizeNotes.map((note) => authorizeAnnotationRoute(note.text)).find((r) => r !== undefined) ?? ACCESS_FILTER_ROUTE
15804
+ });
15298
15805
  }
15806
+ return {
15807
+ name: sourceName,
15808
+ annotations,
15809
+ views,
15810
+ filters,
15811
+ givens,
15812
+ authorize,
15813
+ accessFilter
15814
+ };
15815
+ });
15816
+ return {
15817
+ sources,
15818
+ filterMap,
15819
+ authorizeMap,
15820
+ misplacedAuthorize,
15821
+ authorizeOwnNotes,
15822
+ attributedAuthorizeOwnNotes
15299
15823
  };
15300
15824
  }
15301
- function suggestGivenNames(suggest, lookup) {
15302
- const names = suggest.query !== undefined ? lookup.forQuery(suggest.query) : suggest.source !== undefined ? lookup.forSource(suggest.source) : undefined;
15303
- return names && names.length > 0 ? names : undefined;
15304
- }
15305
- function attachSuggestGivenNames(givens, lookup) {
15306
- for (const given of givens ?? []) {
15307
- if (!given.suggest)
15308
- continue;
15309
- const names = suggestGivenNames(given.suggest, lookup);
15310
- if (names)
15311
- given.suggest.givenNames = names;
15312
- else
15313
- delete given.suggest.givenNames;
15314
- }
15825
+ function extractQueriesFromModelDef(modelDef) {
15826
+ const isNamedQuery = (obj) => obj.type === "query";
15827
+ const namedQueries = Object.values(modelDef.contents).filter(isNamedQuery);
15828
+ const misplacedAuthorize = namedQueries.filter((queryObj) => containsAuthorizeAnnotationTag(ownLevelNoteTexts(queryObj.annotations))).map((queryObj) => ({
15829
+ kind: "query",
15830
+ name: queryObj.as || queryObj.name,
15831
+ route: ownLevelNoteTexts(queryObj.annotations).map((text) => authorizeAnnotationRoute(text)).find((r) => r !== undefined) ?? ACCESS_FILTER_ROUTE
15832
+ }));
15833
+ const queries = namedQueries.map((queryObj) => ({
15834
+ name: queryObj.as || queryObj.name,
15835
+ sourceName: typeof queryObj.structRef === "string" ? queryObj.structRef : undefined,
15836
+ annotations: annotationTexts(queryObj.annotations)
15837
+ }));
15838
+ return { queries, misplacedAuthorize };
15315
15839
  }
15316
- var init_given = __esm(() => {
15840
+ var init_source_extraction = __esm(() => {
15317
15841
  init_annotations();
15318
15842
  init_authorize();
15319
- init_motly();
15843
+ init_authorize_routes();
15844
+ init_filter();
15845
+ init_gate_registry_walk();
15320
15846
  });
15321
15847
 
15322
15848
  // src/utils.ts
@@ -15353,7 +15879,7 @@ init_gate_dimension();
15353
15879
  var import_recursive_readdir = __toESM(require_recursive_readdir(), 1);
15354
15880
  import {
15355
15881
  contextOverlay,
15356
- isSourceDef as isSourceDef6,
15882
+ isSourceDef as isSourceDef7,
15357
15883
  MalloyConfig,
15358
15884
  MalloyError as MalloyError2,
15359
15885
  modelDefToModelInfo as modelDefToModelInfo2,
@@ -15370,6 +15896,8 @@ import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
15370
15896
 
15371
15897
  // src/service/package_manifest.ts
15372
15898
  init_constants();
15899
+ init_errors();
15900
+ init_dashboard();
15373
15901
  var FRESHNESS_FALLBACKS = ["live", "stale_ok", "fail"];
15374
15902
  var PACKAGE_SCOPES = ["version", "package"];
15375
15903
  function parsePackageScope(raw) {
@@ -15379,7 +15907,7 @@ function parsePackageScope(raw) {
15379
15907
  if (PACKAGE_SCOPES.includes(raw)) {
15380
15908
  return raw;
15381
15909
  }
15382
- throw new Error(`Invalid "scope" in the package manifest: ${JSON.stringify(raw)}. ` + `Expected "version" or "package" (default "package").`);
15910
+ throw new PackageManifestError(`Invalid "scope" in the package manifest: ${JSON.stringify(raw)}. ` + `Expected "version" or "package" (default "package").`);
15383
15911
  }
15384
15912
  function resolvePackageScope(rootRaw, materializationRaw) {
15385
15913
  const envelopeRaw = materializationRaw && typeof materializationRaw === "object" ? materializationRaw.scope : undefined;
@@ -15389,7 +15917,7 @@ function resolvePackageScope(rootRaw, materializationRaw) {
15389
15917
  const envelopeScope = parsePackageScope(envelopeRaw);
15390
15918
  if (rootDeclared && envelopeDeclared) {
15391
15919
  if (rootScope !== envelopeScope) {
15392
- throw new Error(`Conflicting "scope" in publisher.json: root "${rootScope}" vs ` + `"materialization.scope" "${envelopeScope}". The package cannot ` + `load until they agree. Edit "materialization": { "scope": ... } ` + `to the value you want and delete the root-level "scope" (the ` + `server rewrites both homes on its next manifest write).`);
15920
+ throw new PackageManifestError(`Conflicting "scope" in publisher.json: root "${rootScope}" vs ` + `"materialization.scope" "${envelopeScope}". The package cannot ` + `load until they agree. Edit "materialization": { "scope": ... } ` + `to the value you want and delete the root-level "scope" (the ` + `server rewrites both homes on its next manifest write).`);
15393
15921
  }
15394
15922
  return { scope: envelopeScope, warnings: [] };
15395
15923
  }
@@ -15502,43 +16030,51 @@ function resolveExplores(input) {
15502
16030
  const hasIndexModel = modelPaths.includes(INDEX_MODEL_NAME);
15503
16031
  const warnings = [];
15504
16032
  if (declaredExplores !== undefined && !(Array.isArray(declaredExplores) && declaredExplores.every((entry) => typeof entry === "string"))) {
15505
- throw new Error(`Invalid "explores" in publisher.json: expected an array of model ` + `paths, got ${JSON.stringify(declaredExplores)}. The package is ` + `not served, because ignoring the key would publish every source ` + `it was meant to withhold. Fix: "explores": ` + `["${INDEX_MODEL_NAME}"], or delete the key to use a root ` + `"${INDEX_MODEL_NAME}" instead.`);
16033
+ throw new PackageManifestError(`Invalid "explores" in publisher.json: it must be a list of file ` + `names, but is ${JSON.stringify(declaredExplores)}. The package ` + `was not loaded. Fix: delete "explores" and add an ` + `${INDEX_MODEL_NAME}.`);
15506
16034
  }
15507
16035
  const declared = Array.isArray(declaredExplores) ? declaredExplores.map((entry) => normalizeModelPath(entry)) : undefined;
15508
16036
  if (declaredQueryableSources !== undefined && declaredQueryableSources !== "all") {
15509
16037
  warnings.push(QUERYABLE_SOURCES_DEPRECATION);
15510
16038
  }
15511
16039
  if (declared !== undefined) {
15512
- if (declared.length === 0 && !hasIndexModel) {
15513
- warnings.push(EXPLORES_EMPTY_IS_UNCURATED);
15514
- } else if (declared.length === 0) {
15515
- warnings.push(EXPLORES_EMPTY_SUPPRESSES_CONVENTION);
15516
- } else if (declared.length === 1) {
15517
- warnings.push(EXPLORES_DEPRECATION);
16040
+ if (declared.length === 0) {
16041
+ warnings.push(hasIndexModel ? EXPLORES_EMPTY_SUPPRESSES_CONVENTION : EXPLORES_EMPTY_IS_UNCURATED);
16042
+ } else {
16043
+ warnings.push(exploresDeprecation(declared, hasIndexModel));
15518
16044
  }
15519
- if (hasIndexModel && declared.length > 0) {
15520
- if (!declared.includes(INDEX_MODEL_NAME)) {
15521
- warnings.push(exploresOmitsIndexModel(declared));
15522
- }
16045
+ if (hasIndexModel && declared.length > 0 && !declared.includes(INDEX_MODEL_NAME)) {
16046
+ warnings.push(EXPLORES_OMITS_INDEX_MODEL);
15523
16047
  }
15524
16048
  return { explores: declared, warnings };
15525
16049
  }
15526
16050
  if (!hasIndexModel) {
16051
+ for (const modelPath of modelPaths) {
16052
+ if (!modelPath.includes("/") && modelPath.toLowerCase() === INDEX_MODEL_NAME) {
16053
+ warnings.push(`${modelPath} is ignored: only a root file named exactly ` + `${INDEX_MODEL_NAME} decides what is published. Fix: rename ` + `it to ${INDEX_MODEL_NAME}.`);
16054
+ }
16055
+ }
15527
16056
  return { explores: undefined, warnings };
15528
16057
  }
15529
- if (modelPaths.some((modelPath) => modelPath.endsWith(MODEL_FILE_SUFFIX) && modelPath !== INDEX_MODEL_NAME)) {
15530
- warnings.push(INDEX_MODEL_IS_THE_SURFACE);
15531
- }
15532
16058
  return { explores: [INDEX_MODEL_NAME], warnings };
15533
16059
  }
15534
- var INDEX_MODEL_IS_THE_SURFACE = `This package has a root "${INDEX_MODEL_NAME}" and no "explores" in ` + `publisher.json, so that file is its published surface: only it is listed, ` + `and sources it does not "export { ... }" are refused by name with a 404. ` + `Other models still compile and are still importable and joinable. If that ` + `is intended, nothing to do. If this package is not meant to be curated, ` + `add "explores": [] to publisher.json, which keeps every model listed and ` + `queryable. Do NOT rename or delete the file to opt out: that widens the ` + `surface silently and breaks every import that names it.`;
15535
- function exploresOmitsIndexModel(declared) {
15536
- return `This package has an "${INDEX_MODEL_NAME}" but its publisher.json ` + `"explores" does not list it (${JSON.stringify(declared)}). The ` + `explicit key wins, so "${INDEX_MODEL_NAME}" is neither listed nor ` + `queryable. If that is intended, rename the file to silence this. If you ` + `meant it to be the package's entry point, delete "explores" and let the ` + `convention use it, or add it to the list.`;
16060
+ function listFiles(files) {
16061
+ return files.length <= 1 ? files[0] ?? "" : `${files.slice(0, -1).join(", ")} and ${files[files.length - 1]}`;
16062
+ }
16063
+ function exploresDeprecation(declared, hasIndexModel) {
16064
+ const lead = `"explores" in publisher.json is deprecated.`;
16065
+ const files = declared.filter((entry) => entry !== INDEX_MODEL_NAME && !isDashboardModelPath(entry));
16066
+ if (files.length > 0) {
16067
+ return hasIndexModel ? `${lead} Fix: import ${listFiles(files)} into ${INDEX_MODEL_NAME}, ` + `export the sources you publish from it, then delete "explores".` : `${lead} Fix: add an ${INDEX_MODEL_NAME} at the package root that ` + `imports ${listFiles(files)} and exports the sources you want to ` + `publish, then delete "explores".`;
16068
+ }
16069
+ if (declared.includes(INDEX_MODEL_NAME)) {
16070
+ return `${lead} ${INDEX_MODEL_NAME} already publishes the same thing. ` + `Fix: delete "explores".`;
16071
+ }
16072
+ return hasIndexModel ? `${lead} Fix: delete "explores", so ${INDEX_MODEL_NAME} decides what is published.` : `${lead} Fix: add an ${INDEX_MODEL_NAME} at the package root that ` + `exports the sources you want to publish, then delete "explores".`;
15537
16073
  }
15538
- var EXPLORES_DEPRECATION = `"explores" in publisher.json is deprecated: put an "${INDEX_MODEL_NAME}" ` + `at the package root that imports your models and "export { ... }"s what ` + `you publish, then delete the key. The file is the surface, it is checked ` + `by the compiler rather than by a path list, and it curates and enforces ` + `exactly as the key does. The key still works and is not going away in this ` + `release.`;
15539
- var EXPLORES_EMPTY_SUPPRESSES_CONVENTION = `"explores": [] in publisher.json is keeping this package uncurated. It has ` + `an "${INDEX_MODEL_NAME}", which would otherwise be its published surface: ` + `only that file would be listed, and sources it does not export would stop ` + `answering by name. The empty array suppresses that, so every model stays ` + `listed and queryable. This is supported and is the intended way to opt out ` + `-- do NOT delete the key unless you want the convention to take effect.`;
15540
- var EXPLORES_EMPTY_IS_UNCURATED = `"explores": [] in publisher.json is keeping this package uncurated: every ` + `model is listed and queryable by name. This is supported and is the ` + `intended way to say "do not curate". Nothing to do unless you meant to ` + `publish a surface, in which case list the models, or add a root ` + `"${INDEX_MODEL_NAME}" and delete the key.`;
15541
- var QUERYABLE_SOURCES_DEPRECATION = `"queryableSources" in publisher.json is deprecated. "declared" is already ` + `the default, so the key changes nothing, and an "${INDEX_MODEL_NAME}" ` + `gives the same curated-and-enforced surface with no manifest field at all. ` + `Delete it. The key still works and is not going away in this release.`;
16074
+ var EXPLORES_OMITS_INDEX_MODEL = `${INDEX_MODEL_NAME} is ignored because "explores" in publisher.json ` + `doesn't list it. Fix: delete "explores" to publish what ` + `${INDEX_MODEL_NAME} exports, or rename ${INDEX_MODEL_NAME} (and any import ` + `of it) if it isn't meant to decide what is published.`;
16075
+ var EXPLORES_EMPTY_SUPPRESSES_CONVENTION = `"explores" in publisher.json is deprecated. Here it stops ` + `${INDEX_MODEL_NAME} from limiting what this package publishes. Fix: to ` + `publish everything, rename ${INDEX_MODEL_NAME}, point any import of it at ` + `the new name, then delete "explores".`;
16076
+ var EXPLORES_EMPTY_IS_UNCURATED = `"explores": [] in publisher.json does nothing. Fix: delete it.`;
16077
+ var QUERYABLE_SOURCES_DEPRECATION = `"queryableSources" in publisher.json does nothing. Fix: delete it.`;
15542
16078
 
15543
16079
  // src/package_load/package_load_worker.ts
15544
16080
  init_source_extraction();
@@ -15776,7 +16312,15 @@ function buildWorkerMalloyConfig(job) {
15776
16312
  async function readPackageMetadata(packagePath, modelPaths) {
15777
16313
  const manifestPath = path2.join(packagePath, PACKAGE_MANIFEST_NAME);
15778
16314
  const contents = await fs2.promises.readFile(manifestPath, "utf8");
15779
- const parsed = JSON.parse(contents);
16315
+ let parsed;
16316
+ try {
16317
+ parsed = JSON.parse(contents);
16318
+ } catch (error) {
16319
+ throw new PackageManifestError(`Invalid ${PACKAGE_MANIFEST_NAME}: it is not valid JSON ` + `(${error instanceof Error ? error.message : String(error)}). ` + `The package is not served until the file parses. Fix: make it ` + `valid JSON; a trailing comma or an unquoted key is the usual cause.`);
16320
+ }
16321
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
16322
+ throw new PackageManifestError(`Invalid ${PACKAGE_MANIFEST_NAME}: expected a JSON object, got ` + `${JSON.stringify(parsed)}. Fix: { "name": "my-package" }.`);
16323
+ }
15780
16324
  const scope = resolvePackageScope(parsed.scope, parsed.materialization);
15781
16325
  const queryMetadata = resolvePackageQueryMetadata(parsed.queryMetadata, parsed.materialization);
15782
16326
  const explores = resolveExplores({
@@ -15910,7 +16454,7 @@ async function compileMalloyModel(job, malloyConfig, modelPath) {
15910
16454
  onRowLevelGateUnexpressible: authorizeWarningCollection.onRowLevelGateUnexpressible,
15911
16455
  onOwnRowLevelConditionCompiled: (sourceName, condition, route) => {
15912
16456
  const struct = modelDef.contents[sourceName];
15913
- if (!struct || !isSourceDef6(struct))
16457
+ if (!struct || !isSourceDef7(struct))
15914
16458
  return;
15915
16459
  validateSourceLineGateGivenUsage(sourceName, route, struct, condition.refSummary, condition.e, modelDef, (cause, detail) => {
15916
16460
  recordRowLevelGateRejected(cause);
@@ -16035,7 +16579,7 @@ async function compileNotebookModel(job, malloyConfig, modelPath) {
16035
16579
  onRowLevelGateUnexpressible: authorizeWarningCollection.onRowLevelGateUnexpressible,
16036
16580
  onOwnRowLevelConditionCompiled: (sourceName, condition, route) => {
16037
16581
  const struct = finalCompiledModelDef.contents[sourceName];
16038
- if (!struct || !isSourceDef6(struct))
16582
+ if (!struct || !isSourceDef7(struct))
16039
16583
  return;
16040
16584
  validateSourceLineGateGivenUsage(sourceName, route, struct, condition.refSummary, condition.e, finalCompiledModelDef, (cause, detail) => {
16041
16585
  recordRowLevelGateRejected(cause);
@@ -16133,6 +16677,14 @@ function serializeError(error) {
16133
16677
  isCompilationError: true
16134
16678
  };
16135
16679
  }
16680
+ if (error instanceof PackageManifestError) {
16681
+ return {
16682
+ name: error.name,
16683
+ message: error.message,
16684
+ stack: error.stack,
16685
+ isManifestError: true
16686
+ };
16687
+ }
16136
16688
  if (error instanceof Error) {
16137
16689
  return {
16138
16690
  name: error.name,