@malloy-publisher/server 0.8.0 → 0.8.2

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 (25) hide show
  1. package/dist/app/api-doc.yaml +122 -70
  2. package/dist/app/assets/EnvironmentPage-J-7-dQTF.js +1 -0
  3. package/dist/app/assets/HomePage-C_iQUW74.js +1 -0
  4. package/dist/app/assets/{LightMode-DyjqECoH.js → LightMode-X55OaqWn.js} +1 -1
  5. package/dist/app/assets/{MainPage-DAaQnh6y.js → MainPage-HlxDGd7Z.js} +1 -1
  6. package/dist/app/assets/ModelPage-4DQomN_d.js +2 -0
  7. package/dist/app/assets/PackagePage-B_-oiLXm.js +1 -0
  8. package/dist/app/assets/{RenderedResult-CYyhQekT.es-DJ3BRrLl.js → RenderedResult-DKOl0FQ8.es-BATOL6jV.js} +3 -3
  9. package/dist/app/assets/{RouteError-kdHl1qpH.js → RouteError-DWKxdq5d.js} +1 -1
  10. package/dist/app/assets/{ThemeEditorPage-DIt45eWv.js → ThemeEditorPage-Dc7994Z3.js} +1 -1
  11. package/dist/app/assets/{ToggleButtonGroup-Dl_oTC8r.js → ToggleButtonGroup-0p89QLtB.js} +1 -1
  12. package/dist/app/assets/{index-C5hv_y6G.js → index-C8Lbp1ov.js} +1 -1
  13. package/dist/app/assets/{index-C1qGlR2a.js → index-CSUU2t5Q.js} +92 -92
  14. package/dist/app/assets/{index-t9-rAE4l.js → index-CqxaMaWg.js} +1 -1
  15. package/dist/app/assets/{index-T1Cl-rQv.js → index-Csw7MKd3.js} +1 -1
  16. package/dist/app/assets/{index-Bzqs2UuN.js → index-YH_yBCQI.js} +1 -1
  17. package/dist/app/assets/{index.es-BlAu_Rj1.js → index.es-PC1YK9io.js} +23 -23
  18. package/dist/app/index.html +1 -1
  19. package/dist/package_load_worker.mjs +1350 -775
  20. package/dist/server.mjs +3481 -460
  21. package/package.json +1 -1
  22. package/dist/app/assets/EnvironmentPage-Be-JAMS6.js +0 -1
  23. package/dist/app/assets/HomePage-DsdQpZuT.js +0 -1
  24. package/dist/app/assets/ModelPage-B7yaRDmM.js +0 -2
  25. package/dist/app/assets/PackagePage-7b0klbWy.js +0 -1
