@malloy-publisher/server 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (21) hide show
  1. package/dist/app/api-doc.yaml +173 -47
  2. package/dist/app/assets/{EnvironmentPage-cVoREk0T.js → EnvironmentPage-C4IrQrKG.js} +1 -1
  3. package/dist/app/assets/{HomePage-9sQJEb1e.js → HomePage-DDyGPzmK.js} +1 -1
  4. package/dist/app/assets/{LightMode-C8uGSmfv.js → LightMode-BpHRmI5M.js} +1 -1
  5. package/dist/app/assets/{MainPage-BIG2W9IX.js → MainPage-BHleF-Kb.js} +1 -1
  6. package/dist/app/assets/{ModelPage-DKkVe4Md.js → ModelPage-Cgw-SbZi.js} +2 -2
  7. package/dist/app/assets/{PackagePage-Cax6RcJM.js → PackagePage-Cy0c37D0.js} +1 -1
  8. package/dist/app/assets/{RenderedResult-BB7eokcf.es-D-hyXuFF.js → RenderedResult-DSgqZUiN.es-CL0FEank.js} +2 -2
  9. package/dist/app/assets/{RouteError-BzJ2T1nX.js → RouteError-DBMc9ccU.js} +1 -1
  10. package/dist/app/assets/{ThemeEditorPage-Ce7Cp5zL.js → ThemeEditorPage-ByJlsmNu.js} +1 -1
  11. package/dist/app/assets/{ToggleButtonGroup-DOVU2psA.js → ToggleButtonGroup-BHEVw4Ti.js} +1 -1
  12. package/dist/app/assets/{index-CPy5UJ3W.js → index-BdwkgpbO.js} +1 -1
  13. package/dist/app/assets/{index-DycyDIlK.js → index-C5BtJ1GX.js} +4 -4
  14. package/dist/app/assets/{index-BOX8mX0R.js → index-Dyxfz1yO.js} +1 -1
  15. package/dist/app/assets/{index-DfULFk5d.js → index-c3i6ScGn.js} +1 -1
  16. package/dist/app/assets/{index-BJv4aICx.js → index-rWWmF7hn.js} +1 -1
  17. package/dist/app/assets/{index.es-Doid8lz7.js → index.es-VI5lesX5.js} +4 -4
  18. package/dist/app/index.html +1 -1
  19. package/dist/package_load_worker.mjs +694 -353
  20. package/dist/server.mjs +2743 -726
  21. package/package.json +20 -2
@@ -13103,6 +13103,29 @@ var init_errors = __esm(() => {
13103
13103
  };
13104
13104
  });
13105
13105
 
13106
+ // src/service/authorize_routes.ts
13107
+ function canonicalAuthorizeRoute(route) {
13108
+ switch (route) {
13109
+ case ACCESS_FILTER_ROUTE:
13110
+ return ACCESS_FILTER_ROUTE;
13111
+ case AUTHORIZE_ROUTE:
13112
+ return AUTHORIZE_ROUTE;
13113
+ default:
13114
+ return;
13115
+ }
13116
+ }
13117
+ var AUTHORIZE_ROUTE = "authorize", ACCESS_FILTER_ROUTE = "access_filter", RECOGNIZED_AUTHORIZE_SPELLINGS, CANONICAL_AUTHORIZE_ROUTES;
13118
+ var init_authorize_routes = __esm(() => {
13119
+ RECOGNIZED_AUTHORIZE_SPELLINGS = [
13120
+ ACCESS_FILTER_ROUTE,
13121
+ AUTHORIZE_ROUTE
13122
+ ];
13123
+ CANONICAL_AUTHORIZE_ROUTES = [
13124
+ ACCESS_FILTER_ROUTE,
13125
+ AUTHORIZE_ROUTE
13126
+ ];
13127
+ });
13128
+
13106
13129
  // src/service/authorize.ts
13107
13130
  import { payloadOf, routeOf } from "@malloydata/malloy";