@@ -12889,6 +12889,15 @@ function internalErrorToHttpError(error) {
12889
12889
  return httpError(404, error.message);
12890
12890
  } else if (error instanceof NotQueryableError) {
12891
12891
  return httpError(404, error.message);
12892
+ } else if (error instanceof QueryCompileError) {
12893
+ return {
12894
+ status: 400,
12895
+ json: {
12896
+ code: 400,
12897
+ message: error.message,
12898
+ problems: error.problems.map(toQueryTextProblem)
12899
+ }
12900
+ };
12892
12901
  } else if (error instanceof MalloyError) {
12893
12902
  return httpError(400, error.message);
12894
12903
  } else if (error instanceof TableNotFoundError) {
@@ -12905,6 +12914,8 @@ function internalErrorToHttpError(error) {
12905
12914
  return httpError(422, error.message);
12906
12915
  } else if (error instanceof ModelCompilationError) {
12907
12916
  return httpError(424, error.message);
12917
+ } else if (error instanceof PackageManifestError) {
12918
+ return httpError(424, error.message);
12908
12919
  } else if (error instanceof ConnectionError) {
12909
12920
  if (error.callerSafe) {
12910
12921
  return httpError(502, error.message);
@@ -12945,7 +12956,15 @@ function httpError(code, message, reason) {
12945
12956
  }
12946
12957
  };
12947
12958
  }
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;
12959
+ function toQueryTextProblem(problem) {
12960
+ return {
12961
+ message: problem.message,
12962
+ severity: problem.severity,
12963
+ code: problem.code,
12964
+ ...problem.at ? { at: { range: problem.at.range } } : {}
12965
+ };
12966
+ }
12967
+ 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, QueryCompileError, NotQueryableError, OffSurfaceError, MaterializationNotFoundError, MaterializationConflictError, WriteConflictError, WriteRolledBackError, InvalidStateTransitionError, ServiceUnavailableError, PayloadTooLargeError, ResponseUnserializableError, QueryTimeoutError;
12949
12968
  var init_errors = __esm(() => {
12950
12969
  init_constants();
12951
12970
  init_logger();
@@ -13020,6 +13039,12 @@ var init_errors = __esm(() => {
13020
13039
  super(error.message);
13021
13040
  }
13022
13041
  };
13042
+ PackageManifestError = class PackageManifestError extends Error {
13043
+ constructor(message) {
13044
+ super(message);
13045
+ this.name = "PackageManifestError";
13046
+ }
13047
+ };
13023
13048
  MaterializationEligibilityError = class MaterializationEligibilityError extends Error {
13024
13049
  reason;
13025
13050
  constructor(error) {
@@ -13046,12 +13071,24 @@ var init_errors = __esm(() => {
13046
13071
  this.name = "AccessDeniedError";
13047
13072
  }
13048
13073
  };
13074
+ QueryCompileError = class QueryCompileError extends MalloyError {
13075
+ constructor(message, problems) {
13076
+ super(message, problems);
13077
+ this.name = "QueryCompileError";
13078
+ }
13079
+ };
13049
13080
  NotQueryableError = class NotQueryableError extends Error {
13050
13081
  constructor(message) {
13051
13082
  super(message);
13052
13083
  this.name = "NotQueryableError";
13053
13084
  }
13054
13085
  };
13086
+ OffSurfaceError = class OffSurfaceError extends NotQueryableError {
13087
+ constructor(message) {
13088
+ super(message);
13089
+ this.name = "OffSurfaceError";
13090
+ }
13091
+ };
13055
13092
  MaterializationNotFoundError = class MaterializationNotFoundError extends Error {
13056
13093
  constructor(message) {
13057
13094
  super(message);
@@ -13419,11 +13456,11 @@ function recordAuthorizeBypass(entryPoint) {
13419
13456
  });
13420
13457
  bypassCounter.add(1, { entry_point: entryPoint });
13421
13458
  }
13422
- function recordRowLevelGateDecision(decision) {
13459
+ function recordRowLevelGateDecision(decision, site = "entry_point") {
13423
13460
  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."
13461
+ 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
13462
  });
13426
- rowLevelDecisionCounter.add(1, { decision });
13463
+ rowLevelDecisionCounter.add(1, { decision, site });
13427
13464
  }
13428
13465
  function recordRowLevelGateRejected(cause) {
13429
13466
  rowLevelRejectionCounter ??= publisherMeter().createCounter("publisher_authorize_row_level_rejected_total", {
@@ -13437,11 +13474,11 @@ function recordAuthorizeAdmitAllGate(route) {
13437
13474
  });
13438
13475
  admitAllCounter.add(1, { route });
13439
13476
  }
13440
- function recordLockDecision(decision) {
13477
+ function recordLockDecision(decision, site = "entry_point") {
13441
13478
  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."
13479
+ 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
13480
  });
13444
- lockDecisionCounter.add(1, { decision });
13481
+ lockDecisionCounter.add(1, { decision, site });
13445
13482
  }
13446
13483
  var guardRejectionCounter = null, bypassCounter = null, rowLevelDecisionCounter = null, rowLevelRejectionCounter = null, admitAllCounter = null, lockDecisionCounter = null;
13447
13484
  var init_authorize_metrics = __esm(() => {
@@ -14465,858 +14502,1370 @@ var init_gate_classification = __esm(() => {
14465
14502
  init_gate_registry_walk();
14466
14503
  });
14467
14504
 
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
- }
14505
+ // src/service/motly.ts
14506
+ import { parseAnnotation } from "@malloydata/malloy-tag";
14507
+ function onMotlyRoute(text) {
14508
+ const afterSigil = text.replace(/^##?\|?/, "");
14509
+ return afterSigil === "" || /^[ \t\r\n]/.test(afterSigil);
14510
+ }
14511
+ function hasEnvReference(annotation) {
14512
+ return annotation.includes("@env.");
14513
+ }
14514
+ function pollutionTargets() {
14515
+ const targets = [Object.prototype];
14516
+ for (const key of Object.getOwnPropertyNames(Object.prototype)) {
14517
+ const descriptor = Object.getOwnPropertyDescriptor(Object.prototype, key);
14518
+ if (!descriptor)
14519
+ continue;
14520
+ let value;
14521
+ if ("value" in descriptor) {
14522
+ value = descriptor.value;
14502
14523
  } 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}"`);
14524
+ if (BUILT_IN_PROTO_GETTER === undefined || descriptor.get !== BUILT_IN_PROTO_GETTER) {
14525
+ return;
14510
14526
  }
14527
+ continue;
14528
+ }
14529
+ if ((typeof value === "object" || typeof value === "function") && value !== null && !targets.includes(value)) {
14530
+ targets.push(value);
14511
14531
  }
14512
14532
  }
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
- };
14533
+ return targets;
14526
14534
  }
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);
14535
+ function parseGuarded(texts) {
14536
+ const targets = pollutionTargets();
14537
+ if (targets === undefined) {
14538
+ return { tag: undefined, messages: [UNSAFE_TO_PARSE] };
14539
+ }
14540
+ let unreadable = false;
14541
+ const ownNames = (target) => {
14542
+ try {
14543
+ return Object.getOwnPropertyNames(target);
14544
+ } catch {
14545
+ unreadable = true;
14546
+ return [];
14533
14547
  }
14548
+ };
14549
+ const before = targets.map(ownNames);
14550
+ if (unreadable)
14551
+ return { tag: undefined, messages: [UNSAFE_TO_PARSE] };
14552
+ const undoPollution = () => {
14553
+ let polluted = false;
14554
+ targets.forEach((target, index) => {
14555
+ for (const key of ownNames(target)) {
14556
+ if (before[index].includes(key))
14557
+ continue;
14558
+ polluted = true;
14559
+ try {
14560
+ delete target[key];
14561
+ } catch {}
14562
+ }
14563
+ });
14564
+ return polluted || unreadable;
14565
+ };
14566
+ if (texts.some((text) => text.length > MAX_ANNOTATION_CHARS)) {
14567
+ return { tag: undefined, messages: [ANNOTATION_TOO_LONG] };
14568
+ }
14569
+ try {
14570
+ const result = parseAnnotation([...texts]);
14571
+ if (undoPollution())
14572
+ return { tag: undefined, messages: [UNSAFE_TO_PARSE] };
14573
+ return {
14574
+ tag: result.tag,
14575
+ messages: result.log.map((error) => error.message)
14576
+ };
14577
+ } catch {
14578
+ undoPollution();
14579
+ return { tag: undefined, messages: [UNSAFE_TO_PARSE] };
14534
14580
  }
14535
- return [...byName.values()];
14536
- }
14537
- function escapeMalloyString(value) {
14538
- return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
14539
14581
  }
14540
- function isBooleanLiteral(v) {
14541
- const lower = v.toLowerCase();
14542
- return lower === "true" || lower === "false";
14582
+ function docCommentText(texts) {
14583
+ const lines = texts.filter((text) => /^##?\|?"([ \t\r\n]|$)/.test(text)).map((text) => text.replace(/^##?\|?"[ \t\r\n]?/, "").trimEnd());
14584
+ return lines.some((text) => text.trim().length > 0) ? lines.join(`
14585
+ `) : undefined;
14543
14586
  }
14544
- function isDateLiteral(v) {
14545
- return ISO_DATE_RE.test(v) || ISO_TIMESTAMP_RE.test(v);
14587
+ function splitDocComment(texts) {
14588
+ const text = docCommentText(texts);
14589
+ if (text === undefined)
14590
+ return {};
14591
+ const lines = text.split(`
14592
+ `);
14593
+ const titleLine = lines.findIndex((line) => line.trim().length > 0);
14594
+ if (titleLine === -1)
14595
+ return {};
14596
+ const body = lines.slice(titleLine + 1).join(`
14597
+ `).replace(/^[ \t\r\n]+/, "").trimEnd();
14598
+ return {
14599
+ title: lines[titleLine].trim(),
14600
+ body: body.length > 0 ? body : undefined
14601
+ };
14546
14602
  }
14547
- function malloyLiteral(v) {
14548
- if (isBooleanLiteral(v)) {
14549
- return v.toLowerCase();
14550
- }
14551
- if (isDateLiteral(v)) {
14552
- return `@${v.slice(0, 10)}`;
14603
+ function docCommentTitleAndDescription(texts, explicitTitle) {
14604
+ if (explicitTitle !== undefined) {
14605
+ return { title: explicitTitle, description: docCommentText(texts) };
14553
14606
  }
14554
- return `'${escapeMalloyString(v)}'`;
14607
+ const { title, body } = splitDocComment(texts);
14608
+ return { title, description: body };
14555
14609
  }
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)}`;
14610
+ function quoteFilterLiterals(annotation) {
14611
+ const VALUE_START = /[=[,]/;
14612
+ const BARE_FILTER_LITERAL = /^([ \t]*)f(['"])((?:\\.|(?!\2)[^\\])*)\2/;
14613
+ let out = "";
14614
+ let i = 0;
14615
+ while (i < annotation.length) {
14616
+ const char = annotation[i];
14617
+ const delimited = endOfDelimited(annotation, i);
14618
+ if (delimited > i) {
14619
+ out += annotation.slice(i, delimited);
14620
+ i = delimited;
14621
+ continue;
14562
14622
  }
14563
- case "in": {
14564
- const values = Array.isArray(value) ? value : [value];
14565
- if (values.length === 1) {
14566
- return `${dim} = ${malloyLiteral(values[0])}`;
14623
+ if (VALUE_START.test(char)) {
14624
+ const match = BARE_FILTER_LITERAL.exec(annotation.slice(i + 1));
14625
+ if (match) {
14626
+ const body = match[3].replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
14627
+ out += `${char}${match[1]}"f'${body}'"`;
14628
+ i += 1 + match[0].length;
14629
+ continue;
14567
14630
  }
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
14631
  }
14632
+ out += char;
14633
+ i += 1;
14585
14634
  }
14635
+ return out;
14586
14636
  }
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`);
14637
+ function endOfDelimited(text, start) {
14638
+ const char = text[start];
14639
+ if (char === "`") {
14640
+ let i = start + 1;
14641
+ while (i < text.length) {
14642
+ if (text[i] === "\\") {
14643
+ i += 2;
14644
+ continue;
14595
14645
  }
14596
- continue;
14646
+ if (text[i] === "`")
14647
+ return i + 1;
14648
+ if (text[i] === `
14649
+ `)
14650
+ return text.length;
14651
+ i += 1;
14597
14652
  }
14598
- predicates.push(buildPredicate(filter, value));
14653
+ return text.length;
14599
14654
  }
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;
14655
+ if (text.startsWith("<<<", start)) {
14656
+ let lineStart = start + 3;
14657
+ for (;; ) {
14658
+ const newline = text.indexOf(`
14659
+ `, lineStart);
14660
+ const lineEnd = newline === -1 ? text.length : newline;
14661
+ if (text.slice(lineStart, lineEnd).trim() === ">>>")
14662
+ return lineEnd;
14663
+ if (newline === -1)
14664
+ return text.length;
14665
+ lineStart = newline + 1;
14666
+ }
14608
14667
  }
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;
14668
+ if (char === '"' || char === "'") {
14669
+ const triple = char.repeat(3);
14670
+ if (text.startsWith(triple, start)) {
14671
+ let i2 = start + triple.length;
14672
+ while (i2 < text.length) {
14673
+ if (text[i2] === "\\") {
14674
+ i2 += 2;
14675
+ continue;
14676
+ }
14677
+ if (text.startsWith(triple, i2))
14678
+ return i2 + triple.length;
14679
+ i2 += 1;
14623
14680
  }
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 = "";
14681
+ return text.length;
14682
+ }
14683
+ let i = start + 1;
14684
+ while (i < text.length) {
14685
+ if (text[i] === "\\") {
14686
+ i += 2;
14687
+ continue;
14631
14688
  }
14632
- } else {
14633
- current += ch;
14689
+ if (text[i] === char)
14690
+ return i + 1;
14691
+ if (text[i] === `
14692
+ `)
14693
+ return i;
14694
+ i += 1;
14634
14695
  }
14696
+ return text.length;
14635
14697
  }
14636
- if (current) {
14637
- tokens.push(current);
14698
+ return start;
14699
+ }
14700
+ function unwrapFilterLiteral(value) {
14701
+ const match = /^f(['"])([\s\S]*)\1$/.exec(value);
14702
+ return match ? match[2] : value;
14703
+ }
14704
+ function parseMotly(texts) {
14705
+ const onRoute = texts.filter(onMotlyRoute);
14706
+ const motly = onRoute.filter((text) => !hasEnvReference(text));
14707
+ const envDropped = onRoute.length - motly.length;
14708
+ const envErrors = envDropped > 0 ? [ENV_REFERENCE_DROPPED] : [];
14709
+ if (motly.length === 0)
14710
+ return { tag: undefined, errors: envErrors };
14711
+ const direct = parseGuarded(motly);
14712
+ if (direct.messages.length === 0)
14713
+ return { tag: direct.tag, errors: envErrors };
14714
+ const rescued = motly.map((text) => {
14715
+ if (parseGuarded([text]).messages.length === 0)
14716
+ return text;
14717
+ const rewritten = quoteFilterLiterals(text);
14718
+ return parseGuarded([rewritten]).messages.length === 0 ? rewritten : text;
14719
+ });
14720
+ const after = parseGuarded(rescued);
14721
+ return { tag: after.tag, errors: [...envErrors, ...after.messages] };
14722
+ }
14723
+ function tagText(tag, ...path) {
14724
+ try {
14725
+ return tag?.text(...path);
14726
+ } catch {
14727
+ return;
14638
14728
  }
14639
- return tokens;
14640
14729
  }
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
- };
14730
+ function tagNumeric(tag, ...path) {
14731
+ try {
14732
+ const raw = tagText(tag, ...path);
14733
+ if (raw === undefined)
14734
+ return;
14735
+ const DECIMAL = /^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?$/;
14736
+ const text = raw.trim();
14737
+ if (!DECIMAL.test(text))
14738
+ return;
14739
+ const value = Number(text);
14740
+ return Number.isFinite(value) ? value : undefined;
14741
+ } catch {
14742
+ return;
14743
+ }
14744
+ }
14745
+ function motlyTag(texts) {
14746
+ return parseMotly(texts).tag;
14747
+ }
14748
+ function motlyParseErrors(texts) {
14749
+ return parseMotly(texts).errors;
14750
+ }
14751
+ function readAutorun(tag) {
14752
+ return tagText(tag, "autorun") !== "false";
14753
+ }
14754
+ function scalarTypeOf(tag) {
14755
+ try {
14756
+ return tag?.scalarType();
14757
+ } catch {
14758
+ return;
14759
+ }
14760
+ }
14761
+ function authoredDay(isoText) {
14762
+ const value = new Date(isoText);
14763
+ if (Number.isNaN(value.getTime()))
14764
+ return isoText.slice(0, 10);
14765
+ const atUtcMidnight = value.getUTCHours() === 0 && value.getUTCMinutes() === 0 && value.getUTCSeconds() === 0 && value.getUTCMilliseconds() === 0;
14766
+ const year = atUtcMidnight ? value.getUTCFullYear() : value.getFullYear();
14767
+ const month = atUtcMidnight ? value.getUTCMonth() : value.getMonth();
14768
+ const day = atUtcMidnight ? value.getUTCDate() : value.getDate();
14769
+ return `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
14770
+ }
14771
+ function readStartingGivens(tag, declaredType) {
14772
+ const entries = tag?.tag("givens");
14773
+ if (!entries)
14774
+ return;
14775
+ const collected = {};
14776
+ for (const [name, value] of entries.entries()) {
14777
+ const tag2 = value;
14778
+ const text = tagText(tag2);
14779
+ if (text === undefined)
14780
+ continue;
14781
+ const declared = declaredType(name);
14782
+ collected[name] = scalarTypeOf(tag2) === "date" && declared !== "timestamp" && declared !== "timestamptz" ? authoredDay(text) : declared?.startsWith("filter<") ? unwrapFilterLiteral(text) : text;
14783
+ }
14784
+ return Object.keys(collected).length > 0 ? collected : undefined;
14785
+ }
14786
+ 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";
14787
+ var init_motly = __esm(() => {
14788
+ BUILT_IN_PROTO_GETTER = Object.getOwnPropertyDescriptor(Object.prototype, "__proto__")?.get;
14789
+ ANNOTATION_TOO_LONG = `annotation exceeds ${MAX_ANNOTATION_CHARS} characters and was not parsed`;
14658
14790
  });
14659
14791
 
14660
- // src/service/source_extraction.ts
14792
+ // src/service/given.ts
14661
14793
  import {
14662
- isJoined as isJoined3,
14663
- isSourceDef as isSourceDef4,
14664
- modelDefToModelInfo
14794
+ isSourceDef as isSourceDef4
14665
14795
  } from "@malloydata/malloy";
14666
- function joinFieldNamesUnresolvableDeclaration(field, modelDef) {
14667
- const ids = [field.referenceID, field.sourceID].filter((id) => !!id);
14668
- if (ids.length === 0)
14796
+ function presentText(tag, ...path) {
14797
+ const raw = tagText(tag, ...path);
14798
+ return raw !== undefined && raw.trim() !== "" ? raw : undefined;
14799
+ }
14800
+ function readGivenControlSpec(annotationTexts2) {
14801
+ const spec = {};
14802
+ const tag = motlyTag(annotationTexts2);
14803
+ if (!tag)
14804
+ return spec;
14805
+ const label = presentText(tag, "label");
14806
+ if (label !== undefined)
14807
+ spec.label = label;
14808
+ const description = presentText(tag, "description");
14809
+ if (description !== undefined)
14810
+ spec.description = description;
14811
+ const control = tagText(tag, "control");
14812
+ if (control === "select" || control === "multiselect") {
14813
+ spec.control = control;
14814
+ }
14815
+ const rangeMin = tagNumeric(tag, "range_min");
14816
+ if (rangeMin !== undefined)
14817
+ spec.rangeMin = rangeMin;
14818
+ const rangeMax = tagNumeric(tag, "range_max");
14819
+ if (rangeMax !== undefined)
14820
+ spec.rangeMax = rangeMax;
14821
+ const suggest = tag.tag("suggest");
14822
+ if (suggest) {
14823
+ const parsed = {};
14824
+ const query = presentText(suggest, "query");
14825
+ if (query !== undefined)
14826
+ parsed.query = query;
14827
+ const source = presentText(suggest, "source");
14828
+ if (source !== undefined)
14829
+ parsed.source = source;
14830
+ const dimension = presentText(suggest, "dimension");
14831
+ if (dimension !== undefined)
14832
+ parsed.dimension = dimension;
14833
+ const runnable = parsed.query !== undefined || parsed.source !== undefined && parsed.dimension !== undefined;
14834
+ if (runnable)
14835
+ spec.suggest = parsed;
14836
+ }
14837
+ return spec;
14838
+ }
14839
+ function malloyGivenToApi(given) {
14840
+ const type = given.type;
14841
+ const renderedType = type.type === "filter expression" ? `filter<${type.filterType}>` : type.type === "array" && type.elementTypeDef?.type ? `${type.elementTypeDef.type}[]` : type.type;
14842
+ const allNotes = given.annotations.forRoute(undefined);
14843
+ return {
14844
+ name: given.name,
14845
+ type: renderedType,
14846
+ annotations: allNotes.filter((note) => !isReservedRoute(note.route)).map((note) => note.text),
14847
+ ...readGivenControlSpec(allNotes.map((note) => note.text)),
14848
+ default: given._internal?.defaultText
14849
+ };
14850
+ }
14851
+ function collectGivenRefs(value, into) {
14852
+ if (Array.isArray(value)) {
14853
+ for (const item of value)
14854
+ collectGivenRefs(item, into);
14855
+ return;
14856
+ }
14857
+ if (value === null || typeof value !== "object")
14858
+ return;
14859
+ const node = value;
14860
+ if (node.node === "given" && typeof node.refName === "string") {
14861
+ into.add(node.refName);
14862
+ }
14863
+ for (const child of Object.values(node))
14864
+ collectGivenRefs(child, into);
14865
+ }
14866
+ function gateGivenSource(sources, name) {
14867
+ const source = sources.find((candidate) => candidate.name === name);
14868
+ if (!source)
14869
+ return;
14870
+ return [...source.authorize ?? [], ...source.accessFilter ?? []];
14871
+ }
14872
+ function suggestGivenLookup(modelDef, gatesBySource, surfaced) {
14873
+ const registry = modelDef.givens ?? {};
14874
+ const bySource = new Map;
14875
+ const byQuery = new Map;
14876
+ for (const obj of Object.values(modelDef.contents)) {
14877
+ if (isSourceDef4(obj)) {
14878
+ const name = obj.as || obj.name;
14879
+ const refs = new Set;
14880
+ collectGivenRefs(obj.filterList, refs);
14881
+ for (const expr of gatesBySource(name) ?? []) {
14882
+ for (const given of referencedGivenNames(expr))
14883
+ refs.add(given);
14884
+ }
14885
+ bySource.set(name, Array.from(refs));
14886
+ } else if (obj.type === "query") {
14887
+ const query = obj;
14888
+ byQuery.set(query.as || query.name, {
14889
+ own: (query.givenUsage ?? []).map((usage) => registry[usage.id]?.name).filter((n) => n !== undefined),
14890
+ source: typeof query.structRef === "string" ? query.structRef : undefined
14891
+ });
14892
+ }
14893
+ }
14894
+ const narrow = (names) => Array.from(new Set(names)).filter((name) => surfaced === undefined || surfaced.has(name));
14895
+ return {
14896
+ forSource: (name) => {
14897
+ const found = bySource.get(name);
14898
+ return found && narrow(found);
14899
+ },
14900
+ forQuery: (name) => {
14901
+ const found = byQuery.get(name);
14902
+ if (!found)
14903
+ return;
14904
+ return narrow([
14905
+ ...found.own,
14906
+ ...found.source ? bySource.get(found.source) ?? [] : []
14907
+ ]);
14908
+ }
14909
+ };
14910
+ }
14911
+ function suggestGivenNames(suggest, lookup) {
14912
+ const names = suggest.query !== undefined ? lookup.forQuery(suggest.query) : suggest.source !== undefined ? lookup.forSource(suggest.source) : undefined;
14913
+ return names && names.length > 0 ? names : undefined;
14914
+ }
14915
+ function attachSuggestGivenNames(givens, lookup) {
14916
+ for (const given of givens ?? []) {
14917
+ if (!given.suggest)
14918
+ continue;
14919
+ const names = suggestGivenNames(given.suggest, lookup);
14920
+ if (names)
14921
+ given.suggest.givenNames = names;
14922
+ else
14923
+ delete given.suggest.givenNames;
14924
+ }
14925
+ }
14926
+ var init_given = __esm(() => {
14927
+ init_annotations();
14928
+ init_authorize();
14929
+ init_motly();
14930
+ });
14931
+
14932
+ // src/service/dashboard.ts
14933
+ import { isSourceDef as isSourceDef5 } from "@malloydata/malloy";
14934
+ function filterPublisherOwnedRenderLogs(logs, modelPath) {
14935
+ const couldBeATile = isDashboardModelPath(modelPath);
14936
+ return logs.filter((log) => {
14937
+ const match = UNKNOWN_RENDER_TAG.exec(log.message ?? "");
14938
+ if (!match)
14939
+ return true;
14940
+ const [, tag, field] = match;
14941
+ const root = tag.split(".")[0];
14942
+ if (PUBLISHER_OWNED_TAGS.includes(root))
14943
+ return false;
14944
+ return !(couldBeATile && RENDERER_CHILD_TAGS.includes(root) && field === "root");
14945
+ });
14946
+ }
14947
+ function isDashboardModelPath(modelPath) {
14948
+ if (!modelPath.endsWith(MODEL_FILE_SUFFIX2))
14669
14949
  return false;
14670
- for (const id of ids) {
14671
- const entry = modelDef.sourceRegistry?.[id]?.entry;
14672
- if (!entry)
14950
+ const segments = modelPath.split("/");
14951
+ return segments.length === 2 && segments[0] === DASHBOARDS_DIR;
14952
+ }
14953
+ function dashboardSlug(modelPath) {
14954
+ const basename = modelPath.slice(modelPath.lastIndexOf("/") + 1);
14955
+ return basename.slice(0, -MODEL_FILE_SUFFIX2.length);
14956
+ }
14957
+ function matchesDocumentedDashboardName(slug) {
14958
+ return /^[a-zA-Z0-9_-]+$/.test(slug);
14959
+ }
14960
+ function readArtifactTag(tag, declaredType) {
14961
+ const artifact = tag.tag("artifact");
14962
+ if (!artifact)
14963
+ return;
14964
+ const givens = readStartingGivens(artifact, declaredType);
14965
+ const autorun = readAutorun(artifact);
14966
+ return {
14967
+ title: tagText(artifact, "title"),
14968
+ tiles: artifact.array("tiles")?.map((tile) => tagText(tile)).filter((tile) => tile !== undefined),
14969
+ dashboardColumns: positiveInteger(tagNumeric(tag.tag("dashboard"), "columns")),
14970
+ givens,
14971
+ autorun
14972
+ };
14973
+ }
14974
+ function positiveInteger(value) {
14975
+ return value !== undefined && Number.isInteger(value) && value >= 1 ? value : undefined;
14976
+ }
14977
+ function normalizeTileExpression(tile) {
14978
+ return tile.split("->").map((part) => part.trim().replace(/\s+/g, " ")).join(" -> ").replace(/\s+/g, " ");
14979
+ }
14980
+ function readDashboardModelFacts(modelPath, modelDef, surfacedGivenNames, gatesBySource) {
14981
+ const registry = modelDef.givens ?? {};
14982
+ const surfaced = new Set(surfacedGivenNames);
14983
+ const givens = new Map;
14984
+ for (const given of Object.values(registry)) {
14985
+ if (!surfaced.has(given.name))
14986
+ continue;
14987
+ const type = given.type;
14988
+ const isFilter = type.type === "filter expression";
14989
+ givens.set(given.name, {
14990
+ name: given.name,
14991
+ type: isFilter ? `filter<${type.filterType}>` : type.type,
14992
+ default: given.defaultText,
14993
+ annotations: (given.annotations?.blockNotes ?? []).concat(given.annotations?.notes ?? []).map((note) => note.text)
14994
+ });
14995
+ }
14996
+ const isNamedQuery = (obj) => obj.type === "query";
14997
+ const queries = Object.values(modelDef.contents).filter(isNamedQuery).map((query) => ({
14998
+ name: query.as || query.name,
14999
+ annotations: [
15000
+ ...query.annotations?.blockNotes ?? [],
15001
+ ...query.annotations?.notes ?? []
15002
+ ].map((note) => note.text),
15003
+ givens: (query.givenUsage ?? []).map((usage) => registry[usage.id]?.name).filter((name) => name !== undefined),
15004
+ sourceName: typeof query.structRef === "string" ? query.structRef : undefined
15005
+ }));
15006
+ const viewGivens = new Map;
15007
+ const viewAnnotations = new Map;
15008
+ const sourceFields = new Map;
15009
+ const drills = [];
15010
+ for (const obj of Object.values(modelDef.contents)) {
15011
+ if (!isSourceDef5(obj))
15012
+ continue;
15013
+ const sourceName = obj.as || obj.name;
15014
+ const fieldNames = new Set;
15015
+ sourceFields.set(sourceName, fieldNames);
15016
+ const sourceGivens = new Set;
15017
+ collectGivenRefs(obj.filterList, sourceGivens);
15018
+ for (const field of obj.fields) {
15019
+ const fieldName = field.as || field.name;
15020
+ fieldNames.add(fieldName);
15021
+ if (field.type === "turtle") {
15022
+ const refs = new Set(sourceGivens);
15023
+ collectGivenRefs(field.pipeline, refs);
15024
+ const key = normalizeTileExpression(`${sourceName} -> ${fieldName}`);
15025
+ viewGivens.set(key, Array.from(refs));
15026
+ viewAnnotations.set(key, [
15027
+ ...field.annotations?.blockNotes ?? [],
15028
+ ...field.annotations?.notes ?? []
15029
+ ].map((note) => note.text));
15030
+ continue;
15031
+ }
15032
+ const drill = readDrillTag([
15033
+ ...field.annotations?.blockNotes ?? [],
15034
+ ...field.annotations?.notes ?? []
15035
+ ].map((note) => note.text));
15036
+ if (drill) {
15037
+ drills.push({ ...drill, source: sourceName, dimension: fieldName });
15038
+ }
15039
+ }
15040
+ }
15041
+ return {
15042
+ modelPath,
15043
+ modelAnnotations: ownModelNotes(modelDef),
15044
+ queries,
15045
+ givens,
15046
+ viewGivens,
15047
+ viewAnnotations,
15048
+ sourceFields,
15049
+ drills,
15050
+ suggestGivens: suggestGivenLookup(modelDef, (source) => gatesBySource?.get(source), surfaced)
15051
+ };
15052
+ }
15053
+ function readTileLayout(annotations) {
15054
+ const tag = motlyTag(annotations);
15055
+ if (!tag)
15056
+ return;
15057
+ const layout = {};
15058
+ const label = tagText(tag, "label");
15059
+ if (label !== undefined)
15060
+ layout.label = label;
15061
+ const subtitle = tagText(tag, "subtitle");
15062
+ if (subtitle !== undefined)
15063
+ layout.subtitle = subtitle;
15064
+ const colspan = positiveInteger(tagNumeric(tag, "colspan"));
15065
+ if (colspan !== undefined)
15066
+ layout.colspan = colspan;
15067
+ if (tag.has("break"))
15068
+ layout.break = true;
15069
+ if (tag.has("borderless"))
15070
+ layout.borderless = true;
15071
+ return Object.keys(layout).length > 0 ? layout : undefined;
15072
+ }
15073
+ function readDrillTag(annotations) {
15074
+ const drill = motlyTag(annotations)?.tag("drill");
15075
+ if (!drill)
15076
+ return;
15077
+ const to = tagTextArray(drill, "to") ?? [tagText(drill, "to")].filter(isString);
15078
+ const given = tagText(drill, "given");
15079
+ return given === undefined ? { to } : { to, given };
15080
+ }
15081
+ function isString(value) {
15082
+ return value !== undefined;
15083
+ }
15084
+ function tagTextArray(tag, ...path) {
15085
+ const items = tag?.array(...path);
15086
+ return items?.map((item) => tagText(item)).filter(isString);
15087
+ }
15088
+ function resolveTileGivens(tile, facts) {
15089
+ return referencedTileGivens(tile, facts)?.filter((name) => facts.givens.has(name));
15090
+ }
15091
+ function resolveTileLayout(tile, facts) {
15092
+ const annotations = tileAnnotations(tile, facts);
15093
+ return annotations && readTileLayout(annotations);
15094
+ }
15095
+ function tileAnnotations(tile, facts) {
15096
+ const normalized = normalizeTileExpression(tile);
15097
+ return facts.viewAnnotations.get(normalized) ?? facts.queries.find((query) => query.name === normalized)?.annotations;
15098
+ }
15099
+ function referencedTileGivens(tile, facts) {
15100
+ const normalized = normalizeTileExpression(tile);
15101
+ return facts.viewGivens.get(normalized) ?? facts.queries.find((query) => query.name === normalized)?.givens;
15102
+ }
15103
+ function buildGivenSpecs(names, facts) {
15104
+ const declarations = facts.givens;
15105
+ const specs = [];
15106
+ for (const name of new Set(names)) {
15107
+ const declaration = declarations.get(name);
15108
+ if (!declaration)
14673
15109
  continue;
14674
- const declared = entry.type === "source_registry_reference" ? modelDef.contents[entry.name] : entry;
14675
- if (declared && isSourceDef4(declared))
14676
- return false;
15110
+ specs.push(givenSpec(declaration, facts));
14677
15111
  }
14678
- return true;
15112
+ return specs;
14679
15113
  }
14680
- function isEarlierPosition(a, b) {
14681
- return a.line < b.line || a.line === b.line && a.character < b.character;
15114
+ function givenSpec(declaration, facts) {
15115
+ const control = readGivenControlSpec(declaration.annotations);
15116
+ if (control.suggest) {
15117
+ const names = suggestGivenNames(control.suggest, facts.suggestGivens);
15118
+ if (names)
15119
+ control.suggest.givenNames = names;
15120
+ }
15121
+ return {
15122
+ name: declaration.name,
15123
+ type: declaration.type,
15124
+ default: declaration.type.startsWith("filter<") && declaration.default !== undefined ? unwrapFilterLiteral(declaration.default) : declaration.default,
15125
+ annotations: declaration.annotations.filter((text) => /^##?\(/.test(text)),
15126
+ ...control
15127
+ };
14682
15128
  }
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);
15129
+ function factsCarryArtifactTag(facts) {
15130
+ if (motlyTag(facts.modelAnnotations)?.tag("artifact"))
15131
+ return true;
15132
+ return facts.queries.some((query) => Boolean(motlyTag(query.annotations)?.tag("artifact")));
15133
+ }
15134
+ function buildDashboardManifest(facts) {
15135
+ const name = dashboardSlug(facts.modelPath);
15136
+ const base = {
15137
+ name,
15138
+ entryFile: facts.modelPath
15139
+ };
15140
+ const modelTag = motlyTag(facts.modelAnnotations);
15141
+ const declaredType = (name2) => facts.givens.get(name2)?.type;
15142
+ const composite = modelTag ? readArtifactTag(modelTag, declaredType) : undefined;
15143
+ if (composite?.tiles?.length) {
15144
+ const tiles = composite.tiles.map((query) => {
15145
+ const givenNames = resolveTileGivens(query, facts);
15146
+ const layout = resolveTileLayout(query, facts);
15147
+ return {
15148
+ query,
15149
+ ...givenNames ? { givenNames } : {},
15150
+ ...layout
15151
+ };
15152
+ });
15153
+ const doc = docCommentTitleAndDescription(facts.modelAnnotations, composite.title);
15154
+ return {
15155
+ ...base,
15156
+ title: doc.title ?? name,
15157
+ description: doc.description,
15158
+ tiles,
15159
+ dashboardColumns: composite.dashboardColumns,
15160
+ startingGivens: composite.givens,
15161
+ autorun: composite.autorun,
15162
+ givens: buildGivenSpecs(tiles.some((tile) => tile.givenNames === undefined) ? Array.from(facts.givens.keys()) : tiles.flatMap((tile) => tile.givenNames ?? []), facts)
15163
+ };
15164
+ }
15165
+ for (const query of facts.queries) {
15166
+ const tag = motlyTag(query.annotations);
15167
+ const artifact = tag ? readArtifactTag(tag, declaredType) : undefined;
15168
+ if (!artifact)
15169
+ continue;
15170
+ const doc = docCommentTitleAndDescription(query.annotations, artifact.title);
15171
+ return {
15172
+ ...base,
15173
+ title: doc.title ?? name,
15174
+ description: doc.description,
15175
+ query: query.name,
15176
+ dashboardColumns: artifact.dashboardColumns,
15177
+ startingGivens: artifact.givens,
15178
+ autorun: artifact.autorun,
15179
+ givens: buildGivenSpecs(query.givens, facts)
15180
+ };
14690
15181
  }
15182
+ return;
14691
15183
  }
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");
15184
+ function plainTileParts(tile) {
15185
+ const piped = /^(\w+)\s*->\s*(\w+)$/.exec(tile.trim());
15186
+ if (piped)
15187
+ return { source: piped[1], view: piped[2] };
15188
+ const bare = /^\w+$/.exec(tile.trim());
15189
+ return bare ? { name: tile.trim() } : undefined;
14696
15190
  }
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))
15191
+ function lintDashboard(facts, manifest) {
15192
+ const findings = [];
15193
+ const slug = manifest.name;
15194
+ const add = (message, severity = "warn") => findings.push({ subject: slug, message, severity });
15195
+ const ownAnnotations = manifest.tiles ? facts.modelAnnotations : facts.queries.find((query) => query.name === manifest.query)?.annotations ?? [];
15196
+ const ownTag = motlyTag(ownAnnotations);
15197
+ const artifactTag = ownTag?.tag("artifact");
15198
+ const lintTargets = new Set([...facts.modelAnnotations, ...ownAnnotations]);
15199
+ for (const message of new Set(motlyParseErrors([...lintTargets]))) {
15200
+ add(`Annotation ${describeParseFailure(message)}, so that line had no ` + `effect on this dashboard.`);
15201
+ }
15202
+ if (!manifest.tiles) {
15203
+ const modelArtifact = motlyTag(facts.modelAnnotations)?.tag("artifact");
15204
+ if (modelArtifact) {
15205
+ add(`${describeTilelessModelArtifact(modelArtifact)}, so it builds ` + `nothing and this dashboard is the query's own '# artifact' ` + `instead. ${NAME_THE_TILES}`);
15206
+ }
15207
+ }
15208
+ const knownProperties = manifest.tiles ? COMPOSITE_ARTIFACT_PROPERTIES : QUERY_ARTIFACT_PROPERTIES;
15209
+ for (const property of Object.keys(artifactTag?.dict ?? {})) {
15210
+ if (knownProperties.includes(property))
14707
15211
  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)));
15212
+ 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
15213
  }
14721
- for (const value of Object.values(modelDef.sourceRegistry ?? {})) {
14722
- const entry = value.entry;
14723
- if (entry.type === "source_registry_reference")
15214
+ for (const entry of artifactTag?.array("tiles") ?? []) {
15215
+ const carried = Object.keys(entry.dict ?? {});
15216
+ if (carried.length === 0)
14724
15217
  continue;
14725
- if (!isSourceDef4(entry))
15218
+ const named = carried.map((property) => `\`${property}\``).join(", ");
15219
+ 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\`).`);
15220
+ }
15221
+ for (const { query: tile } of manifest.tiles ?? []) {
15222
+ const tag = motlyTag(tileAnnotations(tile, facts) ?? []);
15223
+ if (!tag?.has("colspan"))
15224
+ continue;
15225
+ const colspan = positiveInteger(tagNumeric(tag, "colspan"));
15226
+ if (colspan === undefined) {
15227
+ const raw = tagText(tag, "colspan");
15228
+ 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
15229
  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
15230
  }
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
- });
15231
+ const columns = manifest.dashboardColumns;
15232
+ if (columns === undefined) {
15233
+ 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.`);
15234
+ } else if (colspan > columns) {
15235
+ 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
15236
  }
14757
15237
  }
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));
15238
+ const columnsTag = ownTag?.tag("dashboard");
15239
+ if (columnsTag?.has("columns")) {
15240
+ if (positiveInteger(tagNumeric(columnsTag, "columns")) === undefined) {
15241
+ const raw = tagText(columnsTag, "columns");
15242
+ 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
15243
  }
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);
15244
+ }
15245
+ for (const { query: tile } of manifest.tiles ?? []) {
15246
+ const parts = plainTileParts(tile);
15247
+ if (!parts)
15248
+ continue;
15249
+ const resolved = referencedTileGivens(tile, facts) !== undefined || "name" in parts && facts.queries.some((query) => query.name === parts.name);
15250
+ if (!resolved) {
15251
+ 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`;
15252
+ add(`tile "${tile}" does not resolve: ${detail}.`, "error");
14814
15253
  }
14815
- if (groupsForSource.length > 0) {
14816
- authorizeMap.set(sourceName, groupsForSource);
15254
+ const unbindable = (referencedTileGivens(tile, facts) ?? []).filter((name) => !facts.givens.has(name));
15255
+ for (const name of unbindable) {
15256
+ 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
15257
  }
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
- });
15258
+ }
15259
+ if (manifest.query !== undefined) {
15260
+ const query = facts.queries.find((q) => q.name === manifest.query);
15261
+ for (const name of query?.givens ?? []) {
15262
+ if (facts.givens.has(name))
14844
15263
  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
- });
15264
+ 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
15265
  }
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;
15266
+ }
15267
+ for (const name of Object.keys(manifest.startingGivens ?? {})) {
15268
+ if (facts.givens.has(name))
15269
+ continue;
15270
+ 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}.`);
15271
+ }
15272
+ for (const spec of manifest.givens) {
15273
+ const suggest = spec.suggest;
15274
+ if (!suggest) {
15275
+ const declared = motlyTag(facts.givens.get(spec.name)?.annotations ?? [])?.tag("suggest");
15276
+ if (declared) {
15277
+ const has = (key) => tagText(declared, key) !== undefined;
15278
+ 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=`;
15279
+ 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
15280
  }
14917
15281
  continue;
14918
15282
  }
14919
- if ((typeof value === "object" || typeof value === "function") && value !== null && !targets.includes(value)) {
14920
- targets.push(value);
15283
+ if (suggest.query !== undefined) {
15284
+ const named = facts.queries.find((query) => query.name === suggest.query);
15285
+ if (!named) {
15286
+ add(`given "${spec.name}" suggests options from query ` + `"${suggest.query}", which this file does not define.`, "error");
15287
+ } else if (named.sourceName !== undefined && !facts.sourceFields.has(named.sourceName)) {
15288
+ 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");
15289
+ }
15290
+ }
15291
+ if (suggest.source !== undefined) {
15292
+ const fields = facts.sourceFields.get(suggest.source);
15293
+ if (!fields) {
15294
+ add(`given "${spec.name}" suggests options from source ` + `"${suggest.source}", which this file does not define.`, "error");
15295
+ } else if (suggest.dimension !== undefined && !fields.has(suggest.dimension)) {
15296
+ add(`given "${spec.name}" suggests options from ` + `"${suggest.source} -> ${suggest.dimension}", but that ` + `source has no field "${suggest.dimension}".`, "error");
15297
+ }
14921
15298
  }
14922
15299
  }
14923
- return targets;
15300
+ return findings;
14924
15301
  }