13108
13131
  function noteRoute(text) {
@@ -13111,8 +13134,61 @@ function noteRoute(text) {
13111
13134
  function notePayload(text) {
13112
13135
  return payloadOf({ value: text.trimStart() }) ?? "";
13113
13136
  }
13137
+ function nearMissRouteNames(route) {
13138
+ switch (route) {
13139
+ case AUTHORIZE_ROUTE:
13140
+ return [
13141
+ "authorize",
13142
+ "source_authorize",
13143
+ "source-authorize",
13144
+ "sourceauthorize",
13145
+ "authorize_source",
13146
+ "authorize-source",
13147
+ "authorizesource"
13148
+ ];
13149
+ case ACCESS_FILTER_ROUTE:
13150
+ return [
13151
+ "access_filter",
13152
+ "access-filter",
13153
+ "accessfilter",
13154
+ "filter_access",
13155
+ "filter-access",
13156
+ "filteraccess",
13157
+ "row_authorize",
13158
+ "row-authorize",
13159
+ "rowauthorize",
13160
+ "authorize_row",
13161
+ "authorize-row",
13162
+ "authorizerow"
13163
+ ];
13164
+ default:
13165
+ return [route];
13166
+ }
13167
+ }
13168
+ function nearMissWordAlternation(route) {
13169
+ const names = nearMissRouteNames(route);
13170
+ if (names.length === 0) {
13171
+ throw new Error(`No near-miss spellings for authorize route \`${route}\` — an empty ` + "alternation matches every malformed-prefix note, which would " + "fail package loads over annotations nobody wrote.");
13172
+ }
13173
+ return names.map((name) => name.replace(REGEXP_SPECIAL, "\\$&")).join("|");
13174
+ }
13175
+ function motlyAuthorizePayloadPattern(route) {
13176
+ return new RegExp(`^[ \\t]*[([{<][ \\t]*(?:${nearMissWordAlternation(route)})[ \\t]*[)\\]}>]`, "iu");
13177
+ }
13178
+ function malformedAuthorizeAttemptPattern(route) {
13179
+ return new RegExp(`^##?\\|?[ \\t]*[([{<]?[ \\t]*(?:${nearMissWordAlternation(route)})`, "iu");
13180
+ }
13114
13181
  function authorizeNoteContent(text) {
13115
- return noteRoute(text) === AUTHORIZE_ROUTE ? notePayload(text) : undefined;
13182
+ const route = canonicalAuthorizeRoute(noteRoute(text));
13183
+ if (route === undefined)
13184
+ return;
13185
+ return { route, content: notePayload(text) };
13186
+ }
13187
+ function authorizeAnnotationRoute(text) {
13188
+ return authorizeNoteContent(text)?.route;
13189
+ }
13190
+ function hasCallerAuthorizeAnnotation(callerText) {
13191
+ return AUTHORIZE_ANNOTATION_ANYWHERE.test(callerText);
13116
13192
  }
13117
13193
  function assertNoCallerAuthorizeAnnotation(callerText) {
13118
13194
  if (!AUTHORIZE_ANNOTATION_ANYWHERE.test(callerText))
@@ -13122,37 +13198,59 @@ function assertNoCallerAuthorizeAnnotation(callerText) {
13122
13198
  function containsAuthorizeAnnotationTag(texts) {
13123
13199
  return texts.some((text) => authorizeNoteContent(text) !== undefined);
13124
13200
  }
13125
- function collectAuthorizeNearMisses(texts) {
13201
+ function collectAuthorizeNearMisses(texts, route) {
13202
+ const motlyPattern = motlyAuthorizePayloadPattern(route);
13203
+ const malformedPattern = malformedAuthorizeAttemptPattern(route);
13204
+ const nearMissNames = new Set(nearMissRouteNames(route).map((name) => name.toLowerCase()));
13126
13205
  const found = [];
13127
13206
  for (const text of texts) {
13128
13207
  const trimmed = text.trimStart();
13129
- const route = noteRoute(trimmed);
13130
- if (route === AUTHORIZE_ROUTE)
13208
+ const noted = noteRoute(trimmed);
13209
+ if (noted === route)
13131
13210
  continue;
13132
- const nearMiss = route === undefined ? MALFORMED_AUTHORIZE_ATTEMPT.test(trimmed) : route === "" ? MOTLY_AUTHORIZE_PAYLOAD.test(notePayload(trimmed)) : route.toLowerCase() === AUTHORIZE_ROUTE;
13211
+ const nearMiss = noted === undefined ? malformedPattern.test(trimmed) : noted === "" ? motlyPattern.test(notePayload(trimmed)) : nearMissNames.has(noted.toLowerCase());
13133
13212
  if (!nearMiss)
13134
13213
  continue;
13135
13214
  found.push(trimmed.split(/[\r\n]/, 1)[0]);
13136
13215
  }
13137
13216
  return found;
13138
13217
  }
13218
+ function collectAuthorizeNearMissesAllRoutes(texts) {
13219
+ const materialized = [...texts];
13220
+ return RECOGNIZED_AUTHORIZE_SPELLINGS.flatMap((route) => collectAuthorizeNearMisses(materialized, route));
13221
+ }
13222
+ function guessedNearMissRoute(text) {
13223
+ const lower = text.toLowerCase();
13224
+ let best = ACCESS_FILTER_ROUTE;
13225
+ let bestLength = 0;
13226
+ for (const route of RECOGNIZED_AUTHORIZE_SPELLINGS) {
13227
+ for (const name of nearMissRouteNames(route)) {
13228
+ if (lower.includes(name) && name.length > bestLength) {
13229
+ best = route;
13230
+ bestLength = name.length;
13231
+ }
13232
+ }
13233
+ }
13234
+ return best;
13235
+ }
13139
13236
  function assertNoAuthorizeNearMisses(found) {
13140
13237
  if (found.length === 0)
13141
13238
  return;
13142
13239
  const unique = [...new Set(found)];
13143
13240
  throw new ModelCompilationError({
13144
- message: `These annotations are not \`authorize\` gates and nothing enforces ` + `them:
13145
- ${unique.map((t) => ` - \`${t}\``).join(`
13241
+ message: `These annotations are never enforced:
13242
+ ${unique.map((t) => ` - \`${t}\` (meant \`#(${guessedNearMissRoute(t)})\`?)`).join(`
13146
13243
  `)}
13147
- ` + `Malloy routes an annotation by its prefix, and only ` + `\`#(authorize)\` (or \`##(authorize)\`, or the block form ` + `\`#|(authorize)\`) reaches the authorize route — a space after the ` + `\`#\`, spaces inside the brackets, or anything trailing the closing ` + `bracket makes it a plain tag Malloy hands to something else. Write ` + `\`#(authorize) <expression>\` on its own line directly above the ` + `\`source:\` statement you mean to protect (unquoted — the quoted ` + `string form is retired). This is refused rather than interpreted: ` + `guessing at the ` + `intent would let publisher start enforcing a filter on a package that ` + `has been serving every row.`
13244
+ ` + `Malloy routes an annotation by its prefix, and only ` + `\`#(authorize)\` and \`#(access_filter)\` reach an access-control ` + `route (in their \`##\` and ` + `block-form \`#|(...)\` spellings too). A space after the \`#\`, spaces ` + `inside the brackets, a mis-hyphenated or transposed word, or anything ` + `trailing the closing bracket makes it a plain tag Malloy hands to ` + `something else. Write the tag named above, unquoted, on its own line ` + `directly above the \`source:\` statement you mean to protect (the ` + `quoted string form is retired). This is refused rather than ` + `interpreted: guessing at the intent would let publisher start ` + `enforcing a filter on a package that has been serving every row.`
13148
13245
  });
13149
13246
  }
13150
13247
  function describeMisplacedAuthorizeAnnotation(f) {
13151
- if (f.kind === "query")
13152
- return `on query "${f.name}"`;
13153
13248
  if (f.kind === "file")
13154
- return "at the file level (`##(authorize)`)";
13155
- return `on field "${f.fieldName}" of source "${f.name}"`;
13249
+ return `at the file level (\`##(${f.route})\`)`;
13250
+ const tag = `\`#(${f.route})\``;
13251
+ if (f.kind === "query")
13252
+ return `on query "${f.name}" (${tag})`;
13253
+ return `on field "${f.fieldName}" of source "${f.name}" (${tag})`;
13156
13254
  }
13157
13255
  function assertNoMisplacedAuthorizeAnnotations(found) {
13158
13256
  if (found.length === 0)
@@ -13160,9 +13258,9 @@ function assertNoMisplacedAuthorizeAnnotations(found) {
13160
13258
  const positions = found.map((f) => ` - ${describeMisplacedAuthorizeAnnotation(f)}`).join(`
13161
13259
  `);
13162
13260
  throw new ModelCompilationError({
13163
- message: `An \`#(authorize)\` annotation is never enforced at:
13261
+ message: `An authorize annotation is never enforced at:
13164
13262
  ${positions}
13165
- ` + `A gate only applies where model load looks for one — a \`source:\`'s ` + `own annotation, or one it inherits from an \`extend\`/query-source ` + `base. File-level \`##(authorize)\` is deprecated and no longer ` + `enforced anywhere, so it always lands here: declare \`#(authorize)\` ` + `on each \`source:\` it was meant to protect instead. Every other ` + `position above should move to the \`source:\` statement it is meant ` + `to protect.`
13263
+ ` + `A gate only applies where model load looks for one — a \`source:\`'s ` + `own annotation, or one it inherits from an \`extend\`/query-source ` + `base. A file-level annotation (\`##(authorize)\` / ` + `\`##(access_filter)\`) is deprecated and no longer enforced ` + `anywhere, so it always lands here: declare the same tag on each ` + `\`source:\` it was meant to protect instead. Every other ` + `position above should move — with the same tag named beside it — ` + `to the \`source:\` statement it is meant ` + `to protect.`
13166
13264
  });
13167
13265
  }
13168
13266
  function referencedGivenNames(expr) {
@@ -13199,7 +13297,7 @@ function liftProbeFilterCondition(prepared, label, filterText) {
13199
13297
  return lifted;
13200
13298
  }
13201
13299
  function gateFilterText(exprs) {
13202
- return exprs.map((e) => `(${e})`).join(" or ");
13300
+ return exprs.map((e) => `(${e})`).join(" and ");
13203
13301
  }
13204
13302
  async function liftRowLevelCondition(compiler, sourceName, exprs) {
13205
13303
  const filterText = gateFilterText(exprs);
@@ -13209,7 +13307,7 @@ async function liftRowLevelCondition(compiler, sourceName, exprs) {
13209
13307
  async function validateAuthorizeProbes(compiler, options) {
13210
13308
  const ownNotesOf = options.authorizeOwnNotes ?? new Map;
13211
13309
  for (const [sourceName, groups] of options.authorizeMap ?? []) {
13212
- for (const exprs of groups) {
13310
+ for (const { route, exprs } of groups) {
13213
13311
  if (exprs.length === 0)
13214
13312
  continue;
13215
13313
  let condition;
@@ -13217,17 +13315,17 @@ async function validateAuthorizeProbes(compiler, options) {
13217
13315
  condition = await liftRowLevelCondition(compiler, sourceName, exprs);
13218
13316
  } catch (err) {
13219
13317
  const detail = err instanceof Error ? err.message : String(err);
13220
- const ownNotes = ownNotesOf.get(sourceName) ?? [];
13318
+ const ownNotes = ownNotesOf.get(sourceName)?.get(route) ?? [];
13221
13319
  if (ownNotes.length === 0) {
13222
13320
  options.onRowLevelGateRejected?.("entry_point_unexpressible");
13223
13321
  options.onRowLevelGateUnexpressible?.(sourceName, detail);
13224
13322
  continue;
13225
13323
  }
13226
13324
  throw new ModelCompilationError({
13227
- message: `Invalid #(authorize) annotation on source "${sourceName}" ` + `[${exprs.join(" | ")}]: ${detail}`
13325
+ message: `Invalid #(${route}) annotation on source "${sourceName}" ` + `[${exprs.join(" and ")}]: ${detail}`
13228
13326
  });
13229
13327
  }
13230
- options.onOwnRowLevelConditionCompiled?.(sourceName, condition);
13328
+ options.onOwnRowLevelConditionCompiled?.(sourceName, condition, route);
13231
13329
  }
13232
13330
  }
13233
13331
  }
@@ -13241,59 +13339,41 @@ function unquoteLegacyGatePayload(payload) {
13241
13339
  return trimmed[0] === "'" ? inner.replace(SINGLE_QUOTE_ESCAPE, "$1") : inner.replace(DOUBLE_QUOTE_ESCAPE, "$1");
13242
13340
  }
13243
13341
  function parseAuthorizeAnnotation(annotation) {
13244
- const content = authorizeNoteContent(annotation);
13245
- if (content === undefined)
13342
+ const parsed = authorizeNoteContent(annotation);
13343
+ if (parsed === undefined)
13246
13344
  return null;
13247
- const trimmed = content.trim();
13345
+ const trimmed = parsed.content.trim();
13248
13346
  if (trimmed.length === 0) {
13249
13347
  throw new Error("authorize annotation has an empty expression body");
13250
13348
  }
13251
- return trimmed;
13349
+ return { route: parsed.route, expr: trimmed };
13252
13350
  }
13253
13351
  function collectAuthorizeExprs(annotations) {
13254
13352
  const exprs = [];
13255
13353
  for (const annotation of annotations) {
13256
- const expr = parseAuthorizeAnnotation(annotation);
13257
- if (expr !== null) {
13258
- exprs.push(expr);
13354
+ const parsed = parseAuthorizeAnnotation(annotation);
13355
+ if (parsed !== null) {
13356
+ exprs.push(parsed);
13259
13357
  }
13260
13358
  }
13261
13359
  return exprs;
13262
13360
  }
13263
- function findMultipleAuthorizeGates(authorizeOwnNotes) {
13264
- const found = [];
13265
- for (const [sourceName, notes] of authorizeOwnNotes) {
13266
- if (notes.length > 1) {
13267
- found.push({ sourceName, texts: notes.map((note) => note.text) });
13268
- }
13269
- }
13270
- return found;
13271
- }
13272
- function assertAtMostOneAuthorizeGate(found) {
13273
- if (found.length === 0)
13274
- return;
13275
- const positions = found.map(({ sourceName, texts }) => ` - source "${sourceName}" declares ${texts.length}:
13276
- ` + texts.map((t) => ` \`${t.trim().split(/[\r\n]/, 1)[0]}\``).join(`
13277
- `)).join(`
13278
- `);
13279
- throw new ModelCompilationError({
13280
- message: `A source may declare at most one \`#(authorize)\` annotation:
13281
- ${positions}
13282
- ` + `Combine multiple conditions into one expression with \`or\` instead of ` + `repeating the annotation, e.g. ` + "`#(authorize) $ROLE = 'admin' or org_id in $GROUPS`."
13283
- });
13361
+ function collectAuthorizeExprsForRoute(annotations, route) {
13362
+ return collectAuthorizeExprs(annotations).filter((e) => e.route === route).map((e) => e.expr);
13284
13363
  }
13285
13364
  function findLegacyStringGates(authorizeOwnNotes) {
13286
13365
  const found = [];
13287
- for (const [sourceName, notes] of authorizeOwnNotes) {
13366
+ for (const [sourceName, notesByRoute] of authorizeOwnNotes) {
13367
+ const notes = [...notesByRoute.values()].flat();
13288
13368
  const legacyNotes = notes.filter((note) => {
13289
13369
  const content = authorizeNoteContent(note.text);
13290
- return content !== undefined && isLegacyQuotedPayload(content);
13370
+ return content !== undefined && isLegacyQuotedPayload(content.content);
13291
13371
  });
13292
13372
  if (legacyNotes.length === 0)
13293
13373
  continue;
13294
- const exprs = collectAuthorizeExprs(legacyNotes.map((note) => note.text));
13295
- if (exprs.length > 0) {
13296
- found.push({ sourceName, exprs });
13374
+ const gates = legacyNotes.flatMap((note) => collectAuthorizeExprs([note.text]));
13375
+ if (gates.length > 0) {
13376
+ found.push({ sourceName, gates });
13297
13377
  }
13298
13378
  }
13299
13379
  return found;
@@ -13301,22 +13381,22 @@ function findLegacyStringGates(authorizeOwnNotes) {
13301
13381
  function assertNoLegacyStringGate(found) {
13302
13382
  if (found.length === 0)
13303
13383
  return;
13304
- const rewrites = found.flatMap(({ sourceName, exprs }) => exprs.map((expr) => ` - source "${sourceName}": replace \`#(authorize) ${expr}\`` + ` with
13305
- #(authorize) ${unquoteLegacyGatePayload(expr)}`)).join(`
13384
+ const rewrites = found.flatMap(({ sourceName, gates }) => gates.map(({ route, expr }) => ` - source "${sourceName}": replace \`#(${route}) ${expr}\`` + ` with
13385
+ #(${route}) ${unquoteLegacyGatePayload(expr)}`)).join(`
13306
13386
  `);
13307
13387
  throw new ModelCompilationError({
13308
- message: `The string form of \`#(authorize)\` (a Malloy-quoted expression on ` + `the \`source:\` line) is no longer accepted. Replace it with the ` + `unquoted expression, carried by an \`#(authorize)\` annotation on ` + `its own line directly above the \`source:\` line:
13388
+ message: `The string form of an authorize gate (a Malloy-quoted expression on ` + `the \`source:\` line) is no longer accepted. Replace it with the ` + `unquoted expression, carried by the same annotation on its own line ` + `directly above the \`source:\` line:
13309
13389
  ${rewrites}
13310
13390
  ` + `Test that the rewrite gates the same rows and check the row count ` + `matches what the string form served.`
13311
13391
  });
13312
13392
  }
13313
- var AUTHORIZE_ROUTE = "authorize", AUTHORIZE_TAG_LIKE, AUTHORIZE_ANNOTATION_ANYWHERE, MOTLY_AUTHORIZE_PAYLOAD, MALFORMED_AUTHORIZE_ATTEMPT, GIVEN_REF_PATTERN, SINGLE_QUOTE_ESCAPE, DOUBLE_QUOTE_ESCAPE, STRING_LITERAL_PATTERN, everyMemberOf = () => (...members) => members, ROW_LEVEL_GATE_REJECTION_CAUSES, WHOLE_BODY_QUOTED_STRING, WHOLE_BODY_SINGLE_QUOTED_STRING;
13393
+ var AUTHORIZE_TAG_LIKE, AUTHORIZE_ANNOTATION_ANYWHERE, REGEXP_SPECIAL, GIVEN_REF_PATTERN, SINGLE_QUOTE_ESCAPE, DOUBLE_QUOTE_ESCAPE, STRING_LITERAL_PATTERN, everyMemberOf = () => (...members) => members, ROW_LEVEL_GATE_REJECTION_CAUSES, WHOLE_BODY_QUOTED_STRING, WHOLE_BODY_SINGLE_QUOTED_STRING;
13314
13394
  var init_authorize = __esm(() => {
13315
13395
  init_errors();
13316
- AUTHORIZE_TAG_LIKE = String.raw`##?\|?[ \t]*[([{<]?[ \t]*authorize(?=[)\]}>]|[ \t]|$)`;
13396
+ init_authorize_routes();
13397
+ AUTHORIZE_TAG_LIKE = String.raw`##?\|?[ \t]*[([{<]?[ \t]*(?:(?:(?:row|source)[-_]?)?authorize|access[-_]?filter)(?=[)\]}>]|[ \t]|$)`;
13317
13398
  AUTHORIZE_ANNOTATION_ANYWHERE = new RegExp(AUTHORIZE_TAG_LIKE, "iu");
13318
- MOTLY_AUTHORIZE_PAYLOAD = /^[ \t]*[([{<][ \t]*authorize[ \t]*[)\]}>]/iu;
13319
- MALFORMED_AUTHORIZE_ATTEMPT = /^##?\|?[ \t]*[([{<]?[ \t]*authorize/iu;
13399
+ REGEXP_SPECIAL = /[.*+?^${}()|[\]\\]/g;
13320
13400
  GIVEN_REF_PATTERN = /\$([A-Za-z_][A-Za-z0-9_]*)/g;
13321
13401
  SINGLE_QUOTE_ESCAPE = /\\(['\\])/g;
13322
13402
  DOUBLE_QUOTE_ESCAPE = /\\(["\\])/g;
@@ -13341,20 +13421,33 @@ function recordAuthorizeBypass(entryPoint) {
13341
13421
  }
13342
13422
  function recordRowLevelGateDecision(decision) {
13343
13423
  rowLevelDecisionCounter ??= publisherMeter().createCounter("publisher_authorize_row_level_total", {
13344
- description: "How a row-level `#(authorize)` 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."
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."
13345
13425
  });
13346
13426
  rowLevelDecisionCounter.add(1, { decision });
13347
13427
  }
13348
13428
  function recordRowLevelGateRejected(cause) {
13349
13429
  rowLevelRejectionCounter ??= publisherMeter().createCounter("publisher_authorize_row_level_rejected_total", {
13350
- description: "Row-level `#(authorize)` gates that were refused, warned about, or could not be resolved. Label: cause (" + ROW_LEVEL_GATE_REJECTION_CAUSES.map((c) => `'${c}'`).join("|") + "). Only 'legacy_string_gate' fails the whole model load. 'entry_point_unexpressible', 'source_line_gate_no_given_reference' and 'source_line_gate_negated_membership' warn at load and leave the model servable. 'unreachable_given', 'given_usage_unresolvable' and 'unclassifiable_condition' are request-time only — that entry point denies every request. Alert on any nonzero value since the last publish; the request-time causes also make this a rate signal, so see the doc above before writing an alert."
13430
+ description: "Row-level `#(access_filter)` gates that were refused, warned about, or could not be resolved. Label: cause (" + ROW_LEVEL_GATE_REJECTION_CAUSES.map((c) => `'${c}'`).join("|") + "). Only 'legacy_string_gate' fails the whole model load. 'entry_point_unexpressible', 'source_line_gate_no_given_reference' and 'source_line_gate_negated_membership' warn at load and leave the model servable. 'unreachable_given', 'given_usage_unresolvable' and 'unclassifiable_condition' are request-time only — that entry point denies every request. Alert on any nonzero value since the last publish; the request-time causes also make this a rate signal, so see the doc above before writing an alert."
13351
13431
  });
13352
13432
  rowLevelRejectionCounter.add(1, { cause });
13353
13433
  }
13354
- var guardRejectionCounter = null, bypassCounter = null, rowLevelDecisionCounter = null, rowLevelRejectionCounter = null;
13434
+ function recordAuthorizeAdmitAllGate(route) {
13435
+ admitAllCounter ??= publisherMeter().createCounter("publisher_authorize_admit_all_total", {
13436
+ description: "Sources declaring their OWN unconditional admit-all gate (`#(authorize) true`), counted once each at package load. Label: route (" + CANONICAL_AUTHORIZE_ROUTES.map((r) => `'${r}'`).join("|") + "). Not an error — `true` is the only spelling for an extension that deliberately re-opens a gated base — but it is the one declaration that turns a gate off, so a jump since the last publish is worth a look."
13437
+ });
13438
+ admitAllCounter.add(1, { route });
13439
+ }
13440
+ function recordLockDecision(decision) {
13441
+ 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."
13443
+ });
13444
+ lockDecisionCounter.add(1, { decision });
13445
+ }
13446
+ var guardRejectionCounter = null, bypassCounter = null, rowLevelDecisionCounter = null, rowLevelRejectionCounter = null, admitAllCounter = null, lockDecisionCounter = null;
13355
13447
  var init_authorize_metrics = __esm(() => {
13356
13448
  init_telemetry();
13357
13449
  init_authorize();
13450
+ init_authorize_routes();
13358
13451
  });
13359
13452
 
13360
13453
  // src/data_styles.ts
@@ -13480,6 +13573,317 @@ function ownLevelNotes(annote) {
13480
13573
  }
13481
13574
  var init_annotations = () => {};
13482
13575
 
13576
+ // src/service/authorize_grammar.ts
13577
+ import { routeOf as routeOf2 } from "@malloydata/malloy";
13578
+ function noteRoute2(text) {
13579
+ return routeOf2({ value: text.trimStart() });
13580
+ }
13581
+ function splitFieldPathSegments(fieldPath) {
13582
+ const segments = [];
13583
+ let i = 0;
13584
+ while (i < fieldPath.length) {
13585
+ if (fieldPath[i] === "`") {
13586
+ const end = fieldPath.indexOf("`", i + 1);
13587
+ segments.push(fieldPath.slice(i + 1, end));
13588
+ i = end + 1;
13589
+ } else {
13590
+ const dot = fieldPath.indexOf(".", i);
13591
+ const end = dot === -1 ? fieldPath.length : dot;
13592
+ segments.push(fieldPath.slice(i, end));
13593
+ i = end;
13594
+ }
13595
+ if (fieldPath[i] === ".")
13596
+ i++;
13597
+ }
13598
+ return segments;
13599
+ }
13600
+ function rejectionMessage(sourceName, body, detail) {
13601
+ return `Source "${sourceName}" declares an authorize gate \`${body}\`: ${detail} ` + GRAMMAR_SUMMARY;
13602
+ }
13603
+ function reject(sourceName, body, cause, detail) {
13604
+ throw new AuthorizeGrammarError(cause, rejectionMessage(sourceName, body, detail));
13605
+ }
13606
+ function rejectCoherence(sourceName, cause, detail) {
13607
+ throw new AuthorizeGrammarError(cause, `Source "${sourceName}" declares authorize notes whose terms ` + `conflict: ${detail} ${GRAMMAR_SUMMARY}`);
13608
+ }
13609
+ function splitTerms(body) {
13610
+ const terms = [];
13611
+ const compoundTokens = [];
13612
+ let depth = 0;
13613
+ let quote;
13614
+ let start = 0;
13615
+ let i = 0;
13616
+ while (i < body.length) {
13617
+ const ch = body[i];
13618
+ if (quote) {
13619
+ if (ch === "\\" && quote !== "`") {
13620
+ i += 2;
13621
+ continue;
13622
+ }
13623
+ if (ch === quote)
13624
+ quote = undefined;
13625
+ i++;
13626
+ continue;
13627
+ }
13628
+ if (isQuote(ch)) {
13629
+ quote = ch;
13630
+ i++;
13631
+ continue;
13632
+ }
13633
+ if (ch === "(") {
13634
+ depth++;
13635
+ i++;
13636
+ continue;
13637
+ }
13638
+ if (ch === ")") {
13639
+ depth = Math.max(0, depth - 1);
13640
+ i++;
13641
+ continue;
13642
+ }
13643
+ if (depth === 0) {
13644
+ const wordMatch = /^(and|or|not)\b/i.exec(body.slice(i));
13645
+ if (wordMatch && (i === 0 || !/[A-Za-z0-9_]/.test(body[i - 1]))) {
13646
+ const word = wordMatch[1].toLowerCase();
13647
+ if (word === "and") {
13648
+ terms.push(body.slice(start, i));
13649
+ i += wordMatch[0].length;
13650
+ start = i;
13651
+ continue;
13652
+ }
13653
+ compoundTokens.push(word);
13654
+ }
13655
+ }
13656
+ i++;
13657
+ }
13658
+ terms.push(body.slice(start));
13659
+ return { terms, compoundTokens };
13660
+ }
13661
+ function isQuote(ch) {
13662
+ return ch === "'" || ch === '"' || ch === "`";
13663
+ }
13664
+ function findOperatorOutsideQuotes(s, re) {
13665
+ let quote;
13666
+ for (let i = 0;i < s.length; i++) {
13667
+ const ch = s[i];
13668
+ if (quote) {
13669
+ if (ch === "\\" && quote !== "`") {
13670
+ i++;
13671
+ continue;
13672
+ }
13673
+ if (ch === quote)
13674
+ quote = undefined;
13675
+ continue;
13676
+ }
13677
+ if (isQuote(ch)) {
13678
+ quote = ch;
13679
+ continue;
13680
+ }
13681
+ re.lastIndex = i;
13682
+ const m = re.exec(s);
13683
+ if (m && m.index === i)
13684
+ return i;
13685
+ }
13686
+ return -1;
13687
+ }
13688
+ function parseTerm(sourceName, body, term, givenDeclaredTypes) {
13689
+ const trimmed = term.trim();
13690
+ if (findOperatorOutsideQuotes(trimmed, NEGATED_OPERATOR_RE) !== -1) {
13691
+ reject(sourceName, body, "negated_operator", "`!=` is not allowed.");
13692
+ }
13693
+ if (findOperatorOutsideQuotes(trimmed, COMPARISON_OPERATOR_RE) !== -1) {
13694
+ reject(sourceName, body, "comparison_operator", "only `=` and `in` are allowed, not `<`/`>`/`<=`/`>=`.");
13695
+ }
13696
+ const inIdx = findOperatorOutsideQuotes(trimmed, IN_OPERATOR_RE);
13697
+ const eqIdx = findOperatorOutsideQuotes(trimmed, EQ_OPERATOR_RE);
13698
+ let operator;
13699
+ let opIdx;
13700
+ let opLen;
13701
+ if (inIdx !== -1 && (eqIdx === -1 || inIdx < eqIdx)) {
13702
+ operator = "in";
13703
+ opIdx = inIdx;
13704
+ opLen = 2;
13705
+ } else if (eqIdx !== -1) {
13706
+ operator = "=";
13707
+ opIdx = eqIdx;
13708
+ opLen = 1;
13709
+ } else {
13710
+ reject(sourceName, body, "malformed_body", "no `=` or `in` operator was found.");
13711
+ }
13712
+ let left = trimmed.slice(0, opIdx).trim();
13713
+ let right = trimmed.slice(opIdx + opLen).trim();
13714
+ if (GIVEN_REF_RE.test(left) && STRING_LITERAL_RE.test(right)) {
13715
+ if (operator === "in") {
13716
+ reject(sourceName, body, "reversed_in_operands", `\`${left} in ${right}\` reads $GIVEN in 'literal', which Malloy ` + `rejects as array-in-string membership. Write the literal ` + `first: \`${right} in ${left}\`.`);
13717
+ }
13718
+ [left, right] = [right, left];
13719
+ }
13720
+ const leftIsFieldPath = FIELD_PATH_RE.test(left);
13721
+ const leftIsLiteral = STRING_LITERAL_RE.test(left);
13722
+ if (!leftIsFieldPath && !leftIsLiteral) {
13723
+ reject(sourceName, body, "left_not_field_path", `\`${left}\` is neither a field path nor a string literal — one ` + "side must be a single column, a dotted join path, or a quoted " + "literal, and the other a `$GIVEN`.");
13724
+ }
13725
+ const givenMatch = GIVEN_REF_RE.exec(right);
13726
+ if (!givenMatch) {
13727
+ reject(sourceName, body, "missing_given_reference", `\`${right}\` is not a given reference — a term compares against ` + "`$NAME`.");
13728
+ }
13729
+ const given = givenMatch[1];
13730
+ const declaredType = givenDeclaredTypes.get(given);
13731
+ if (declaredType !== undefined) {
13732
+ const isListType = declaredType === "array" || declaredType.endsWith("[]");
13733
+ if (operator === "=" && isListType) {
13734
+ reject(sourceName, body, "operator_arity_mismatch", `\`$${given}\` is a list-typed given — use \`in\`, not \`=\`.`);
13735
+ }
13736
+ if (operator === "in" && !isListType) {
13737
+ reject(sourceName, body, "operator_arity_mismatch", `\`$${given}\` is a scalar given — use \`=\`, not \`in\`.`);
13738
+ }
13739
+ }
13740
+ if (leftIsLiteral) {
13741
+ return { scope: "source_level", literal: left, given };
13742
+ }
13743
+ return {
13744
+ scope: "row_level",
13745
+ fieldPath: left,
13746
+ fieldPathSegments: splitFieldPathSegments(left),
13747
+ given,
13748
+ operator
13749
+ };
13750
+ }
13751
+ function assertAuthorizeGrammarTermsCoherent(sourceName, terms) {
13752
+ if (terms.length > 1 && terms.some(({ term }) => term.scope === "deny_all")) {
13753
+ rejectCoherence(sourceName, "deny_all_with_sibling", "an unconditional `false` deny-all cannot be combined with any " + "other authorize note — a deny-all admits nothing, so a " + "sibling note can never change what is served and its " + "presence is very likely a mistake.");
13754
+ }
13755
+ const byRoute = new Map;
13756
+ for (const { term, route } of terms) {
13757
+ if (term.scope === "deny_all")
13758
+ continue;
13759
+ const list = byRoute.get(route);
13760
+ if (list)
13761
+ list.push(term);
13762
+ else
13763
+ byRoute.set(route, [term]);
13764
+ }
13765
+ for (const allRouteTerms of byRoute.values()) {
13766
+ if (allRouteTerms.length > 1 && allRouteTerms.some((t) => t.scope === "admit_all")) {
13767
+ rejectCoherence(sourceName, "admit_all_with_sibling", "an unconditional `true` admit-all cannot be combined with " + "another `#(authorize)` note — the lock's notes AND " + "together, so `true and x` reduces to `x` and the admit-all " + "is dead text. To unlock a base while its rows stay " + "filtered, pair the `true` with `#(access_filter)` instead.");
13768
+ }
13769
+ const routeTerms = allRouteTerms.filter((t) => t.scope !== "admit_all" && t.scope !== "deny_all");
13770
+ const seenGivens = new Set;
13771
+ const seenFieldPaths = new Set;
13772
+ for (const t of routeTerms) {
13773
+ if (seenGivens.has(t.given)) {
13774
+ rejectCoherence(sourceName, "duplicate_given", `\`$${t.given}\` is used by more than one term — a given ` + "can back at most one term per route.");
13775
+ }
13776
+ seenGivens.add(t.given);
13777
+ if (t.scope === "row_level") {
13778
+ const canonicalPath = JSON.stringify(t.fieldPathSegments);
13779
+ if (seenFieldPaths.has(canonicalPath)) {
13780
+ rejectCoherence(sourceName, "duplicate_field_path", `\`${t.fieldPath}\` is used by more than one term.`);
13781
+ }
13782
+ seenFieldPaths.add(canonicalPath);
13783
+ }
13784
+ }
13785
+ }
13786
+ }
13787
+ function parseAuthorizeGrammarBody(sourceName, body, givenDeclaredTypes, route) {
13788
+ const trimmedBody = body.trim();
13789
+ if (trimmedBody.length === 0) {
13790
+ reject(sourceName, trimmedBody, "empty_body", "the expression body is empty.");
13791
+ }
13792
+ const sentinel = trimmedBody.toLowerCase();
13793
+ if (sentinel === "false" || sentinel === "true") {
13794
+ if (route !== AUTHORIZE_ROUTE) {
13795
+ reject(sourceName, trimmedBody, "sentinel_in_access_filter", `a bare \`${sentinel}\` is not allowed in \`#(access_filter)\` — ` + (sentinel === "false" ? "write `#(authorize) false`, which refuses the caller " + "rather than serving them zero rows." : "it filters nothing; delete the annotation, or write " + "`#(authorize) true` to re-open a locked base."));
13796
+ }
13797
+ return [{ scope: sentinel === "false" ? "deny_all" : "admit_all" }];
13798
+ }
13799
+ const { terms: rawTerms, compoundTokens } = splitTerms(trimmedBody);
13800
+ if (compoundTokens.length > 0) {
13801
+ reject(sourceName, trimmedBody, "compound_boolean", `a compound boolean (\`${compoundTokens[0]}\`) is not allowed — ` + "join terms with `and` only.");
13802
+ }
13803
+ const parsed = rawTerms.map((term) => parseTerm(sourceName, trimmedBody, term, givenDeclaredTypes));
13804
+ const ADMITTED_SCOPE = {
13805
+ [AUTHORIZE_ROUTE]: "source_level",
13806
+ [ACCESS_FILTER_ROUTE]: "row_level"
13807
+ };
13808
+ const admitted = ADMITTED_SCOPE[route];
13809
+ const offending = parsed.find((t) => t.scope !== admitted);
13810
+ if (offending) {
13811
+ const inLock = route === AUTHORIZE_ROUTE;
13812
+ reject(sourceName, trimmedBody, inLock ? "row_level_term_in_authorize" : "source_level_term_in_access_filter", inLock ? "a row-level term (field on the left) is not allowed in " + "`#(authorize)`, which decides whether the caller may reach " + "the source at all — move the term to `#(access_filter)`." : "a source-level term (literal on the left) is not allowed in " + "`#(access_filter)`, which decides which rows a caller may " + "see — move the term to `#(authorize)`, which refuses a " + "caller it does not admit rather than serving them zero rows.");
13813
+ }
13814
+ assertAuthorizeGrammarTermsCoherent(sourceName, parsed.map((term) => ({ term, route })));
13815
+ return parsed;
13816
+ }
13817
+ function containsRetiredRouteTag(texts) {
13818
+ for (const text of texts) {
13819
+ const route = noteRoute2(text);
13820
+ if (route !== undefined && RETIRED_ROUTES.includes(route)) {
13821
+ return route;
13822
+ }
13823
+ }
13824
+ return;
13825
+ }
13826
+ function reachesRetiredRouteTagBelow(node, seen = new WeakSet, depth = 0) {
13827
+ if (depth > MAX_RETIRED_ROUTE_WALK_DEPTH) {
13828
+ throw new Error("retired-route IR walk exceeded max depth");
13829
+ }
13830
+ if (node === null || typeof node !== "object")
13831
+ return;
13832
+ if (seen.has(node))
13833
+ return;
13834
+ seen.add(node);
13835
+ if (Array.isArray(node)) {
13836
+ for (const item of node) {
13837
+ const found = reachesRetiredRouteTagBelow(item, seen, depth + 1);
13838
+ if (found)
13839
+ return found;
13840
+ }
13841
+ return;
13842
+ }
13843
+ const record = node;
13844
+ if (depth > 0) {
13845
+ for (const key of ["blockNotes", "notes"]) {
13846
+ const arr = record[key];
13847
+ if (!Array.isArray(arr))
13848
+ continue;
13849
+ const texts = arr.map((n) => typeof n === "string" ? n : n && typeof n === "object" && typeof n.text === "string" ? n.text : undefined).filter((text) => text !== undefined);
13850
+ const found = containsRetiredRouteTag(texts);
13851
+ if (found)
13852
+ return found;
13853
+ }
13854
+ }
13855
+ for (const [key, value] of Object.entries(record)) {
13856
+ if (depth === 0 && key === "annotations")
13857
+ continue;
13858
+ const found = reachesRetiredRouteTagBelow(value, seen, depth + 1);
13859
+ if (found)
13860
+ return found;
13861
+ }
13862
+ return;
13863
+ }
13864
+ var AuthorizeGrammarError, IDENT = "[A-Za-z_][A-Za-z0-9_]*", QUOTED_IDENT = "`[^`]+`", SEGMENT, FIELD_PATH_RE, GIVEN_REF_RE, STRING_LITERAL_RE, NEGATED_OPERATOR_RE, COMPARISON_OPERATOR_RE, GRAMMAR_SUMMARY, IN_OPERATOR_RE, EQ_OPERATOR_RE, RETIRED_ROUTES, MAX_RETIRED_ROUTE_WALK_DEPTH = 200;
13865
+ var init_authorize_grammar = __esm(() => {
13866
+ init_errors();
13867
+ init_authorize_routes();
13868
+ AuthorizeGrammarError = class AuthorizeGrammarError extends ModelCompilationError {
13869
+ rejectionCause;
13870
+ constructor(rejectionCause, message) {
13871
+ super({ message });
13872
+ this.rejectionCause = rejectionCause;
13873
+ }
13874
+ };
13875
+ SEGMENT = `(?:${IDENT}|${QUOTED_IDENT})`;
13876
+ FIELD_PATH_RE = new RegExp(`^${SEGMENT}(?:\\.${SEGMENT})*$`);
13877
+ GIVEN_REF_RE = new RegExp(`^\\$(${IDENT})$`);
13878
+ STRING_LITERAL_RE = /^(?:'(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*")$/;
13879
+ NEGATED_OPERATOR_RE = /!=/gu;
13880
+ COMPARISON_OPERATOR_RE = /(?:>=|<=|>|<)/gu;
13881
+ GRAMMAR_SUMMARY = "An authorize gate only accepts one or more `and`-joined terms, each " + "either `<column> <op> $GIVEN` (a single field or a dotted join " + "path on the left) or `'<literal>' in $GIVEN` (a literal on the " + "left), where <op> is fixed by the given's own arity: `in` for a " + "list-typed given, `=` for a scalar one.";
13882
+ IN_OPERATOR_RE = /\bin\b/giu;
13883
+ EQ_OPERATOR_RE = /=/gu;
13884
+ RETIRED_ROUTES = ["partition"];
13885
+ });
13886
+
13483
13887
  // src/service/gate_dimension.ts
13484
13888
  import {
13485
13889
  isJoined,
@@ -13544,11 +13948,12 @@ function containsNegatedMembership(node, depth = 0) {
13544
13948
  return containsNegatedMembership(n.e, depth + 1);
13545
13949
  return false;
13546
13950
  }
13547
- function validateSourceLineGateGivenUsage(sourceName, struct, refSummary, conditionExpr, modelDef, onWarning) {
13951
+ function validateSourceLineGateGivenUsage(sourceName, route, struct, refSummary, conditionExpr, modelDef, onWarning) {
13952
+ const gate = `#(${route}) gate`;
13548
13953
  const expansion = expandRefSummaryGivenIds(struct, refSummary);
13549
13954
  if (!expansion.ok) {
13550
13955
  throw new ModelCompilationError({
13551
- message: `#(authorize) on source "${sourceName}" references ` + `"${expansion.unresolvedPath}", which could not be resolved to a ` + `field this model can reach. An unresolvable reference is refused, ` + `not treated as referencing no given`
13956
+ message: `The ${gate} on source "${sourceName}" references ` + `"${expansion.unresolvedPath}", which could not be resolved to a ` + `field this model can reach. An unresolvable reference is refused, ` + `not treated as referencing no given`
13552
13957
  });
13553
13958
  }
13554
13959
  const givenIds = expansion.givenIds;
@@ -13556,15 +13961,15 @@ function validateSourceLineGateGivenUsage(sourceName, struct, refSummary, condit
13556
13961
  const given = modelDef.givens?.[id];
13557
13962
  if (given && (given.default !== undefined || given.defaultText !== undefined)) {
13558
13963
  throw new ModelCompilationError({
13559
- message: `#(authorize) on source "${sourceName}" references ` + `\`$${given.name}\`, which is declared with a default. A ` + `caller who supplies no value for \`$${given.name}\` gets ` + `that default, which can admit rows the gate was meant to ` + `exclude. Declare \`$${given.name}\` with no default`
13964
+ message: `The ${gate} on source "${sourceName}" references ` + `\`$${given.name}\`, which is declared with a default. A ` + `caller who supplies no value for \`$${given.name}\` gets ` + `that default, which can admit what the gate was meant to ` + `exclude. Declare \`$${given.name}\` with no default`
13560
13965
  });
13561
13966
  }
13562
13967
  }
13563
13968
  if (givenIds.size === 0) {
13564
- onWarning?.("source_line_gate_no_given_reference", `#(authorize) on source "${sourceName}" references no given; it is ` + `a fixed predicate, not an access rule keyed on the caller`);
13969
+ onWarning?.("source_line_gate_no_given_reference", `The ${gate} on source "${sourceName}" references no given; it is ` + `a fixed predicate, not an access rule keyed on the caller`);
13565
13970
  }
13566
13971
  if (containsNegatedMembership(conditionExpr)) {
13567
- onWarning?.("source_line_gate_negated_membership", `#(authorize) on source "${sourceName}" negates a membership test; ` + `an empty given then matches every row instead of none`);
13972
+ onWarning?.("source_line_gate_negated_membership", `The ${gate} on source "${sourceName}" negates a membership test; ` + `an empty given then matches everything instead of nothing`);
13568
13973
  }
13569
13974
  }
13570
13975
  var init_gate_dimension = __esm(() => {
@@ -13592,10 +13997,10 @@ function resolveDeclaredSource(struct, modelDef) {
13592
13997
  }
13593
13998
  return sawBrokenEntry ? { kind: "unresolvable" } : { kind: "none" };
13594
13999
  }
13595
- function ancestorGateExprs(struct, modelDef, seen = new Set) {
14000
+ function ancestorGateExprs(struct, modelDef, route, seen = new Set) {
13596
14001
  let inherited = struct.annotations?.inherits;
13597
14002
  for (let depth = 0;inherited && depth < ANCESTOR_WALK_MAX_DEPTH; depth++) {
13598
- const exprs2 = collectAuthorizeExprs(ownLevelNoteTexts(inherited));
14003
+ const exprs2 = collectAuthorizeExprsForRoute(ownLevelNoteTexts(inherited), route);
13599
14004
  if (exprs2.length > 0)
13600
14005
  return exprs2;
13601
14006
  inherited = inherited.inherits;
@@ -13603,15 +14008,17 @@ function ancestorGateExprs(struct, modelDef, seen = new Set) {
13603
14008
  if (inherited)
13604
14009
  return ["false"];
13605
14010
  seen.add(struct);
13606
- if (seen.size > ANCESTOR_WALK_MAX_DEPTH)
14011
+ if (seen.size > ANCESTOR_WALK_MAX_DEPTH) {
13607
14012
  return ["false"];
14013
+ }
13608
14014
  const declared = resolveDeclaredSource(struct, modelDef);
13609
- if (declared.kind === "unresolvable")
14015
+ if (declared.kind === "unresolvable") {
13610
14016
  return ["false"];
14017
+ }
13611
14018
  if (declared.kind === "none" || seen.has(declared.source))
13612
14019
  return [];
13613
- const exprs = collectAuthorizeExprs(ownLevelNoteTexts(declared.source.annotations));
13614
- return exprs.length > 0 ? exprs : ancestorGateExprs(declared.source, modelDef, seen);
14020
+ const exprs = collectAuthorizeExprsForRoute(ownLevelNoteTexts(declared.source.annotations), route);
14021
+ return exprs.length > 0 ? exprs : ancestorGateExprs(declared.source, modelDef, route, seen);
13615
14022
  }
13616
14023
  function resolveQuerySourceBase(struct, modelDef) {
13617
14024
  const duck = struct;
@@ -13625,8 +14032,8 @@ function resolveCompositeResolvedBase(struct) {
13625
14032
  const duck = struct;
13626
14033
  return duck.type === "query_source" ? duck.query?.compositeResolvedSourceDef : undefined;
13627
14034
  }
13628
- function effectiveAncestorGateExprs(struct, modelDef, seen = new Set) {
13629
- const direct = ancestorGateExprs(struct, modelDef, new Set(seen));
14035
+ function effectiveAncestorGateExprs(struct, modelDef, route, seen = new Set) {
14036
+ const direct = ancestorGateExprs(struct, modelDef, route, new Set(seen));
13630
14037
  if (direct.length > 0)
13631
14038
  return [direct];
13632
14039
  if (seen.has(struct))
@@ -13636,18 +14043,19 @@ function effectiveAncestorGateExprs(struct, modelDef, seen = new Set) {
13636
14043
  const base = resolveQuerySourceBase(struct, modelDef);
13637
14044
  if (!base) {
13638
14045
  const duck = struct;
13639
- if (duck.type === "query_source")
14046
+ if (duck.type === "query_source") {
13640
14047
  groups.push(["false"]);
14048
+ }
13641
14049
  } else if (!seen.has(base)) {
13642
- const ownExprs = collectAuthorizeExprs(ownLevelNoteTexts(base.annotations));
13643
- groups.push(...ownExprs.length > 0 ? [ownExprs] : effectiveAncestorGateExprs(base, modelDef, seen));
14050
+ const ownExprs = collectAuthorizeExprsForRoute(ownLevelNoteTexts(base.annotations), route);
14051
+ groups.push(...ownExprs.length > 0 ? [ownExprs] : effectiveAncestorGateExprs(base, modelDef, route, seen));
13644
14052
  }
13645
14053
  const composite = resolveCompositeResolvedBase(struct);
13646
14054
  if (composite && !seen.has(composite)) {
13647
14055
  const parentOwnNotes = base ? ownLevelNotes(base.annotations) : [];
13648
14056
  const compositeOwnNotes = ownLevelNotes(composite.annotations).filter((note) => !parentOwnNotes.includes(note));
13649
- const compositeOwn = collectAuthorizeExprs(compositeOwnNotes.map((note) => note.text));
13650
- groups.push(...compositeOwn.length > 0 ? [compositeOwn] : effectiveAncestorGateExprs(composite, modelDef, seen));
14057
+ const compositeOwn = collectAuthorizeExprsForRoute(compositeOwnNotes.map((note) => note.text), route);
14058
+ groups.push(...compositeOwn.length > 0 ? [compositeOwn] : effectiveAncestorGateExprs(composite, modelDef, route, seen));
13651
14059
  }
13652
14060
  return groups;
13653
14061
  }
@@ -13676,120 +14084,9 @@ var init_gate_registry_walk = __esm(() => {
13676
14084
  init_authorize();
13677
14085
  });
13678
14086
 
13679
- // src/service/partition_annotation.ts
13680
- import { payloadOf as payloadOf2, routeOf as routeOf2 } from "@malloydata/malloy";
13681
- function noteRoute2(text) {
13682
- return routeOf2({ value: text.trimStart() });
13683
- }
13684
- function containsPartitionAnnotationTag(texts) {
13685
- return texts.some((text) => noteRoute2(text) === PARTITION_ROUTE);
13686
- }
13687
- function reachesPartitionTagBelow(node, seen = new WeakSet, depth = 0) {
13688
- if (depth > MAX_PARTITION_IR_WALK_DEPTH) {
13689
- throw new Error("partition-marker IR walk exceeded max depth");
13690
- }
13691
- if (node === null || typeof node !== "object")
13692
- return false;
13693
- if (seen.has(node))
13694
- return false;
13695
- seen.add(node);
13696
- if (Array.isArray(node)) {
13697
- return node.some((item) => reachesPartitionTagBelow(item, seen, depth + 1));
13698
- }
13699
- const record = node;
13700
- if (depth > 0 && record.join !== undefined)
13701
- return false;
13702
- if (depth > 0) {
13703
- for (const key of ["blockNotes", "notes"]) {
13704
- const arr = record[key];
13705
- if (!Array.isArray(arr))
13706
- continue;
13707
- const texts = arr.map((n) => typeof n === "string" ? n : n && typeof n === "object" && typeof n.text === "string" ? n.text : undefined).filter((text) => text !== undefined);
13708
- if (containsPartitionAnnotationTag(texts))
13709
- return true;
13710
- }
13711
- }
13712
- return Object.entries(record).some(([key, value]) => depth === 0 && key === "annotations" ? false : reachesPartitionTagBelow(value, seen, depth + 1));
13713
- }
13714
- function notePayload2(text) {
13715
- return payloadOf2({ value: text.trimStart() }) ?? "";
13716
- }
13717
- function rejectionMessage(sourceName, body, detail) {
13718
- return `Source "${sourceName}" declares \`#(partition) ${body}\`: ${detail} ` + `#(partition) only accepts \`<column> = $GIVEN\`, where <column> is a ` + `single field or a dotted join path.`;
13719
- }
13720
- function reject(sourceName, body, cause, detail) {
13721
- throw new PartitionAnnotationError(cause, rejectionMessage(sourceName, body, detail));
13722
- }
13723
- function parsePartitionAnnotation(sourceName, annotationText) {
13724
- if (noteRoute2(annotationText) !== PARTITION_ROUTE)
13725
- return null;
13726
- const body = notePayload2(annotationText).trim();
13727
- if (body.length === 0) {
13728
- reject(sourceName, body, "empty_body", "the expression body is empty.");
13729
- }
13730
- if (COMPOUND_BOOLEAN_RE.test(body)) {
13731
- reject(sourceName, body, "compound_boolean", "a compound boolean (`and`/`or`/`not`) is not allowed — declare one " + "`#(partition)` marker per column.");
13732
- }
13733
- if (IN_OPERATOR_RE.test(body)) {
13734
- reject(sourceName, body, "in_operator", "the `in` operator is not allowed.");
13735
- }
13736
- if (NEGATED_OPERATOR_RE.test(body)) {
13737
- reject(sourceName, body, "negated_operator", "`!=` is not allowed.");
13738
- }
13739
- if (COMPARISON_OPERATOR_RE.test(body)) {
13740
- reject(sourceName, body, "comparison_operator", "only `=` is allowed, not `<`/`>`/`<=`/`>=`.");
13741
- }
13742
- const eq = body.indexOf("=");
13743
- if (eq === -1) {
13744
- reject(sourceName, body, "malformed_body", "no `=` was found.");
13745
- }
13746
- const left = body.slice(0, eq).trim();
13747
- const right = body.slice(eq + 1).trim();
13748
- if (!FIELD_PATH_RE.test(left)) {
13749
- reject(sourceName, body, "left_not_field_path", `\`${left}\` is not a field path — the left side must be a single ` + "column or a dotted join path, not an expression.");
13750
- }
13751
- const givenMatch = GIVEN_REF_RE.exec(right);
13752
- if (!givenMatch) {
13753
- reject(sourceName, body, "missing_given_reference", `\`${right}\` is not a given reference — the right side must be ` + "`$NAME`.");
13754
- }
13755
- return { column: left, given: givenMatch[1] };
13756
- }
13757
- function collectPartitionPairs(sourceName, annotationTexts2) {
13758
- const pairs = [];
13759
- const seenGivens = new Map;
13760
- for (const text of annotationTexts2) {
13761
- const pair = parsePartitionAnnotation(sourceName, text);
13762
- if (pair === null)
13763
- continue;
13764
- const priorColumn = seenGivens.get(pair.given);
13765
- if (priorColumn !== undefined) {
13766
- throw new PartitionAnnotationError("duplicate_given", `Source "${sourceName}" declares \`#(partition)\` on both ` + `\`${priorColumn}\` and \`${pair.column}\` for the same given ` + `\`$${pair.given}\` — a given can back at most one partition ` + `column per source.`);
13767
- }
13768
- seenGivens.set(pair.given, pair.column);
13769
- pairs.push(pair);
13770
- }
13771
- return pairs;
13772
- }
13773
- var PARTITION_ROUTE = "partition", MAX_PARTITION_IR_WALK_DEPTH = 200, PartitionAnnotationError, IDENT = "[A-Za-z_][A-Za-z0-9_]*", FIELD_PATH_RE, GIVEN_REF_RE, COMPOUND_BOOLEAN_RE, IN_OPERATOR_RE, NEGATED_OPERATOR_RE, COMPARISON_OPERATOR_RE;
13774
- var init_partition_annotation = __esm(() => {
13775
- init_errors();
13776
- PartitionAnnotationError = class PartitionAnnotationError extends ModelCompilationError {
13777
- rejectionCause;
13778
- constructor(rejectionCause, message) {
13779
- super({ message });
13780
- this.rejectionCause = rejectionCause;
13781
- }
13782
- };
13783
- FIELD_PATH_RE = new RegExp(`^${IDENT}(?:\\.${IDENT})*$`);
13784
- GIVEN_REF_RE = new RegExp(`^\\$(${IDENT})$`);
13785
- COMPOUND_BOOLEAN_RE = /\b(and|or|not)\b/i;
13786
- IN_OPERATOR_RE = /\bin\b/i;
13787
- NEGATED_OPERATOR_RE = /!=/;
13788
- COMPARISON_OPERATOR_RE = /(>=|<=|>|<)/;
13789
- });
13790
-
13791
14087
  // src/service/gate_classification.ts
13792
14088
  import {
14089
+ isJoined as isJoined2,
13793
14090
  isSourceDef as isSourceDef3
13794
14091
  } from "@malloydata/malloy";
13795
14092
  function createGateClassificationDeps(givens, modelPath) {
@@ -13799,30 +14096,41 @@ function createGateClassificationDeps(givens, modelPath) {
13799
14096
  modelPath
13800
14097
  };
13801
14098
  }
13802
- function gateExprsForOwnAnnotations(struct, modelDef, excludeNotes = []) {
14099
+ function gateExprsForOwnAnnotations(struct, route, modelDef, excludeNotes = []) {
13803
14100
  const ownNotes = ownLevelNotes(struct.annotations).filter((note) => !excludeNotes.includes(note));
13804
14101
  try {
13805
- const own = collectAuthorizeExprs(ownNotes.map((note) => note.text));
14102
+ const own = collectAuthorizeExprsForRoute(ownNotes.map((note) => note.text), route);
13806
14103
  if (own.length > 0) {
13807
14104
  return { exprs: own, fromAncestor: false };
13808
14105
  }
13809
- const ancestor = ancestorGateExprs(struct, modelDef);
14106
+ const ancestor = ancestorGateExprs(struct, modelDef, route);
13810
14107
  return { exprs: ancestor, fromAncestor: ancestor.length > 0 };
13811
14108
  } catch {
13812
- return { exprs: ["false"], fromAncestor: false };
14109
+ return {
14110
+ exprs: ["false"],
14111
+ fromAncestor: false
14112
+ };
13813
14113
  }
13814
14114
  }
13815
14115
  function collectEntryPointGates(struct, modelDef, seen = new Set, treatAsOwnGate = false, entryPointStruct = struct, excludeNotes = []) {
14116
+ const results = [];
14117
+ for (const route of CANONICAL_AUTHORIZE_ROUTES) {
14118
+ results.push(...collectEntryPointGatesForRoute(struct, modelDef, route, new Set(seen), treatAsOwnGate, entryPointStruct, excludeNotes));
14119
+ }
14120
+ return results;
14121
+ }
14122
+ function collectEntryPointGatesForRoute(struct, modelDef, route, seen, treatAsOwnGate, entryPointStruct, excludeNotes) {
13816
14123
  if (!struct || !modelDef || seen.has(struct))
13817
14124
  return [];
13818
14125
  seen.add(struct);
13819
14126
  const results = [];
13820
14127
  const label = struct.as ?? struct.name;
13821
- const { exprs: ownExprs, fromAncestor } = gateExprsForOwnAnnotations(struct, modelDef, excludeNotes);
14128
+ const { exprs: ownExprs, fromAncestor } = gateExprsForOwnAnnotations(struct, route, modelDef, excludeNotes);
13822
14129
  if (ownExprs.length > 0) {
13823
14130
  results.push({
13824
14131
  label,
13825
14132
  exprs: ownExprs,
14133
+ route,
13826
14134
  selfContained: fromAncestor || !treatAsOwnGate,
13827
14135
  struct: entryPointStruct
13828
14136
  });
@@ -13831,17 +14139,18 @@ function collectEntryPointGates(struct, modelDef, seen = new Set, treatAsOwnGate
13831
14139
  if (duck.type === "query_source") {
13832
14140
  const base = resolveQuerySourceBase(struct, modelDef);
13833
14141
  if (base) {
13834
- results.push(...collectEntryPointGates(base, modelDef, seen, false, entryPointStruct));
14142
+ results.push(...collectEntryPointGatesForRoute(base, modelDef, route, seen, false, entryPointStruct, []));
13835
14143
  } else {
13836
14144
  results.push({
13837
14145
  label,
13838
14146
  exprs: ["false"],
14147
+ route,
13839
14148
  selfContained: true
13840
14149
  });
13841
14150
  }
13842
14151
  const resolved = duck.query?.compositeResolvedSourceDef;
13843
14152
  if (resolved) {
13844
- results.push(...collectEntryPointGates(resolved, modelDef, seen, false, entryPointStruct, base ? ownLevelNotes(base.annotations) : []));
14153
+ results.push(...collectEntryPointGatesForRoute(resolved, modelDef, route, seen, false, entryPointStruct, base ? ownLevelNotes(base.annotations) : []));
13845
14154
  }
13846
14155
  }
13847
14156
  return results;
@@ -13858,7 +14167,7 @@ async function resolveGateShape(entry, originModelDef, graftScope, deps) {
13858
14167
  return { shape: "rejected" };
13859
14168
  }
13860
14169
  const filterText = gateFilterText(entry.exprs);
13861
- const cacheKey = `${graftScope.cacheScope}\x00${graftTarget}\x00${filterText}`;
14170
+ const cacheKey = `${graftScope.cacheScope}\x00${entry.route}\x00${graftTarget}\x00${filterText}`;
13862
14171
  let cached = deps.gateShapeCache.get(cacheKey);
13863
14172
  if (!cached) {
13864
14173
  let condition;
@@ -13881,7 +14190,11 @@ async function resolveGateShape(entry, originModelDef, graftScope, deps) {
13881
14190
  detail: "this entry's lifted condition carries no usable expression"
13882
14191
  };
13883
14192
  } else if (isBareFalseLiteral(condition.e)) {
13884
- classification = { shape: "row_level", givenNames: [] };
14193
+ classification = {
14194
+ shape: "row_level",
14195
+ givenNames: [],
14196
+ givenNamesById: new Map
14197
+ };
13885
14198
  } else {
13886
14199
  const targetStruct = graftScope.modelDef.contents[graftTarget];
13887
14200
  if (!isSourceDef3(targetStruct)) {
@@ -13899,7 +14212,13 @@ async function resolveGateShape(entry, originModelDef, graftScope, deps) {
13899
14212
  detail: `this gate references \`${expansion.unresolvedPath}\`, which could not be resolved on the graft target`
13900
14213
  };
13901
14214
  } else {
13902
- const givenNames = Array.from(expansion.givenIds).map((id) => graftScope.modelDef.givens?.[id]?.name).filter((name) => !!name);
14215
+ const givenNamesById = new Map;
14216
+ for (const id of expansion.givenIds) {
14217
+ const name = graftScope.modelDef.givens?.[id]?.name;
14218
+ if (name)
14219
+ givenNamesById.set(id, name);
14220
+ }
14221
+ const givenNames = Array.from(givenNamesById.values());
13903
14222
  const literalNames = referencedGivenNames(filterText);
13904
14223
  const accountedFor = new Set(givenNames);
13905
14224
  const unaccounted = literalNames.filter((name) => !accountedFor.has(name));
@@ -13910,7 +14229,11 @@ async function resolveGateShape(entry, originModelDef, graftScope, deps) {
13910
14229
  detail: "this gate references a given id that does not resolve to a name on this model"
13911
14230
  };
13912
14231
  } else {
13913
- classification = { shape: "row_level", givenNames };
14232
+ classification = {
14233
+ shape: "row_level",
14234
+ givenNames,
14235
+ givenNamesById
14236
+ };
13914
14237
  }
13915
14238
  }
13916
14239
  }
@@ -13936,6 +14259,14 @@ async function resolveGateShape(entry, originModelDef, graftScope, deps) {
13936
14259
  if (cached.classification.shape === "rejected") {
13937
14260
  return { shape: "rejected", cause: cached.classification.cause };
13938
14261
  }
14262
+ if (entry.route === AUTHORIZE_ROUTE) {
14263
+ return {
14264
+ shape: "lock",
14265
+ condition: cached.condition,
14266
+ givenNames: cached.classification.givenNames,
14267
+ givenNamesById: cached.classification.givenNamesById
14268
+ };
14269
+ }
13939
14270
  return {
13940
14271
  shape: "row_level",
13941
14272
  graftTarget,
@@ -14018,142 +14349,120 @@ function isBareFalseLiteral(expr) {
14018
14349
  function computeGivenDeclaredTypes(givens) {
14019
14350
  return new Map((givens ?? []).filter((g) => g.name != null && g.type != null).map((g) => [g.name, g.type]));
14020
14351
  }
14021
- function resolveEntryPointPartitions(struct, modelDef, seen = new Set) {
14022
- if (!struct || !modelDef || seen.has(struct))
14023
- return [];
14024
- seen.add(struct);
14025
- const label = struct.as ?? struct.name;
14026
- const own = collectPartitionPairs(label, ownLevelNotes(struct.annotations).map((note) => note.text));
14027
- if (own.length > 0)
14028
- return own;
14029
- let inherited = struct.annotations?.inherits;
14030
- for (let depth = 0;inherited && depth < ANCESTOR_WALK_MAX_DEPTH; depth++) {
14031
- const pairs = collectPartitionPairs(label, ownLevelNoteTexts(inherited));
14032
- if (pairs.length > 0)
14033
- return pairs;
14034
- inherited = inherited.inherits;
14035
- }
14036
- if (inherited)
14037
- throw unresolvableAncestry(label);
14038
- const declared = resolveDeclaredSource(struct, modelDef);
14039
- if (declared.kind === "unresolvable")
14040
- throw unresolvableAncestry(label);
14041
- if (declared.kind === "resolved") {
14042
- const pairs = resolveEntryPointPartitions(declared.source, modelDef, seen);
14043
- if (pairs.length > 0)
14044
- return pairs;
14045
- }
14046
- const queryBase = resolveQuerySourceBase(struct, modelDef);
14047
- if (queryBase)
14048
- return resolveEntryPointPartitions(queryBase, modelDef, seen);
14049
- return [];
14050
- }
14051
- function unresolvableAncestry(label) {
14052
- return new PartitionAnnotationError("ancestry_unresolvable", `Could not resolve whether source "${label}" carries a ` + `\`#(partition)\` marker: the derivation chain it inherits from ` + `could not be read. Denying rather than serving unfiltered.`);
14053
- }
14054
- function assertPartitionAnnotationsValid(modelDef) {
14352
+ function assertAuthorizeGrammarValid(modelDef, authorizeMap, authorizeOwnNotes, givenDeclaredTypes, onAdmitAllGate, attributedAuthorizeOwnNotes) {
14055
14353
  if (!modelDef)
14056
14354
  return;
14057
- for (const [key, obj] of Object.entries(modelDef.contents)) {
14058
- if (!isSourceDef3(obj))
14059
- continue;
14060
- const label = obj.as ?? obj.name ?? key;
14061
- if (obj.type !== "composite") {
14062
- let resolved = [];
14063
- try {
14064
- resolved = resolveEntryPointPartitions(obj, modelDef);
14065
- } catch (err) {
14066
- if (!(err instanceof PartitionAnnotationError) || err.rejectionCause !== "ancestry_unresolvable") {
14067
- throw err;
14355
+ for (const [sourceName, groups] of authorizeMap) {
14356
+ const struct = modelDef.contents[sourceName];
14357
+ const ownGroupTerms = [];
14358
+ const declaredAdmitAllRoutes = [];
14359
+ for (const { route, exprs } of groups) {
14360
+ const isOwn = (authorizeOwnNotes.get(sourceName)?.get(route)?.length ?? 0) > 0;
14361
+ const filteredExprs = exprs.filter((expr) => (isOwn || expr !== "false") && !isLegacyQuotedPayload(expr));
14362
+ const groupTerms = [];
14363
+ for (const expr of filteredExprs) {
14364
+ const terms = parseAuthorizeGrammarBody(sourceName, expr, givenDeclaredTypes, route);
14365
+ for (const term of terms) {
14366
+ groupTerms.push({ term, route });
14367
+ if (term.scope === "admit_all" && (attributedAuthorizeOwnNotes?.get(sourceName)?.get(route)?.length ?? 0) > 0) {
14368
+ declaredAdmitAllRoutes.push(route);
14369
+ }
14370
+ if (term.scope === "row_level" && isSourceDef3(struct)) {
14371
+ assertNoFanoutFieldPath(sourceName, struct, term.fieldPath, term.fieldPathSegments);
14372
+ }
14068
14373
  }
14069
- continue;
14070
14374
  }
14071
- assertNoUnreachableMarker(obj, label, resolved);
14072
- continue;
14375
+ if (isOwn) {
14376
+ ownGroupTerms.push(...groupTerms);
14377
+ } else {
14378
+ assertAuthorizeGrammarTermsCoherent(sourceName, groupTerms);
14379
+ }
14073
14380
  }
14074
- if (resolveEntryPointPartitions(obj, modelDef).length > 0) {
14075
- throw new PartitionAnnotationError("partitioned_composite", `Source "${label}" is a composite source (\`compose(...)\`) and ` + `also declares \`#(partition)\` (on itself or an ancestor it ` + `derives from). A composite run target compiles each query ` + `against one resolved member branch, which a partition filter ` + `cannot attach to — declare \`#(partition)\` on a non-composite ` + `source instead.`);
14381
+ if (ownGroupTerms.length > 0) {
14382
+ assertAuthorizeGrammarTermsCoherent(sourceName, ownGroupTerms);
14076
14383
  }
14077
- const marked = partitionedMemberLabel(obj, modelDef);
14078
- if (marked !== undefined) {
14079
- throw new PartitionAnnotationError("partitioned_composite", `Source "${label}" is a composite source (\`compose(...)\`) with ` + `member "${marked}", which declares \`#(partition)\`. MEASURED: ` + `querying the composite reads EVERY partition of that member — ` + `the graft lands on the composite's own struct, not the ` + `resolved member branch, so no filter reaches the executed ` + `query. Remove the composite, or drop \`#(partition)\` from ` + `"${marked}" and expose it as its own source.`);
14384
+ for (const route of declaredAdmitAllRoutes) {
14385
+ onAdmitAllGate?.(sourceName, route);
14080
14386
  }
14081
14387
  }
14082
14388
  }
14083
- function assertNoUnreachableMarker(struct, label, resolved) {
14084
- if (resolved.length > 0)
14085
- return;
14086
- let reachesBelow;
14087
- try {
14088
- reachesBelow = reachesPartitionTagBelow(struct);
14089
- } catch {
14090
- reachesBelow = true;
14091
- }
14092
- if (!reachesBelow)
14093
- return;
14094
- throw new PartitionAnnotationError("marker_unreachable", `Source "${label}" carries a \`#(partition)\` marker that is not ` + `declared on the source itself — it sits on a field, a view, or an ` + `inline \`compose(...)\` inside it. Nothing would filter this ` + `source: every caller would read every partition. Move the marker ` + `onto the \`source:\` declaration.`);
14095
- }
14096
- function partitionedMemberLabel(composite, modelDef, seen = new Set) {
14097
- if (seen.has(composite))
14098
- return;
14099
- seen.add(composite);
14100
- for (const member of composite.sources ?? []) {
14101
- const label = member.as ?? member.name;
14102
- if (resolveEntryPointPartitions(member, modelDef).length > 0) {
14103
- return label;
14389
+ function fanoutSegment(struct, path) {
14390
+ let current = struct;
14391
+ for (let i = 0;i < path.length - 1; i++) {
14392
+ const seg = path[i];
14393
+ const field = (current.fields ?? []).find((f) => (f.as ?? f.name) === seg);
14394
+ if (!field)
14395
+ return { segment: seg, reason: "unresolved" };
14396
+ if (isJoined2(field) && field.join !== "one") {
14397
+ return { segment: seg, reason: "fanout" };
14104
14398
  }
14105
- if (member.type === "composite") {
14106
- const nested = partitionedMemberLabel(member, modelDef, seen);
14107
- if (nested !== undefined)
14108
- return nested;
14399
+ if (!isSourceDef3(field) && field.type !== "record" && field.type !== "array") {
14400
+ return { segment: seg, reason: "unresolved" };
14109
14401
  }
14402
+ current = field;
14110
14403
  }
14111
14404
  return;
14112
14405
  }
14113
- async function resolvePartitionGraftEntries(struct, originModelDef, graftScope, deps) {
14114
- if (!struct || !originModelDef)
14115
- return [];
14116
- const pairs = resolveEntryPointPartitions(struct, originModelDef);
14117
- if (pairs.length === 0)
14118
- return [];
14119
- const label = struct.as ?? struct.name;
14120
- if (!graftScope) {
14121
- logger.debug("Partition filter has no graft scope to attach to; denying", { modelPath: deps.modelPath, label });
14122
- throw new Error(`partition on "${label}" has no graft scope`);
14123
- }
14124
- const graftTarget = resolveGraftTarget(struct, originModelDef, graftScope.modelDef);
14125
- if (!graftTarget) {
14126
- logger.debug("Partition filter resolved to no graft target; denying", {
14127
- modelPath: deps.modelPath,
14128
- label
14129
- });
14130
- throw new Error(`partition on "${label}" resolved to no graft target`);
14406
+ function assertNoFanoutFieldPath(sourceName, struct, fieldPath, fieldPathSegments) {
14407
+ const finding = fanoutSegment(struct, fieldPathSegments);
14408
+ if (finding === undefined)
14409
+ return;
14410
+ const detail = finding.reason === "fanout" ? `which reaches through \`${finding.segment}\`, a fan-out join ` + `(\`join_many\`/\`join_cross\`, or a repeated record) — the ` + `entry row has many values for what follows, so there is no ` + `single key to compare against a given. Gate on a field reached ` + `only through \`join_one\` joins.` : `whose segment \`${finding.segment}\` does not resolve against ` + `this source — refused rather than assumed safe.`;
14411
+ throw new AuthorizeGrammarError("fanout_path", `Source "${sourceName}" declares an authorize gate on \`${fieldPath}\`, ${detail}`);
14412
+ }
14413
+ function collectRetiredRouteMarkers(modelDef) {
14414
+ const found = [];
14415
+ if (!modelDef)
14416
+ return found;
14417
+ const fileMatch = containsRetiredRouteTag(ownLevelNoteTexts(modelAnnotations(modelDef)));
14418
+ if (fileMatch) {
14419
+ found.push(`the model itself declares a retired \`##(${fileMatch})\` annotation`);
14131
14420
  }
14132
- const entries = [];
14133
- for (const pair of pairs) {
14134
- if (!deps.givenDeclaredTypes.has(pair.given)) {
14135
- logger.warn("Partition references a given off the model surface; denying", { modelPath: deps.modelPath, graftTarget, givenName: pair.given });
14136
- throw new Error(`partition on "${label}" references \`$${pair.given}\`, which is not on this model's given surface`);
14421
+ for (const [key, obj] of Object.entries(modelDef.contents)) {
14422
+ const isQuery = obj.type === "query";
14423
+ if (!isSourceDef3(obj) && !isQuery)
14424
+ continue;
14425
+ const label = obj.as ?? obj.name ?? key;
14426
+ const noun = isQuery ? "query" : "source";
14427
+ const ownMatch = containsRetiredRouteTag(annotationTexts(obj.annotations) ?? []);
14428
+ if (ownMatch) {
14429
+ found.push(`${noun} "${label}" declares a retired \`#(${ownMatch})\` annotation`);
14430
+ continue;
14431
+ }
14432
+ if (isQuery) {
14433
+ continue;
14434
+ }
14435
+ let belowMatch;
14436
+ try {
14437
+ belowMatch = reachesRetiredRouteTagBelow(obj);
14438
+ } catch {
14439
+ belowMatch = RETIRED_ROUTES[0];
14440
+ }
14441
+ if (belowMatch) {
14442
+ found.push(`source "${label}" carries a retired \`#(${belowMatch})\` ` + `annotation somewhere in its IR (not on the source line itself)`);
14137
14443
  }
14138
- const filterText = `${pair.column} = $${pair.given}`;
14139
- const condition = await liftGateCondition(graftTarget, filterText, graftScope.materializer);
14140
- entries.push({
14141
- label,
14142
- graftTarget,
14143
- filterText,
14144
- condition,
14145
- givenNames: [pair.given]
14146
- });
14147
14444
  }
14148
- return entries;
14445
+ return found;
14446
+ }
14447
+ function assertNoRetiredRouteMarkers(found) {
14448
+ if (found.length === 0)
14449
+ return;
14450
+ throw new ModelCompilationError({
14451
+ message: `These sources carry a marker from a retired annotation route:
14452
+ ` + `${found.map((f) => ` - ${f}`).join(`
14453
+ `)}
14454
+ ` + `The route is no longer enforced by anything, so Malloy would ` + `otherwise parse it, load the model clean, and serve every row as ` + `if the marker were never written. Remove the annotation.`
14455
+ });
14149
14456
  }
14150
14457
  var init_gate_classification = __esm(() => {
14458
+ init_errors();
14151
14459
  init_logger();
14152
14460
  init_annotations();
14153
14461
  init_authorize();
14462
+ init_authorize_grammar();
14463
+ init_authorize_routes();
14154
14464
  init_gate_dimension();
14155
14465
  init_gate_registry_walk();
14156
- init_partition_annotation();
14157
14466
  });
14158
14467
 
14159
14468
  // src/service/filter.ts
@@ -14350,7 +14659,7 @@ var init_filter = __esm(() => {
14350
14659
 
14351
14660
  // src/service/source_extraction.ts
14352
14661
  import {
14353
- isJoined as isJoined2,
14662
+ isJoined as isJoined3,
14354
14663
  isSourceDef as isSourceDef4,
14355
14664
  modelDefToModelInfo
14356
14665
  } from "@malloydata/malloy";
@@ -14404,7 +14713,7 @@ function extractSourcesFromModelDef(modelDef, givens, onParseError) {
14404
14713
  considerNoteOwner(authorizeNoteDeclaredBy, note, struct);
14405
14714
  }
14406
14715
  }
14407
- nearMissAuthorize.push(...collectAuthorizeNearMisses([
14716
+ nearMissAuthorize.push(...collectAuthorizeNearMissesAllRoutes([
14408
14717
  ...ownLevelNotes(struct.annotations),
14409
14718
  ...struct.fields.flatMap((field) => ownLevelNotes(field.annotations))
14410
14719
  ].map((note) => note.text)));
@@ -14422,23 +14731,29 @@ function extractSourcesFromModelDef(modelDef, givens, onParseError) {
14422
14731
  considerNoteOwner(authorizeNoteDeclaredBy, note, entry);
14423
14732
  }
14424
14733
  }
14425
- nearMissAuthorize.push(...collectAuthorizeNearMisses(ownLevelNotes(entry.annotations).map((note) => note.text)));
14734
+ nearMissAuthorize.push(...collectAuthorizeNearMissesAllRoutes(ownLevelNotes(entry.annotations).map((note) => note.text)));
14426
14735
  }
14427
14736
  for (const struct of derivedStructsReachable(sweptStructs, modelDef)) {
14428
- nearMissAuthorize.push(...collectAuthorizeNearMisses([
14737
+ nearMissAuthorize.push(...collectAuthorizeNearMissesAllRoutes([
14429
14738
  ...ownLevelNotes(struct.annotations),
14430
14739
  ...struct.fields.flatMap((field) => ownLevelNotes(field.annotations))
14431
14740
  ].map((note) => note.text)));
14432
14741
  }
14433
14742
  {
14434
14743
  const folded = modelAnnotations(modelDef);
14435
- nearMissAuthorize.push(...collectAuthorizeNearMisses([...folded.notes ?? [], ...folded.blockNotes ?? []].map((note) => note.text)));
14744
+ nearMissAuthorize.push(...collectAuthorizeNearMissesAllRoutes([...folded.notes ?? [], ...folded.blockNotes ?? []].map((note) => note.text)));
14436
14745
  }
14437
14746
  assertNoAuthorizeNearMisses(nearMissAuthorize);
14438
14747
  const authorizeOwnNotes = new Map;
14439
14748
  const attributedAuthorizeOwnNotes = new Map;
14440
- if (containsAuthorizeAnnotationTag((modelAnnotations(modelDef).notes ?? []).map((note) => note.text))) {
14441
- misplacedAuthorize.push({ kind: "file" });
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
+ });
14756
+ }
14442
14757
  }
14443
14758
  const sources = Object.values(modelDef.contents).filter((obj) => isSourceDef4(obj)).map((sourceObj) => {
14444
14759
  const struct = sourceObj;
@@ -14477,17 +14792,32 @@ function extractSourcesFromModelDef(modelDef, givens, onParseError) {
14477
14792
  }
14478
14793
  }
14479
14794
  const ownNotes = ownLevelNoteTexts(struct.annotations);
14480
- const ownGates = collectAuthorizeExprs(ownNotes);
14481
14795
  const ownAuthorizeNotes = ownLevelNotes(struct.annotations).filter((note) => containsAuthorizeAnnotationTag([note.text]));
14482
- authorizeOwnNotes.set(sourceName, ownAuthorizeNotes);
14483
- attributedAuthorizeOwnNotes.set(sourceName, ownAuthorizeNotes.filter((note) => authorizeNoteDeclaredBy.get(note) === struct));
14484
- const inheritedGroups = ownGates.length === 0 ? effectiveAncestorGateExprs(struct, modelDef) : [];
14485
- const effectiveGroups = ownGates.length > 0 ? [ownGates] : inheritedGroups;
14486
- let authorize;
14487
- if (effectiveGroups.length > 0) {
14488
- authorizeMap.set(sourceName, effectiveGroups);
14489
- authorize = effectiveGroups.flat();
14490
- }
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));
14802
+ }
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);
14814
+ }
14815
+ if (groupsForSource.length > 0) {
14816
+ authorizeMap.set(sourceName, groupsForSource);
14817
+ }
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));
14491
14821
  const views = struct.fields.filter((field) => field.type === "turtle").filter((turtle) => turtle.pipeline.map((stage) => stage.type).every((type) => type === "reduce")).map((turtle) => ({
14492
14822
  name: turtle.as || turtle.name,
14493
14823
  annotations: annotationTexts(turtle.annotations)
@@ -14500,7 +14830,7 @@ function extractSourcesFromModelDef(modelDef, givens, onParseError) {
14500
14830
  continue;
14501
14831
  }
14502
14832
  const fieldName = field.as || field.name;
14503
- if (isJoined2(field) && isSourceDef4(field)) {
14833
+ if (isJoined3(field) && isSourceDef4(field)) {
14504
14834
  const joinedStruct = field;
14505
14835
  if (joinFieldNamesUnresolvableDeclaration(joinedStruct, modelDef)) {
14506
14836
  continue;
@@ -14508,14 +14838,16 @@ function extractSourcesFromModelDef(modelDef, givens, onParseError) {
14508
14838
  misplacedAuthorize.push({
14509
14839
  kind: "field",
14510
14840
  name: sourceName,
14511
- fieldName
14841
+ fieldName,
14842
+ route: fieldAuthorizeNotes.map((note) => authorizeAnnotationRoute(note.text)).find((r) => r !== undefined) ?? ACCESS_FILTER_ROUTE
14512
14843
  });
14513
14844
  continue;
14514
14845
  }
14515
14846
  misplacedAuthorize.push({
14516
14847
  kind: "field",
14517
14848
  name: sourceName,
14518
- fieldName
14849
+ fieldName,
14850
+ route: fieldAuthorizeNotes.map((note) => authorizeAnnotationRoute(note.text)).find((r) => r !== undefined) ?? ACCESS_FILTER_ROUTE
14519
14851
  });
14520
14852
  }
14521
14853
  return {
@@ -14524,7 +14856,8 @@ function extractSourcesFromModelDef(modelDef, givens, onParseError) {
14524
14856
  views,
14525
14857
  filters,
14526
14858
  givens,
14527
- authorize
14859
+ authorize,
14860
+ accessFilter
14528
14861
  };
14529
14862
  });
14530
14863
  return {
@@ -14541,7 +14874,8 @@ function extractQueriesFromModelDef(modelDef) {
14541
14874
  const namedQueries = Object.values(modelDef.contents).filter(isNamedQuery);
14542
14875
  const misplacedAuthorize = namedQueries.filter((queryObj) => containsAuthorizeAnnotationTag(ownLevelNoteTexts(queryObj.annotations))).map((queryObj) => ({
14543
14876
  kind: "query",
14544
- name: queryObj.as || queryObj.name
14877
+ name: queryObj.as || queryObj.name,
14878
+ route: ownLevelNoteTexts(queryObj.annotations).map((text) => authorizeAnnotationRoute(text)).find((r) => r !== undefined) ?? ACCESS_FILTER_ROUTE
14545
14879
  }));
14546
14880
  const queries = namedQueries.map((queryObj) => ({
14547
14881
  name: queryObj.as || queryObj.name,
@@ -14553,6 +14887,7 @@ function extractQueriesFromModelDef(modelDef) {
14553
14887
  var init_source_extraction = __esm(() => {
14554
14888
  init_annotations();
14555
14889
  init_authorize();
14890
+ init_authorize_routes();
14556
14891
  init_filter();
14557
14892
  init_gate_registry_walk();
14558
14893
  });
@@ -14893,7 +15228,7 @@ function readGivenControlSpec(annotationTexts2) {
14893
15228
  }
14894
15229
  function malloyGivenToApi(given) {
14895
15230
  const type = given.type;
14896
- const renderedType = type.type === "filter expression" ? `filter<${type.filterType}>` : type.type;
15231
+ const renderedType = type.type === "filter expression" ? `filter<${type.filterType}>` : type.type === "array" && type.elementTypeDef?.type ? `${type.elementTypeDef.type}[]` : type.type;
14897
15232
  const allNotes = given.annotations.forRoute(undefined);
14898
15233
  return {
14899
15234
  name: given.name,
@@ -14918,7 +15253,13 @@ function collectGivenRefs(value, into) {
14918
15253
  for (const child of Object.values(node))
14919
15254
  collectGivenRefs(child, into);
14920
15255
  }
14921
- function suggestGivenLookup(modelDef, authorizeBySource, surfaced) {
15256
+ function gateGivenSource(sources, name) {
15257
+ const source = sources.find((candidate) => candidate.name === name);
15258
+ if (!source)
15259
+ return;
15260
+ return [...source.authorize ?? [], ...source.accessFilter ?? []];
15261
+ }
15262
+ function suggestGivenLookup(modelDef, gatesBySource, surfaced) {
14922
15263
  const registry = modelDef.givens ?? {};
14923
15264
  const bySource = new Map;
14924
15265
  const byQuery = new Map;
@@ -14927,7 +15268,7 @@ function suggestGivenLookup(modelDef, authorizeBySource, surfaced) {
14927
15268
  const name = obj.as || obj.name;
14928
15269
  const refs = new Set;
14929
15270
  collectGivenRefs(obj.filterList, refs);
14930
- for (const expr of authorizeBySource(name) ?? []) {
15271
+ for (const expr of gatesBySource(name) ?? []) {
14931
15272
  for (const given of referencedGivenNames(expr))
14932
15273
  refs.add(given);
14933
15274
  }
@@ -15441,7 +15782,7 @@ function authorizeWarningCollector() {
15441
15782
  return {
15442
15783
  warnings,
15443
15784
  onRowLevelGateUnexpressible: (sourceName, detail) => {
15444
- warnings.push(`Row-level #(authorize) gate not expressible at entry point "${sourceName}"; every query against it will be denied: ${detail}`);
15785
+ warnings.push(`Row-level #(access_filter) gate not expressible at entry point "${sourceName}"; every query against it will be denied: ${detail}`);
15445
15786
  }
15446
15787
  };
15447
15788
  }
@@ -15500,10 +15841,10 @@ async function compileMalloyModel(job, malloyConfig, modelPath) {
15500
15841
  authorizeOwnNotes,
15501
15842
  attributedAuthorizeOwnNotes
15502
15843
  } = extractSources(modelDef, givens);
15503
- attachSuggestGivenNames(givens, suggestGivenLookup(modelDef, (name) => sources.find((source) => source.name === name)?.authorize, new Set((givens ?? []).map((given) => given.name))));
15844
+ attachSuggestGivenNames(givens, suggestGivenLookup(modelDef, (name) => gateGivenSource(sources, name), new Set((givens ?? []).map((given) => given.name))));
15504
15845
  const queryResult = extractQueries(modelDef);
15505
15846
  const queries = queryResult.queries;
15506
- assertPartitionAnnotationsValid(modelDef);
15847
+ assertNoRetiredRouteMarkers(collectRetiredRouteMarkers(modelDef));
15507
15848
  assertNoMisplacedAuthorizeAnnotations([
15508
15849
  ...misplacedAuthorize,
15509
15850
  ...queryResult.misplacedAuthorize
@@ -15511,20 +15852,20 @@ async function compileMalloyModel(job, malloyConfig, modelPath) {
15511
15852
  const legacyStringGates = findLegacyStringGates(authorizeOwnNotes);
15512
15853
  legacyStringGates.forEach(() => recordRowLevelGateRejected("legacy_string_gate"));
15513
15854
  assertNoLegacyStringGate(legacyStringGates);
15514
- assertAtMostOneAuthorizeGate(findMultipleAuthorizeGates(authorizeOwnNotes));
15855
+ assertAuthorizeGrammarValid(modelDef, authorizeMap, authorizeOwnNotes, computeGivenDeclaredTypes(givens), (_sourceName, route) => recordAuthorizeAdmitAllGate(route), attributedAuthorizeOwnNotes);
15515
15856
  const authorizeWarningCollection = authorizeWarningCollector();
15516
15857
  await validateAuthorizeProbes(mm, {
15517
15858
  authorizeMap,
15518
15859
  authorizeOwnNotes: attributedAuthorizeOwnNotes,
15519
15860
  onRowLevelGateRejected: recordRowLevelGateRejected,
15520
15861
  onRowLevelGateUnexpressible: authorizeWarningCollection.onRowLevelGateUnexpressible,
15521
- onOwnRowLevelConditionCompiled: (sourceName, condition) => {
15862
+ onOwnRowLevelConditionCompiled: (sourceName, condition, route) => {
15522
15863
  const struct = modelDef.contents[sourceName];
15523
15864
  if (!struct || !isSourceDef6(struct))
15524
15865
  return;
15525
- validateSourceLineGateGivenUsage(sourceName, struct, condition.refSummary, condition.e, modelDef, (cause, detail) => {
15866
+ validateSourceLineGateGivenUsage(sourceName, route, struct, condition.refSummary, condition.e, modelDef, (cause, detail) => {
15526
15867
  recordRowLevelGateRejected(cause);
15527
- authorizeWarningCollection.warnings.push(`Row-level #(authorize) gate warning on "${sourceName}" (${cause}): ${detail}`);
15868
+ authorizeWarningCollection.warnings.push(`#(${route}) gate warning on "${sourceName}" (${cause}): ${detail}`);
15528
15869
  });
15529
15870
  }
15530
15871
  });
@@ -15624,11 +15965,11 @@ async function compileNotebookModel(job, malloyConfig, modelPath) {
15624
15965
  finalSourceInfos = collectSourceInfos(finalModelDef);
15625
15966
  const extracted = extractSources(finalModelDef, finalGivens);
15626
15967
  finalSources = extracted.sources;
15627
- attachSuggestGivenNames(finalGivens, suggestGivenLookup(finalModelDef, (name) => extracted.sources.find((source) => source.name === name)?.authorize, new Set((finalGivens ?? []).map((given) => given.name))));
15968
+ attachSuggestGivenNames(finalGivens, suggestGivenLookup(finalModelDef, (name) => gateGivenSource(extracted.sources, name), new Set((finalGivens ?? []).map((given) => given.name))));
15628
15969
  finalFilterMap = extracted.filterMap;
15629
15970
  const finalQueryResult = extractQueries(finalModelDef);
15630
15971
  finalQueries = finalQueryResult.queries;
15631
- assertPartitionAnnotationsValid(finalModelDef);
15972
+ assertNoRetiredRouteMarkers(collectRetiredRouteMarkers(finalModelDef));
15632
15973
  assertNoMisplacedAuthorizeAnnotations([
15633
15974
  ...extracted.misplacedAuthorize,
15634
15975
  ...finalQueryResult.misplacedAuthorize
@@ -15636,20 +15977,20 @@ async function compileNotebookModel(job, malloyConfig, modelPath) {
15636
15977
  const finalLegacyStringGates = findLegacyStringGates(extracted.authorizeOwnNotes);
15637
15978
  finalLegacyStringGates.forEach(() => recordRowLevelGateRejected("legacy_string_gate"));
15638
15979
  assertNoLegacyStringGate(finalLegacyStringGates);
15639
- assertAtMostOneAuthorizeGate(findMultipleAuthorizeGates(extracted.authorizeOwnNotes));
15980
+ assertAuthorizeGrammarValid(finalModelDef, extracted.authorizeMap, extracted.authorizeOwnNotes, computeGivenDeclaredTypes(finalGivens), (_sourceName, route) => recordAuthorizeAdmitAllGate(route), extracted.attributedAuthorizeOwnNotes);
15640
15981
  const finalCompiledModelDef = finalModelDef;
15641
15982
  await validateAuthorizeProbes(mm, {
15642
15983
  authorizeMap: extracted.authorizeMap,
15643
15984
  authorizeOwnNotes: extracted.attributedAuthorizeOwnNotes,
15644
15985
  onRowLevelGateRejected: recordRowLevelGateRejected,
15645
15986
  onRowLevelGateUnexpressible: authorizeWarningCollection.onRowLevelGateUnexpressible,
15646
- onOwnRowLevelConditionCompiled: (sourceName, condition) => {
15987
+ onOwnRowLevelConditionCompiled: (sourceName, condition, route) => {
15647
15988
  const struct = finalCompiledModelDef.contents[sourceName];
15648
15989
  if (!struct || !isSourceDef6(struct))
15649
15990
  return;
15650
- validateSourceLineGateGivenUsage(sourceName, struct, condition.refSummary, condition.e, finalCompiledModelDef, (cause, detail) => {
15991
+ validateSourceLineGateGivenUsage(sourceName, route, struct, condition.refSummary, condition.e, finalCompiledModelDef, (cause, detail) => {
15651
15992
  recordRowLevelGateRejected(cause);
15652
- authorizeWarningCollection.warnings.push(`Row-level #(authorize) gate warning on "${sourceName}" (${cause}): ${detail}`);
15993
+ authorizeWarningCollection.warnings.push(`#(${route}) gate warning on "${sourceName}" (${cause}): ${detail}`);
15653
15994
  });
15654
15995
  }
15655
15996
  });