14925
- function parseGuarded(texts) {
14926
- const targets = pollutionTargets();
14927
- if (targets === undefined) {
14928
- return { tag: undefined, messages: [UNSAFE_TO_PARSE] };
15302
+ function describeParseFailure(message) {
15303
+ if (message === UNSAFE_TO_PARSE) {
15304
+ return `was refused rather than parsed (${message}), which can be caused by something outside this file`;
14929
15305
  }
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] };
15306
+ if (message === ENV_REFERENCE_DROPPED) {
15307
+ return `was dropped rather than parsed (${message}), so nothing on that line took effect`;
14958
15308
  }
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] };
15309
+ if (message === ANNOTATION_TOO_LONG) {
15310
+ return `was refused rather than parsed (${message})`;
14970
15311
  }
15312
+ return `does not parse (${message})`;
14971
15313
  }
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) };
15314
+ function lintUndiscoveredDashboard(facts) {
15315
+ const subject = dashboardSlug(facts.modelPath);
15316
+ const messages = [
15317
+ ...motlyParseErrors(facts.modelAnnotations),
15318
+ ...facts.queries.flatMap((query) => motlyParseErrors(query.annotations))
15319
+ ].map((message) => `Tag ${describeParseFailure(message)}, so the whole tag is discarded ` + `and this file is treated as a shared include rather than a dashboard.`);
15320
+ const findings = Array.from(new Set(messages), (message) => ({ subject, message, severity: "error" }));
15321
+ if (findings.length > 0)
15322
+ return findings;
15323
+ const modelArtifact = motlyTag(facts.modelAnnotations)?.tag("artifact");
15324
+ if (modelArtifact) {
15325
+ findings.push({
15326
+ subject,
15327
+ message: `${describeTilelessModelArtifact(modelArtifact)}, so this file ` + `produces no dashboard. ${NAME_THE_TILES}`,
15328
+ severity: "error"
15329
+ });
14996
15330
  }
14997
- const { title, body } = splitDocComment(texts);
14998
- return { title, description: body };
15331
+ findings.push(...lintArtifactOnView(facts));
15332
+ return findings;
14999
15333
  }
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;
15334
+ function lintArtifactOnView(facts) {
15335
+ const subject = dashboardSlug(facts.modelPath);
15336
+ const findings = [];
15337
+ for (const [view, annotations] of facts.viewAnnotations) {
15338
+ if (!motlyTag(annotations)?.tag("artifact"))
15011
15339
  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;
15340
+ const viewName = view.split("->").at(-1)?.trim() ?? view;
15341
+ findings.push({
15342
+ subject,
15343
+ 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}.`,
15344
+ severity: "error"
15345
+ });
15346
+ }
15347
+ return findings;
15348
+ }
15349
+ function describeTilelessModelArtifact(modelArtifact) {
15350
+ return modelArtifact.array("tiles") === undefined ? `Model-level '## artifact' declares no tiles=` : `Model-level '## artifact' declares an empty tiles=`;
15351
+ }
15352
+ function lintGivenTags(facts) {
15353
+ const findings = new Map;
15354
+ for (const file of facts) {
15355
+ for (const [name, declaration] of file.givens) {
15356
+ for (const message of motlyParseErrors(declaration.annotations)) {
15357
+ findings.set(`${name}|${message}`, {
15358
+ subject: name,
15359
+ 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.`,
15360
+ severity: "error"
15361
+ });
15020
15362
  }
15021
15363
  }
15022
- out += char;
15023
- i += 1;
15024
15364
  }
15025
- return out;
15365
+ return Array.from(findings.values());
15026
15366
  }
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;
15367
+ function lintDrillTargets(facts, knownSlugs) {
15368
+ const findings = new Map;
15369
+ for (const file of facts) {
15370
+ for (const drill of file.drills) {
15371
+ const where = `${drill.source}.${drill.dimension}`;
15372
+ if (drill.to.length === 0) {
15373
+ findings.set(where, {
15374
+ subject: where,
15375
+ message: `# drill on ${where} names no destination (to=…).`,
15376
+ severity: "error"
15377
+ });
15034
15378
  continue;
15035
15379
  }
15036
- if (text[i] === "`")
15037
- return i + 1;
15038
- if (text[i] === `
15039
- `)
15040
- return text.length;
15041
- i += 1;
15380
+ for (const destination of drill.to) {
15381
+ if (destination === "self" || knownSlugs.has(destination))
15382
+ continue;
15383
+ findings.set(`${where}|${destination}`, {
15384
+ subject: where,
15385
+ message: `# drill on ${where} targets "${destination}", which is not ` + `a dashboard in this package.`,
15386
+ severity: "error"
15387
+ });
15388
+ }
15042
15389
  }
15043
- return text.length;
15044
15390
  }
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;
15391
+ return Array.from(findings.values());
15392
+ }
15393
+ function drillGivenName(drill) {
15394
+ return drill.given ?? drill.dimension;
15395
+ }
15396
+ function lintSelfDrills(facts) {
15397
+ const surfaced = new Set;
15398
+ for (const file of facts) {
15399
+ for (const name of file.givens.keys())
15400
+ surfaced.add(name.toLowerCase());
15401
+ }
15402
+ const findings = new Map;
15403
+ for (const file of facts) {
15404
+ for (const drill of file.drills) {
15405
+ if (!drill.to.includes("self"))
15406
+ continue;
15407
+ const given = drillGivenName(drill);
15408
+ if (surfaced.has(given.toLowerCase()))
15409
+ continue;
15410
+ const where = `${drill.source}.${drill.dimension}`;
15411
+ findings.set(where, {
15412
+ subject: where,
15413
+ 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=.`,
15414
+ severity: "error"
15415
+ });
15056
15416
  }
15057
15417
  }
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;
15418
+ return Array.from(findings.values());
15419
+ }
15420
+ 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", …].`;
15421
+ var init_dashboard = __esm(() => {
15422
+ init_annotations();
15423
+ init_given();
15424
+ init_motly();
15425
+ COMPONENT_FILE_SUFFIXES = [".jsx", ".tsx"];
15426
+ PUBLISHER_OWNED_TAGS = ["artifact", "drill"];
15427
+ RENDERER_CHILD_TAGS = ["colspan", "break", "subtitle", "borderless"];
15428
+ UNKNOWN_RENDER_TAG = /^Unknown render tag '([^']+)' on field '([^']*)'/;
15429
+ COMPOSITE_ARTIFACT_PROPERTIES = [
15430
+ "title",
15431
+ "tiles",
15432
+ "givens",
15433
+ "autorun"
15434
+ ];
15435
+ QUERY_ARTIFACT_PROPERTIES = COMPOSITE_ARTIFACT_PROPERTIES.filter((property) => property !== "tiles");
15436
+ ARTIFACT_PROPERTY_REPLACEMENTS = {
15437
+ dashboard_columns: "Write the grid width as `# dashboard { columns=N }` beside the artifact tag.",
15438
+ 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 }`."
15439
+ };
15440
+ });
15441
+
15442
+ // src/service/filter.ts
15443
+ function parseFilterAnnotation(annotation) {
15444
+ const trimmed = annotation.trim();
15445
+ if (!trimmed.startsWith(ANNOTATION_PREFIX)) {
15446
+ return null;
15447
+ }
15448
+ const body = trimmed.slice(ANNOTATION_PREFIX.length).trim();
15449
+ const tokens = tokenize(body);
15450
+ let name;
15451
+ let dimension;
15452
+ let type;
15453
+ let implicit = false;
15454
+ let required = false;
15455
+ for (const token of tokens) {
15456
+ if (token.includes("=")) {
15457
+ const eqIndex = token.indexOf("=");
15458
+ const key = token.slice(0, eqIndex).toLowerCase();
15459
+ const value = token.slice(eqIndex + 1);
15460
+ switch (key) {
15461
+ case "name":
15462
+ name = value;
15463
+ break;
15464
+ case "dimension":
15465
+ dimension = value;
15466
+ break;
15467
+ case "type":
15468
+ if (!VALID_FILTER_TYPES.has(value)) {
15469
+ throw new Error(`Invalid filter type "${value}". Must be one of: ${[...VALID_FILTER_TYPES].join(", ")}`);
15470
+ }
15471
+ type = value;
15472
+ break;
15473
+ default:
15474
+ throw new Error(`Unknown filter parameter "${key}"`);
15070
15475
  }
15071
- return text.length;
15072
- }
15073
- let i = start + 1;
15074
- while (i < text.length) {
15075
- if (text[i] === "\\") {
15076
- i += 2;
15077
- continue;
15476
+ } else {
15477
+ const flag = token.toLowerCase();
15478
+ if (flag === "implicit") {
15479
+ implicit = true;
15480
+ } else if (flag === "required") {
15481
+ required = true;
15482
+ } else {
15483
+ throw new Error(`Unknown filter flag "${token}"`);
15078
15484
  }
15079
- if (text[i] === char)
15080
- return i + 1;
15081
- if (text[i] === `
15082
- `)
15083
- return i;
15084
- i += 1;
15085
15485
  }
15086
- return text.length;
15087
15486
  }
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] };
15487
+ if (!dimension) {
15488
+ throw new Error("filter annotation missing required 'dimension' parameter");
15489
+ }
15490
+ if (!type) {
15491
+ throw new Error("filter annotation missing required 'type' parameter");
15492
+ }
15493
+ return {
15494
+ name: name ?? dimension,
15495
+ dimension,
15496
+ type,
15497
+ implicit,
15498
+ required
15499
+ };
15112
15500
  }
15113
- function tagText(tag, ...path) {
15114
- try {
15115
- return tag?.text(...path);
15116
- } catch {
15117
- return;
15501
+ function parseFilters(annotations) {
15502
+ const byName = new Map;
15503
+ for (const annotation of annotations) {
15504
+ const parsed = parseFilterAnnotation(annotation);
15505
+ if (parsed) {
15506
+ byName.set(parsed.name, parsed);
15507
+ }
15118
15508
  }
15509
+ return [...byName.values()];
15119
15510
  }
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
- }
15511
+ function escapeMalloyString(value) {
15512
+ return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
15134
15513
  }
15135
- function motlyTag(texts) {
15136
- return parseMotly(texts).tag;
15514
+ function isBooleanLiteral(v) {
15515
+ const lower = v.toLowerCase();
15516
+ return lower === "true" || lower === "false";
15137
15517
  }
15138
- function motlyParseErrors(texts) {
15139
- return parseMotly(texts).errors;
15518
+ function isDateLiteral(v) {
15519
+ return ISO_DATE_RE.test(v) || ISO_TIMESTAMP_RE.test(v);
15140
15520
  }
15141
- function readAutorun(tag) {
15142
- return tagText(tag, "autorun") !== "false";
15521
+ function malloyLiteral(v) {
15522
+ if (isBooleanLiteral(v)) {
15523
+ return v.toLowerCase();
15524
+ }
15525
+ if (isDateLiteral(v)) {
15526
+ return `@${v.slice(0, 10)}`;
15527
+ }
15528
+ return `'${escapeMalloyString(v)}'`;
15143
15529
  }
15144
- function scalarTypeOf(tag) {
15145
- try {
15146
- return tag?.scalarType();
15147
- } catch {
15148
- return;
15530
+ function buildPredicate(filter, value) {
15531
+ const dim = `\`${filter.dimension}\``;
15532
+ switch (filter.type) {
15533
+ case "equal": {
15534
+ const v = Array.isArray(value) ? value[0] : value;
15535
+ return `${dim} = ${malloyLiteral(v)}`;
15536
+ }
15537
+ case "in": {
15538
+ const values = Array.isArray(value) ? value : [value];
15539
+ if (values.length === 1) {
15540
+ return `${dim} = ${malloyLiteral(values[0])}`;
15541
+ }
15542
+ const conditions = values.map((v) => `${dim} = ${malloyLiteral(v)}`);
15543
+ return `(${conditions.join(" or ")})`;
15544
+ }
15545
+ case "like": {
15546
+ const v = Array.isArray(value) ? value[0] : value;
15547
+ const escaped = escapeMalloyString(v.toLowerCase());
15548
+ const pattern = escaped.startsWith("%") || escaped.endsWith("%") ? escaped : `%${escaped}%`;
15549
+ return `lower(${dim}) ~ '${pattern}'`;
15550
+ }
15551
+ case "greater_than": {
15552
+ const v = Array.isArray(value) ? value[0] : value;
15553
+ return `${dim} > ${malloyLiteral(v)}`;
15554
+ }
15555
+ case "less_than": {
15556
+ const v = Array.isArray(value) ? value[0] : value;
15557
+ return `${dim} < ${malloyLiteral(v)}`;
15558
+ }
15149
15559
  }
15150
15560
  }
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")}`;
15561
+ function filterHasValue(filter, params) {
15562
+ const value = params[filter.name];
15563
+ return value !== undefined && value !== null && (Array.isArray(value) ? value.length > 0 : value !== "");
15160
15564
  }
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)
15565
+ function buildFilterClause(filters, params) {
15566
+ const predicates = [];
15567
+ for (const filter of filters) {
15568
+ if (!filterHasValue(filter, params)) {
15569
+ if (filter.required) {
15570
+ throw new FilterValidationError(`Required filter "${filter.name}" (dimension: ${filter.dimension}) was not provided`);
15571
+ }
15170
15572
  continue;
15171
- const declared = declaredType(name);
15172
- collected[name] = scalarTypeOf(tag2) === "date" && declared !== "timestamp" && declared !== "timestamptz" ? authoredDay(text) : declared?.startsWith("filter<") ? unwrapFilterLiteral(text) : text;
15573
+ }
15574
+ predicates.push(buildPredicate(filter, params[filter.name]));
15173
15575
  }
15174
- return Object.keys(collected).length > 0 ? collected : undefined;
15576
+ if (predicates.length === 0) {
15577
+ return "";
15578
+ }
15579
+ return predicates.join(" and ");
15175
15580
  }
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;
15581
+ function injectFilterRefinement(query, filterClause) {
15582
+ if (!filterClause) {
15583
+ return query;
15584
+ }
15585
+ return `${query.trimEnd()}
15586
+ + {where: ${filterClause}}`;
15189
15587
  }
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;
15588
+ function tokenize(input) {
15589
+ const tokens = [];
15590
+ let current = "";
15591
+ let inQuote = false;
15592
+ let quoteChar = "";
15593
+ for (const ch of input) {
15594
+ if (inQuote) {
15595
+ if (ch === quoteChar) {
15596
+ inQuote = false;
15597
+ } else {
15598
+ current += ch;
15599
+ }
15600
+ } else if (ch === '"' || ch === "'") {
15601
+ inQuote = true;
15602
+ quoteChar = ch;
15603
+ } else if (ch === " " || ch === "\t") {
15604
+ if (current) {
15605
+ tokens.push(current);
15606
+ current = "";
15607
+ }
15608
+ } else {
15609
+ current += ch;
15610
+ }
15204
15611
  }
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;
15612
+ if (current) {
15613
+ tokens.push(current);
15226
15614
  }
15227
- return spec;
15615
+ return tokens;
15228
15616
  }
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
15617
+ var VALID_FILTER_TYPES, ANNOTATION_PREFIX = "#(filter)", ISO_DATE_RE, ISO_TIMESTAMP_RE, FilterValidationError;
15618
+ var init_filter = __esm(() => {
15619
+ VALID_FILTER_TYPES = new Set([
15620
+ "equal",
15621
+ "in",
15622
+ "like",
15623
+ "greater_than",
15624
+ "less_than"
15625
+ ]);
15626
+ ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
15627
+ ISO_TIMESTAMP_RE = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}/;
15628
+ FilterValidationError = class FilterValidationError extends Error {
15629
+ constructor(message) {
15630
+ super(message);
15631
+ this.name = "FilterValidationError";
15632
+ }
15239
15633
  };
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);
15634
+ });
15635
+
15636
+ // src/service/source_extraction.ts
15637
+ import {
15638
+ isJoined as isJoined3,
15639
+ isSourceDef as isSourceDef6,
15640
+ modelDefToModelInfo
15641
+ } from "@malloydata/malloy";
15642
+ function joinFieldNamesUnresolvableDeclaration(field, modelDef) {
15643
+ const ids = [field.referenceID, field.sourceID].filter((id) => !!id);
15644
+ if (ids.length === 0)
15645
+ return false;
15646
+ for (const id of ids) {
15647
+ const entry = modelDef.sourceRegistry?.[id]?.entry;
15648
+ if (!entry)
15649
+ continue;
15650
+ const declared = entry.type === "source_registry_reference" ? modelDef.contents[entry.name] : entry;
15651
+ if (declared && isSourceDef6(declared))
15652
+ return false;
15252
15653
  }
15253
- for (const child of Object.values(node))
15254
- collectGivenRefs(child, into);
15654
+ return true;
15255
15655
  }
15256
- function gateGivenSource(sources, name) {
15257
- const source = sources.find((candidate) => candidate.name === name);
15258
- if (!source)
15656
+ function isEarlierPosition(a, b) {
15657
+ return a.line < b.line || a.line === b.line && a.character < b.character;
15658
+ }
15659
+ function considerNoteOwner(declaredBy, note, candidate) {
15660
+ const location = candidate.location;
15661
+ if (!location || location.url !== note.at.url)
15259
15662
  return;
15260
- return [...source.authorize ?? [], ...source.accessFilter ?? []];
15663
+ const currentLocation = declaredBy.get(note)?.location;
15664
+ if (!currentLocation || isEarlierPosition(location.range.start, currentLocation.range.start)) {
15665
+ declaredBy.set(note, candidate);
15666
+ }
15261
15667
  }
15262
- function suggestGivenLookup(modelDef, gatesBySource, surfaced) {
15263
- const registry = modelDef.givens ?? {};
15264
- const bySource = new Map;
15265
- const byQuery = new Map;
15668
+ function collectSourceInfos(modelDef) {
15669
+ const names = Object.keys(modelDef.contents).filter((name) => isSourceDef6(modelDef.contents[name]));
15670
+ const exports = [...new Set([...modelDef.exports, ...names])];
15671
+ return modelDefToModelInfo({ ...modelDef, exports }).entries.filter((entry) => entry.kind === "source");
15672
+ }
15673
+ function extractSourcesFromModelDef(modelDef, givens, onParseError) {
15674
+ const filterMap = new Map;
15675
+ const authorizeMap = new Map;
15676
+ const misplacedAuthorize = [];
15677
+ const gatedSourceOwnAuthorizeNotes = new Set;
15678
+ const authorizeNoteDeclaredBy = new Map;
15679
+ const nearMissAuthorize = [];
15680
+ const sweptStructs = [];
15266
15681
  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);
15682
+ if (!isSourceDef6(obj))
15683
+ continue;
15684
+ const struct = obj;
15685
+ sweptStructs.push(obj);
15686
+ for (const note of ownLevelNotes(struct.annotations)) {
15687
+ if (containsAuthorizeAnnotationTag([note.text])) {
15688
+ gatedSourceOwnAuthorizeNotes.add(note);
15689
+ considerNoteOwner(authorizeNoteDeclaredBy, note, struct);
15274
15690
  }
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
15691
+ }
15692
+ nearMissAuthorize.push(...collectAuthorizeNearMissesAllRoutes([
15693
+ ...ownLevelNotes(struct.annotations),
15694
+ ...struct.fields.flatMap((field) => ownLevelNotes(field.annotations))
15695
+ ].map((note) => note.text)));
15696
+ }
15697
+ for (const value of Object.values(modelDef.sourceRegistry ?? {})) {
15698
+ const entry = value.entry;
15699
+ if (entry.type === "source_registry_reference")
15700
+ continue;
15701
+ if (!isSourceDef6(entry))
15702
+ continue;
15703
+ sweptStructs.push(entry);
15704
+ for (const note of ownLevelNotes(entry.annotations)) {
15705
+ if (containsAuthorizeAnnotationTag([note.text])) {
15706
+ gatedSourceOwnAuthorizeNotes.add(note);
15707
+ considerNoteOwner(authorizeNoteDeclaredBy, note, entry);
15708
+ }
15709
+ }
15710
+ nearMissAuthorize.push(...collectAuthorizeNearMissesAllRoutes(ownLevelNotes(entry.annotations).map((note) => note.text)));
15711
+ }
15712
+ for (const struct of derivedStructsReachable(sweptStructs, modelDef)) {
15713
+ nearMissAuthorize.push(...collectAuthorizeNearMissesAllRoutes([
15714
+ ...ownLevelNotes(struct.annotations),
15715
+ ...struct.fields.flatMap((field) => ownLevelNotes(field.annotations))
15716
+ ].map((note) => note.text)));
15717
+ }
15718
+ {
15719
+ const folded = modelAnnotations(modelDef);
15720
+ nearMissAuthorize.push(...collectAuthorizeNearMissesAllRoutes([...folded.notes ?? [], ...folded.blockNotes ?? []].map((note) => note.text)));
15721
+ }
15722
+ assertNoAuthorizeNearMisses(nearMissAuthorize);
15723
+ const authorizeOwnNotes = new Map;
15724
+ const attributedAuthorizeOwnNotes = new Map;
15725
+ {
15726
+ const fileNoteTexts = ownLevelNoteTexts(modelAnnotations(modelDef));
15727
+ if (containsAuthorizeAnnotationTag(fileNoteTexts)) {
15728
+ misplacedAuthorize.push({
15729
+ kind: "file",
15730
+ route: fileNoteTexts.map((text) => authorizeAnnotationRoute(text)).find((r) => r !== undefined) ?? ACCESS_FILTER_ROUTE
15281
15731
  });
15282
15732
  }
15283
15733
  }
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
- ]);
15734
+ const sources = Object.values(modelDef.contents).filter((obj) => isSourceDef6(obj)).map((sourceObj) => {
15735
+ const struct = sourceObj;
15736
+ const sourceName = struct.as || struct.name;
15737
+ const annotations = annotationTexts(struct.annotations);
15738
+ const collected = [];
15739
+ let cur = struct.annotations;
15740
+ while (cur) {
15741
+ if (cur.blockNotes) {
15742
+ collected.push(cur.blockNotes.map((note) => note.text));
15743
+ }
15744
+ cur = cur.inherits;
15745
+ }
15746
+ const allAnnotations = collected.reverse().flat();
15747
+ let filters;
15748
+ if (allAnnotations.length > 0) {
15749
+ try {
15750
+ const parsed = parseFilters(allAnnotations);
15751
+ if (parsed.length > 0) {
15752
+ filterMap.set(sourceName, parsed);
15753
+ const fields = struct.fields;
15754
+ filters = parsed.map((f) => {
15755
+ const field = fields.find((fd) => (fd.as || fd.name) === f.dimension);
15756
+ return {
15757
+ name: f.name,
15758
+ dimension: f.dimension,
15759
+ type: f.type,
15760
+ implicit: f.implicit,
15761
+ required: f.required,
15762
+ dimensionType: field?.type
15763
+ };
15764
+ });
15765
+ }
15766
+ } catch (err) {
15767
+ onParseError?.(sourceName, err);
15768
+ }
15769
+ }
15770
+ const ownNotes = ownLevelNoteTexts(struct.annotations);
15771
+ const ownAuthorizeNotes = ownLevelNotes(struct.annotations).filter((note) => containsAuthorizeAnnotationTag([note.text]));
15772
+ const ownNotesByRoute = new Map;
15773
+ const attributedOwnNotesByRoute = new Map;
15774
+ for (const route of CANONICAL_AUTHORIZE_ROUTES) {
15775
+ const notesForRoute = ownAuthorizeNotes.filter((note) => authorizeAnnotationRoute(note.text) === route);
15776
+ ownNotesByRoute.set(route, notesForRoute);
15777
+ attributedOwnNotesByRoute.set(route, notesForRoute.filter((note) => authorizeNoteDeclaredBy.get(note) === struct));
15778
+ }
15779
+ authorizeOwnNotes.set(sourceName, ownNotesByRoute);
15780
+ attributedAuthorizeOwnNotes.set(sourceName, attributedOwnNotesByRoute);
15781
+ const groupsForSource = [];
15782
+ const routeGroupsByRoute = new Map;
15783
+ for (const route of CANONICAL_AUTHORIZE_ROUTES) {
15784
+ const ownExprsForRoute = collectAuthorizeExprsForRoute(ownNotes, route);
15785
+ const groupsForRoute = ownExprsForRoute.length > 0 ? [ownExprsForRoute] : effectiveAncestorGateExprs(struct, modelDef, route);
15786
+ for (const exprs of groupsForRoute) {
15787
+ groupsForSource.push({ route, exprs });
15788
+ }
15789
+ routeGroupsByRoute.set(route, groupsForRoute);
15790
+ }
15791
+ if (groupsForSource.length > 0) {
15792
+ authorizeMap.set(sourceName, groupsForSource);
15793
+ }
15794
+ const routeGroupsToFlatWire = (groups) => groups && groups.length > 0 ? groups.flat() : undefined;
15795
+ const authorize = routeGroupsToFlatWire(routeGroupsByRoute.get(AUTHORIZE_ROUTE));
15796
+ const accessFilter = routeGroupsToFlatWire(routeGroupsByRoute.get(ACCESS_FILTER_ROUTE));
15797
+ const views = struct.fields.filter((field) => field.type === "turtle").filter((turtle) => turtle.pipeline.map((stage) => stage.type).every((type) => type === "reduce")).map((turtle) => ({
15798
+ name: turtle.as || turtle.name,
15799
+ annotations: annotationTexts(turtle.annotations)
15800
+ }));
15801
+ for (const field of struct.fields) {
15802
+ const fieldAuthorizeNotes = ownLevelNotes(field.annotations).filter((note) => containsAuthorizeAnnotationTag([note.text]));
15803
+ if (fieldAuthorizeNotes.length === 0)
15804
+ continue;
15805
+ if (fieldAuthorizeNotes.every((note) => gatedSourceOwnAuthorizeNotes.has(note))) {
15806
+ continue;
15807
+ }
15808
+ const fieldName = field.as || field.name;
15809
+ if (isJoined3(field) && isSourceDef6(field)) {
15810
+ const joinedStruct = field;
15811
+ if (joinFieldNamesUnresolvableDeclaration(joinedStruct, modelDef)) {
15812
+ continue;
15813
+ }
15814
+ misplacedAuthorize.push({
15815
+ kind: "field",
15816
+ name: sourceName,
15817
+ fieldName,
15818
+ route: fieldAuthorizeNotes.map((note) => authorizeAnnotationRoute(note.text)).find((r) => r !== undefined) ?? ACCESS_FILTER_ROUTE
15819
+ });
15820
+ continue;
15821
+ }
15822
+ misplacedAuthorize.push({
15823
+ kind: "field",
15824
+ name: sourceName,
15825
+ fieldName,
15826
+ route: fieldAuthorizeNotes.map((note) => authorizeAnnotationRoute(note.text)).find((r) => r !== undefined) ?? ACCESS_FILTER_ROUTE
15827
+ });
15298
15828
  }
15829
+ return {
15830
+ name: sourceName,
15831
+ annotations,
15832
+ views,
15833
+ filters,
15834
+ givens,
15835
+ authorize,
15836
+ accessFilter
15837
+ };
15838
+ });
15839
+ return {
15840
+ sources,
15841
+ filterMap,
15842
+ authorizeMap,
15843
+ misplacedAuthorize,
15844
+ authorizeOwnNotes,
15845
+ attributedAuthorizeOwnNotes
15299
15846
  };
15300
15847
  }
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
- }
15848
+ function extractQueriesFromModelDef(modelDef) {
15849
+ const isNamedQuery = (obj) => obj.type === "query";
15850
+ const namedQueries = Object.values(modelDef.contents).filter(isNamedQuery);
15851
+ const misplacedAuthorize = namedQueries.filter((queryObj) => containsAuthorizeAnnotationTag(ownLevelNoteTexts(queryObj.annotations))).map((queryObj) => ({
15852
+ kind: "query",
15853
+ name: queryObj.as || queryObj.name,
15854
+ route: ownLevelNoteTexts(queryObj.annotations).map((text) => authorizeAnnotationRoute(text)).find((r) => r !== undefined) ?? ACCESS_FILTER_ROUTE
15855
+ }));
15856
+ const queries = namedQueries.map((queryObj) => ({
15857
+ name: queryObj.as || queryObj.name,
15858
+ sourceName: typeof queryObj.structRef === "string" ? queryObj.structRef : undefined,
15859
+ annotations: annotationTexts(queryObj.annotations)
15860
+ }));
15861
+ return { queries, misplacedAuthorize };
15315
15862
  }
15316
- var init_given = __esm(() => {
15863
+ var init_source_extraction = __esm(() => {
15317
15864
  init_annotations();
15318
15865
  init_authorize();
15319
- init_motly();
15866
+ init_authorize_routes();
15867
+ init_filter();
15868
+ init_gate_registry_walk();
15320
15869
  });
15321
15870
 
15322
15871
  // src/utils.ts
@@ -15353,7 +15902,7 @@ init_gate_dimension();
15353
15902
  var import_recursive_readdir = __toESM(require_recursive_readdir(), 1);
15354
15903
  import {
15355
15904
  contextOverlay,
15356
- isSourceDef as isSourceDef6,
15905
+ isSourceDef as isSourceDef7,
15357
15906
  MalloyConfig,
15358
15907
  MalloyError as MalloyError2,
15359
15908
  modelDefToModelInfo as modelDefToModelInfo2,
@@ -15370,6 +15919,8 @@ import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
15370
15919
 
15371
15920
  // src/service/package_manifest.ts
15372
15921
  init_constants();
15922
+ init_errors();
15923
+ init_dashboard();
15373
15924
  var FRESHNESS_FALLBACKS = ["live", "stale_ok", "fail"];
15374
15925
  var PACKAGE_SCOPES = ["version", "package"];
15375
15926
  function parsePackageScope(raw) {
@@ -15379,7 +15930,7 @@ function parsePackageScope(raw) {
15379
15930
  if (PACKAGE_SCOPES.includes(raw)) {
15380
15931
  return raw;
15381
15932
  }
15382
- throw new Error(`Invalid "scope" in the package manifest: ${JSON.stringify(raw)}. ` + `Expected "version" or "package" (default "package").`);
15933
+ throw new PackageManifestError(`Invalid "scope" in the package manifest: ${JSON.stringify(raw)}. ` + `Expected "version" or "package" (default "package").`);
15383
15934
  }
15384
15935
  function resolvePackageScope(rootRaw, materializationRaw) {
15385
15936
  const envelopeRaw = materializationRaw && typeof materializationRaw === "object" ? materializationRaw.scope : undefined;
@@ -15389,7 +15940,7 @@ function resolvePackageScope(rootRaw, materializationRaw) {
15389
15940
  const envelopeScope = parsePackageScope(envelopeRaw);
15390
15941
  if (rootDeclared && envelopeDeclared) {
15391
15942
  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).`);
15943
+ 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
15944
  }
15394
15945
  return { scope: envelopeScope, warnings: [] };
15395
15946
  }
@@ -15502,43 +16053,51 @@ function resolveExplores(input) {
15502
16053
  const hasIndexModel = modelPaths.includes(INDEX_MODEL_NAME);
15503
16054
  const warnings = [];
15504
16055
  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.`);
16056
+ 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
16057
  }
15507
16058
  const declared = Array.isArray(declaredExplores) ? declaredExplores.map((entry) => normalizeModelPath(entry)) : undefined;
15508
16059
  if (declaredQueryableSources !== undefined && declaredQueryableSources !== "all") {
15509
16060
  warnings.push(QUERYABLE_SOURCES_DEPRECATION);
15510
16061
  }
15511
16062
  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);
16063
+ if (declared.length === 0) {
16064
+ warnings.push(hasIndexModel ? EXPLORES_EMPTY_SUPPRESSES_CONVENTION : EXPLORES_EMPTY_IS_UNCURATED);
16065
+ } else {
16066
+ warnings.push(exploresDeprecation(declared, hasIndexModel));
15518
16067
  }
15519
- if (hasIndexModel && declared.length > 0) {
15520
- if (!declared.includes(INDEX_MODEL_NAME)) {
15521
- warnings.push(exploresOmitsIndexModel(declared));
15522
- }
16068
+ if (hasIndexModel && declared.length > 0 && !declared.includes(INDEX_MODEL_NAME)) {
16069
+ warnings.push(EXPLORES_OMITS_INDEX_MODEL);
15523
16070
  }
15524
16071
  return { explores: declared, warnings };
15525
16072
  }
15526
16073
  if (!hasIndexModel) {
16074
+ for (const modelPath of modelPaths) {
16075
+ if (!modelPath.includes("/") && modelPath.toLowerCase() === INDEX_MODEL_NAME) {
16076
+ 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}.`);
16077
+ }
16078
+ }
15527
16079
  return { explores: undefined, warnings };
15528
16080
  }
15529
- if (modelPaths.some((modelPath) => modelPath.endsWith(MODEL_FILE_SUFFIX) && modelPath !== INDEX_MODEL_NAME)) {
15530
- warnings.push(INDEX_MODEL_IS_THE_SURFACE);
15531
- }
15532
16081
  return { explores: [INDEX_MODEL_NAME], warnings };
15533
16082
  }
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.`;
16083
+ function listFiles(files) {
16084
+ return files.length <= 1 ? files[0] ?? "" : `${files.slice(0, -1).join(", ")} and ${files[files.length - 1]}`;
16085
+ }
16086
+ function exploresDeprecation(declared, hasIndexModel) {
16087
+ const lead = `"explores" in publisher.json is deprecated.`;
16088
+ const files = declared.filter((entry) => entry !== INDEX_MODEL_NAME && !isDashboardModelPath(entry));
16089
+ if (files.length > 0) {
16090
+ 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".`;
16091
+ }
16092
+ if (declared.includes(INDEX_MODEL_NAME)) {
16093
+ return `${lead} ${INDEX_MODEL_NAME} already publishes the same thing. ` + `Fix: delete "explores".`;
16094
+ }
16095
+ 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
16096
  }
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.`;
16097
+ 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.`;
16098
+ 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".`;
16099
+ var EXPLORES_EMPTY_IS_UNCURATED = `"explores": [] in publisher.json does nothing. Fix: delete it.`;
16100
+ var QUERYABLE_SOURCES_DEPRECATION = `"queryableSources" in publisher.json does nothing. Fix: delete it.`;
15542
16101
 
15543
16102
  // src/package_load/package_load_worker.ts
15544
16103
  init_source_extraction();
@@ -15776,7 +16335,15 @@ function buildWorkerMalloyConfig(job) {
15776
16335
  async function readPackageMetadata(packagePath, modelPaths) {
15777
16336
  const manifestPath = path2.join(packagePath, PACKAGE_MANIFEST_NAME);
15778
16337
  const contents = await fs2.promises.readFile(manifestPath, "utf8");
15779
- const parsed = JSON.parse(contents);
16338
+ let parsed;
16339
+ try {
16340
+ parsed = JSON.parse(contents);
16341
+ } catch (error) {
16342
+ 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.`);
16343
+ }
16344
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
16345
+ throw new PackageManifestError(`Invalid ${PACKAGE_MANIFEST_NAME}: expected a JSON object, got ` + `${JSON.stringify(parsed)}. Fix: { "name": "my-package" }.`);
16346
+ }
15780
16347
  const scope = resolvePackageScope(parsed.scope, parsed.materialization);
15781
16348
  const queryMetadata = resolvePackageQueryMetadata(parsed.queryMetadata, parsed.materialization);
15782
16349
  const explores = resolveExplores({
@@ -15910,7 +16477,7 @@ async function compileMalloyModel(job, malloyConfig, modelPath) {
15910
16477
  onRowLevelGateUnexpressible: authorizeWarningCollection.onRowLevelGateUnexpressible,
15911
16478
  onOwnRowLevelConditionCompiled: (sourceName, condition, route) => {
15912
16479
  const struct = modelDef.contents[sourceName];
15913
- if (!struct || !isSourceDef6(struct))
16480
+ if (!struct || !isSourceDef7(struct))
15914
16481
  return;
15915
16482
  validateSourceLineGateGivenUsage(sourceName, route, struct, condition.refSummary, condition.e, modelDef, (cause, detail) => {
15916
16483
  recordRowLevelGateRejected(cause);
@@ -16035,7 +16602,7 @@ async function compileNotebookModel(job, malloyConfig, modelPath) {
16035
16602
  onRowLevelGateUnexpressible: authorizeWarningCollection.onRowLevelGateUnexpressible,
16036
16603
  onOwnRowLevelConditionCompiled: (sourceName, condition, route) => {
16037
16604
  const struct = finalCompiledModelDef.contents[sourceName];
16038
- if (!struct || !isSourceDef6(struct))
16605
+ if (!struct || !isSourceDef7(struct))
16039
16606
  return;
16040
16607
  validateSourceLineGateGivenUsage(sourceName, route, struct, condition.refSummary, condition.e, finalCompiledModelDef, (cause, detail) => {
16041
16608
  recordRowLevelGateRejected(cause);
@@ -16133,6 +16700,14 @@ function serializeError(error) {
16133
16700
  isCompilationError: true
16134
16701
  };
16135
16702
  }
16703
+ if (error instanceof PackageManifestError) {
16704
+ return {
16705
+ name: error.name,
16706
+ message: error.message,
16707
+ stack: error.stack,
16708
+ isManifestError: true
16709
+ };
16710
+ }
16136
16711
  if (error instanceof Error) {
16137
16712
  return {
16138
16713
  name: error.name,