@datarails-ai/dr-appspace-cli 0.12.37 → 0.12.39

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 (3) hide show
  1. package/README.md +5 -2
  2. package/dist/cli.js +1745 -324
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -13,7 +13,7 @@ import { fileURLToPath as fileURLToPath6 } from "url";
13
13
  // package.json
14
14
  var package_default = {
15
15
  name: "@datarails-ai/dr-appspace-cli",
16
- version: "0.12.37",
16
+ version: "0.12.39",
17
17
  description: "Datarails App Space CLI \u2014 build, try, push and publish App Space apps",
18
18
  type: "module",
19
19
  engines: {
@@ -57,7 +57,7 @@ var package_default = {
57
57
  };
58
58
 
59
59
  // src/buildInfo.ts
60
- var BUILD_TIME = "2026-10-26T14:01:26.088Z";
60
+ var BUILD_TIME = "2026-10-26T15:11:56.868Z";
61
61
  function formatBuildTime(iso) {
62
62
  if (iso === "") return "";
63
63
  const d = new Date(iso);
@@ -97,13 +97,13 @@ function compareSemver(a, b) {
97
97
  async function readCache() {
98
98
  try {
99
99
  const raw = await fs.readFile(cachePath(), "utf8");
100
- const parsed = JSON.parse(raw);
101
- if (typeof parsed.checked_at !== "number" || typeof parsed.latest_version !== "string" || parsed.latest_version === "") {
100
+ const parsed2 = JSON.parse(raw);
101
+ if (typeof parsed2.checked_at !== "number" || typeof parsed2.latest_version !== "string" || parsed2.latest_version === "") {
102
102
  return null;
103
103
  }
104
104
  return {
105
- checked_at: parsed.checked_at,
106
- latest_version: parsed.latest_version
105
+ checked_at: parsed2.checked_at,
106
+ latest_version: parsed2.latest_version
107
107
  };
108
108
  } catch {
109
109
  return null;
@@ -400,14 +400,14 @@ var KeyringStore = class _KeyringStore {
400
400
  return out;
401
401
  }
402
402
  if (raw === void 0) return out;
403
- let parsed;
403
+ let parsed2;
404
404
  try {
405
- parsed = JSON.parse(raw);
405
+ parsed2 = JSON.parse(raw);
406
406
  } catch {
407
407
  return out;
408
408
  }
409
- if (!Array.isArray(parsed)) return out;
410
- for (const item of parsed) {
409
+ if (!Array.isArray(parsed2)) return out;
410
+ for (const item of parsed2) {
411
411
  const savedAt = parseDate(item?.savedAt);
412
412
  const lastUsedAt = parseDate(item?.lastUsedAt);
413
413
  if (typeof item?.email === "string" && savedAt !== void 0 && lastUsedAt !== void 0) {
@@ -434,22 +434,22 @@ var KeyringStore = class _KeyringStore {
434
434
  const sessionAccount = `${serverKey}:${email}`;
435
435
  const raw = await this.backend.get(SERVICE, sessionAccount);
436
436
  if (raw === void 0) return void 0;
437
- let parsed;
437
+ let parsed2;
438
438
  try {
439
- parsed = JSON.parse(raw);
439
+ parsed2 = JSON.parse(raw);
440
440
  } catch (err) {
441
441
  throw new KeychainError(`decode session for ${serverKey}:${email}`, {
442
442
  cause: err
443
443
  });
444
444
  }
445
- const sessionId = typeof parsed.sessionId === "string" ? parsed.sessionId : "";
446
- const csrfToken = typeof parsed.csrfToken === "string" ? parsed.csrfToken : "";
447
- const savedAtStr = typeof parsed.savedAt === "string" ? parsed.savedAt : "";
448
- const impersonatorEmail = typeof parsed.impersonatorEmail === "string" ? parsed.impersonatorEmail : "";
445
+ const sessionId = typeof parsed2.sessionId === "string" ? parsed2.sessionId : "";
446
+ const csrfToken = typeof parsed2.csrfToken === "string" ? parsed2.csrfToken : "";
447
+ const savedAtStr = typeof parsed2.savedAt === "string" ? parsed2.savedAt : "";
448
+ const impersonatorEmail = typeof parsed2.impersonatorEmail === "string" ? parsed2.impersonatorEmail : "";
449
449
  const impersonated = impersonatorEmail !== "";
450
- const cookielessFlag = parsed.cookieless === true;
451
- const apiKey = typeof parsed.apiKey === "string" && parsed.apiKey !== "" ? parsed.apiKey : "";
452
- const apiKeyEnvVar = typeof parsed.apiKeyEnvVar === "string" && parsed.apiKeyEnvVar !== "" ? parsed.apiKeyEnvVar : "";
450
+ const cookielessFlag = parsed2.cookieless === true;
451
+ const apiKey = typeof parsed2.apiKey === "string" && parsed2.apiKey !== "" ? parsed2.apiKey : "";
452
+ const apiKeyEnvVar = typeof parsed2.apiKeyEnvVar === "string" && parsed2.apiKeyEnvVar !== "" ? parsed2.apiKeyEnvVar : "";
453
453
  const keyed = apiKey !== "" || apiKeyEnvVar !== "";
454
454
  const cookieless = impersonated || cookielessFlag || keyed;
455
455
  if (savedAtStr === "" || !cookieless && (sessionId === "" || csrfToken === "")) {
@@ -463,11 +463,11 @@ var KeyringStore = class _KeyringStore {
463
463
  };
464
464
  if (impersonated) {
465
465
  session.impersonatorEmail = impersonatorEmail;
466
- if (typeof parsed.impersonatedUserId === "number") {
467
- session.impersonatedUserId = parsed.impersonatedUserId;
466
+ if (typeof parsed2.impersonatedUserId === "number") {
467
+ session.impersonatedUserId = parsed2.impersonatedUserId;
468
468
  }
469
- if (typeof parsed.impersonatedOrgName === "string" && parsed.impersonatedOrgName !== "") {
470
- session.impersonatedOrgName = parsed.impersonatedOrgName;
469
+ if (typeof parsed2.impersonatedOrgName === "string" && parsed2.impersonatedOrgName !== "") {
470
+ session.impersonatedOrgName = parsed2.impersonatedOrgName;
471
471
  }
472
472
  }
473
473
  if (cookielessFlag) {
@@ -479,14 +479,14 @@ var KeyringStore = class _KeyringStore {
479
479
  if (apiKeyEnvVar !== "") {
480
480
  session.apiKeyEnvVar = apiKeyEnvVar;
481
481
  }
482
- if (typeof parsed.apiKeyExpiresAt === "string" && parsed.apiKeyExpiresAt !== "") {
483
- session.apiKeyExpiresAt = new Date(parsed.apiKeyExpiresAt);
482
+ if (typeof parsed2.apiKeyExpiresAt === "string" && parsed2.apiKeyExpiresAt !== "") {
483
+ session.apiKeyExpiresAt = new Date(parsed2.apiKeyExpiresAt);
484
484
  }
485
- if (typeof parsed.jwtApiKeyFingerprint === "string" && parsed.jwtApiKeyFingerprint !== "") {
486
- session.jwtApiKeyFingerprint = parsed.jwtApiKeyFingerprint;
485
+ if (typeof parsed2.jwtApiKeyFingerprint === "string" && parsed2.jwtApiKeyFingerprint !== "") {
486
+ session.jwtApiKeyFingerprint = parsed2.jwtApiKeyFingerprint;
487
487
  }
488
- if (typeof parsed.jwtExpiresAt === "string" && parsed.jwtExpiresAt !== "") {
489
- session.jwtExpiresAt = new Date(parsed.jwtExpiresAt);
488
+ if (typeof parsed2.jwtExpiresAt === "string" && parsed2.jwtExpiresAt !== "") {
489
+ session.jwtExpiresAt = new Date(parsed2.jwtExpiresAt);
490
490
  }
491
491
  const jwtAccess = await this.backend.get(SERVICE, `${sessionAccount}:access`);
492
492
  if (jwtAccess !== void 0 && jwtAccess !== "") {
@@ -768,12 +768,12 @@ async function requestDeviceCode(host, options) {
768
768
  if (raw === null || typeof raw !== "object") {
769
769
  throw new Error(`device/code parse: expected object`);
770
770
  }
771
- const obj4 = raw;
771
+ const obj5 = raw;
772
772
  return {
773
- deviceCode: typeof obj4.device_code === "string" ? obj4.device_code : "",
774
- userCode: typeof obj4.user_code === "string" ? obj4.user_code : "",
775
- verificationUriComplete: typeof obj4.verification_uri_complete === "string" ? obj4.verification_uri_complete : "",
776
- interval: typeof obj4.interval === "number" ? obj4.interval : 0
773
+ deviceCode: typeof obj5.device_code === "string" ? obj5.device_code : "",
774
+ userCode: typeof obj5.user_code === "string" ? obj5.user_code : "",
775
+ verificationUriComplete: typeof obj5.verification_uri_complete === "string" ? obj5.verification_uri_complete : "",
776
+ interval: typeof obj5.interval === "number" ? obj5.interval : 0
777
777
  };
778
778
  }
779
779
  var defaultSleep = (ms, signal) => new Promise((resolve17, reject) => {
@@ -802,13 +802,13 @@ function parseTokenResponse(text3) {
802
802
  if (raw === null || typeof raw !== "object") {
803
803
  throw new Error(`token parse: expected object`);
804
804
  }
805
- const obj4 = raw;
805
+ const obj5 = raw;
806
806
  const tok = {
807
- sessionId: typeof obj4.session_id === "string" ? obj4.session_id : "",
808
- csrfToken: typeof obj4.csrf_token === "string" ? obj4.csrf_token : ""
807
+ sessionId: typeof obj5.session_id === "string" ? obj5.session_id : "",
808
+ csrfToken: typeof obj5.csrf_token === "string" ? obj5.csrf_token : ""
809
809
  };
810
- if (typeof obj4.access_token === "string") {
811
- tok.accessToken = obj4.access_token;
810
+ if (typeof obj5.access_token === "string") {
811
+ tok.accessToken = obj5.access_token;
812
812
  }
813
813
  return tok;
814
814
  }
@@ -946,16 +946,16 @@ function decodeJwtClaims(token) {
946
946
  } catch (err) {
947
947
  throw new Error(`jwt: decode payload: ${msgOf3(err)}`);
948
948
  }
949
- let parsed;
949
+ let parsed2;
950
950
  try {
951
- parsed = JSON.parse(decoded);
951
+ parsed2 = JSON.parse(decoded);
952
952
  } catch (err) {
953
953
  throw new Error(`jwt: parse claims: ${msgOf3(err)}`);
954
954
  }
955
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
955
+ if (parsed2 === null || typeof parsed2 !== "object" || Array.isArray(parsed2)) {
956
956
  throw new Error("jwt: parse claims: expected object");
957
957
  }
958
- return parsed;
958
+ return parsed2;
959
959
  }
960
960
  function decodeJwtExpiration(token) {
961
961
  const claims = decodeJwtClaims(token);
@@ -975,10 +975,10 @@ function parseTokenBody(text3, label) {
975
975
  if (raw === null || typeof raw !== "object") {
976
976
  throw new Error(`jwt ${label} parse: expected object`);
977
977
  }
978
- const obj4 = raw;
978
+ const obj5 = raw;
979
979
  return {
980
- access: typeof obj4.access === "string" ? obj4.access : "",
981
- refresh: typeof obj4.refresh === "string" ? obj4.refresh : ""
980
+ access: typeof obj5.access === "string" ? obj5.access : "",
981
+ refresh: typeof obj5.refresh === "string" ? obj5.refresh : ""
982
982
  };
983
983
  }
984
984
  async function mintJwt(host, sessionId, csrfToken, options) {
@@ -1081,9 +1081,9 @@ async function loginAs(host, operatorAccessToken, userId, options) {
1081
1081
  let code;
1082
1082
  let detail;
1083
1083
  try {
1084
- const parsed = JSON.parse(text3);
1085
- if (typeof parsed["code"] === "string") code = parsed["code"];
1086
- if (typeof parsed["detail"] === "string") detail = parsed["detail"];
1084
+ const parsed2 = JSON.parse(text3);
1085
+ if (typeof parsed2["code"] === "string") code = parsed2["code"];
1086
+ if (typeof parsed2["detail"] === "string") detail = parsed2["detail"];
1087
1087
  } catch {
1088
1088
  }
1089
1089
  throw new LoginAsError(res.status, {
@@ -1101,9 +1101,9 @@ async function loginAs(host, operatorAccessToken, userId, options) {
1101
1101
  if (raw === null || typeof raw !== "object") {
1102
1102
  throw new Error("jwt loginas parse: expected object");
1103
1103
  }
1104
- const obj4 = raw;
1105
- const access = typeof obj4["access"] === "string" ? obj4["access"] : "";
1106
- const refresh = typeof obj4["refresh"] === "string" ? obj4["refresh"] : "";
1104
+ const obj5 = raw;
1105
+ const access = typeof obj5["access"] === "string" ? obj5["access"] : "";
1106
+ const refresh = typeof obj5["refresh"] === "string" ? obj5["refresh"] : "";
1107
1107
  if (access === "") {
1108
1108
  throw new Error("jwt loginas: empty access token");
1109
1109
  }
@@ -1113,7 +1113,7 @@ async function loginAs(host, operatorAccessToken, userId, options) {
1113
1113
  } catch (err) {
1114
1114
  throw new Error(`jwt loginas: ${msgOf3(err)}`);
1115
1115
  }
1116
- return { access, refresh, expiresAt, user: obj4["user"] };
1116
+ return { access, refresh, expiresAt, user: obj5["user"] };
1117
1117
  }
1118
1118
  async function exchangeApiKey(host, apiKey, options) {
1119
1119
  const key = apiKey.trim();
@@ -2135,14 +2135,14 @@ async function readCredentialsFile(path3) {
2135
2135
  if (err.code === "ENOENT") return void 0;
2136
2136
  throw err;
2137
2137
  }
2138
- let parsed;
2138
+ let parsed2;
2139
2139
  try {
2140
- parsed = JSON.parse(raw);
2140
+ parsed2 = JSON.parse(raw);
2141
2141
  } catch {
2142
2142
  throw new Error(`credential file ${path3} is not valid JSON`);
2143
2143
  }
2144
2144
  try {
2145
- return credentialsFileSchema.parse(parsed);
2145
+ return credentialsFileSchema.parse(parsed2);
2146
2146
  } catch (err) {
2147
2147
  const reason = err instanceof Error ? err.message : String(err);
2148
2148
  throw new Error(`credential file ${path3} is malformed: ${reason}`, { cause: err });
@@ -2227,14 +2227,14 @@ var FileKeyringBackend = class _FileKeyringBackend {
2227
2227
  }
2228
2228
  /** Cookie fields out of a metadata blob, dropped when empty. */
2229
2229
  static cookieOf(blob) {
2230
- let parsed;
2230
+ let parsed2;
2231
2231
  try {
2232
- parsed = JSON.parse(blob);
2232
+ parsed2 = JSON.parse(blob);
2233
2233
  } catch {
2234
2234
  return {};
2235
2235
  }
2236
- const sessionId = typeof parsed.sessionId === "string" ? parsed.sessionId : "";
2237
- const csrfToken = typeof parsed.csrfToken === "string" ? parsed.csrfToken : "";
2236
+ const sessionId = typeof parsed2.sessionId === "string" ? parsed2.sessionId : "";
2237
+ const csrfToken = typeof parsed2.csrfToken === "string" ? parsed2.csrfToken : "";
2238
2238
  return {
2239
2239
  ...sessionId !== "" && { sessionId },
2240
2240
  ...csrfToken !== "" && { csrfToken }
@@ -2248,8 +2248,8 @@ var FileKeyringBackend = class _FileKeyringBackend {
2248
2248
  */
2249
2249
  static fingerprintOf(blob) {
2250
2250
  try {
2251
- const parsed = JSON.parse(blob);
2252
- return typeof parsed.jwtApiKeyFingerprint === "string" ? parsed.jwtApiKeyFingerprint : "";
2251
+ const parsed2 = JSON.parse(blob);
2252
+ return typeof parsed2.jwtApiKeyFingerprint === "string" ? parsed2.jwtApiKeyFingerprint : "";
2253
2253
  } catch {
2254
2254
  return "";
2255
2255
  }
@@ -2467,11 +2467,11 @@ function registerLoginCommand(root) {
2467
2467
  }
2468
2468
  let timeoutMs;
2469
2469
  if (opts.timeout !== void 0) {
2470
- const parsed = Number(opts.timeout);
2471
- if (!Number.isFinite(parsed) || parsed <= 0) {
2470
+ const parsed2 = Number(opts.timeout);
2471
+ if (!Number.isFinite(parsed2) || parsed2 <= 0) {
2472
2472
  throw new Error(`--timeout must be a positive number of milliseconds`);
2473
2473
  }
2474
- timeoutMs = parsed;
2474
+ timeoutMs = parsed2;
2475
2475
  }
2476
2476
  const store = fileStore?.store ?? await KeyringStore.defaultWithCleanup();
2477
2477
  await runLogin({
@@ -2962,6 +2962,16 @@ async function connect(opts) {
2962
2962
  return {
2963
2963
  serverKey: client.serverKey,
2964
2964
  baseUrl,
2965
+ host: client.host,
2966
+ email: client.email,
2967
+ claims() {
2968
+ try {
2969
+ const access = client.session.jwtAccess ?? "";
2970
+ return access === "" ? null : decodeJwtClaims(access);
2971
+ } catch {
2972
+ return null;
2973
+ }
2974
+ },
2965
2975
  cleanup,
2966
2976
  upload: (appId, file) => uploadVia(
2967
2977
  (path3, fields, blob, options) => client.postMultipart(path3, fields, blob, options),
@@ -3008,16 +3018,16 @@ async function connect(opts) {
3008
3018
  `the request was intercepted before reaching App Space (HTTP ${res.status}, HTML page returned \u2014 likely the Cloudflare WAF). The CLI gzips UI bundles to avoid this; if you are calling the API yourself, send frontend_bundle_gz.`
3009
3019
  );
3010
3020
  }
3011
- let parsed = void 0;
3021
+ let parsed2 = void 0;
3012
3022
  try {
3013
- parsed = text3 === "" ? void 0 : JSON.parse(text3);
3023
+ parsed2 = text3 === "" ? void 0 : JSON.parse(text3);
3014
3024
  } catch {
3015
- parsed = text3;
3025
+ parsed2 = text3;
3016
3026
  }
3017
3027
  if (!res.ok) {
3018
- throw new AppSpaceHttpError(describeFailure(res.status, method, path3, parsed), res.status, parsed);
3028
+ throw new AppSpaceHttpError(describeFailure(res.status, method, path3, parsed2), res.status, parsed2);
3019
3029
  }
3020
- return parsed;
3030
+ return parsed2;
3021
3031
  }
3022
3032
  };
3023
3033
  }
@@ -3131,11 +3141,11 @@ function formatProblems(problems) {
3131
3141
  if (problems.length === 0) return "";
3132
3142
  const width = Math.max(...problems.map((p) => p.code.length + 2)) + 2;
3133
3143
  let out = "";
3134
- for (const problem5 of problems) {
3135
- const head = `[${problem5.code}]`.padEnd(width);
3136
- out += `${head}${problem5.pointer}: ${problem5.message}
3144
+ for (const problem6 of problems) {
3145
+ const head = `[${problem6.code}]`.padEnd(width);
3146
+ out += `${head}${problem6.pointer}: ${problem6.message}
3137
3147
  `;
3138
- out += `${" ".repeat(head.length)}${problem5.fix}
3148
+ out += `${" ".repeat(head.length)}${problem6.fix}
3139
3149
  `;
3140
3150
  }
3141
3151
  return out;
@@ -3164,13 +3174,13 @@ var NO_CATALOG = {
3164
3174
  };
3165
3175
  async function readCatalogFile(path3) {
3166
3176
  const raw = await readFile2(path3, "utf8");
3167
- let parsed;
3177
+ let parsed2;
3168
3178
  try {
3169
- parsed = JSON.parse(raw);
3179
+ parsed2 = JSON.parse(raw);
3170
3180
  } catch {
3171
3181
  throw new Error(`${path3} is not valid JSON.`);
3172
3182
  }
3173
- const root = unwrap(parsed);
3183
+ const root = unwrap(parsed2);
3174
3184
  const metrics = rows(root["metrics"] ?? root["kpis"] ?? root["business_metrics"]).map(
3175
3185
  (row) => ({
3176
3186
  id: idOf(row),
@@ -3332,8 +3342,8 @@ function findTable(catalog, named13) {
3332
3342
  (table) => table.id.toLowerCase() === want || (table.alias ?? "").toLowerCase() === want || table.name.toLowerCase() === want
3333
3343
  );
3334
3344
  }
3335
- function unwrap(parsed) {
3336
- let node = isObject(parsed) ? parsed : {};
3345
+ function unwrap(parsed2) {
3346
+ let node = isObject(parsed2) ? parsed2 : {};
3337
3347
  for (const key of ["catalog", "data", "result"]) {
3338
3348
  const held = node[key];
3339
3349
  if (isObject(held) && ("tables" in held || "metrics" in held || "kpis" in held)) {
@@ -6830,7 +6840,7 @@ function specTransforms(handlers, spec, problems) {
6830
6840
  out.push({ path: path3, text: text3 });
6831
6841
  }
6832
6842
  }
6833
- const unreadable = problems.some((problem5) => problem5.code === "transform_unreadable");
6843
+ const unreadable = problems.some((problem6) => problem6.code === "transform_unreadable");
6834
6844
  for (const key of Object.keys(spec.table).filter(isWidgetKey).sort(natural3)) {
6835
6845
  const path3 = isPlainObject(spec.table[key]) ? spec.table[key].transform : void 0;
6836
6846
  if (typeof path3 !== "string" || unreadable || carried !== null && carried.has(path3)) continue;
@@ -8732,8 +8742,8 @@ function unproducedPayloadKeys(contract, { handlerSource = () => void 0, moduleS
8732
8742
  if (produced.includes(key)) continue;
8733
8743
  const there = produced.length === 0 ? "the controls there produce no payload keys" : `the controls there produce ${produced.join(", ")}`;
8734
8744
  const fix = reads.generated ? `Add a control that sets it, or take "${key}" out of the handler's required in contract.json.` : `Add a control that sets it, or read it with payload.get("${key}", <default>).`;
8735
- const problem5 = `handler "${name}" reads payload "${key}", and nothing on ${either(pages)} that calls it produces that key \u2014 ${there}.`;
8736
- out.push({ handler: name, key, pages, produced, generated: reads.generated, problem: problem5, fix, message: `${problem5} ${fix}` });
8745
+ const problem6 = `handler "${name}" reads payload "${key}", and nothing on ${either(pages)} that calls it produces that key \u2014 ${there}.`;
8746
+ out.push({ handler: name, key, pages, produced, generated: reads.generated, problem: problem6, fix, message: `${problem6} ${fix}` });
8737
8747
  }
8738
8748
  }
8739
8749
  return out;
@@ -8767,9 +8777,9 @@ function undeclaredCustomCalls(contract, { moduleSource = () => void 0 } = {}) {
8767
8777
  }
8768
8778
  function undeclaredCallMessage(id, module, handler, calls, how) {
8769
8779
  const listed = calls.length === 0 ? "lists none" : `lists ${calls.join(", ")}`;
8770
- const problem5 = `custom "${id}" calls handler "${handler}" (ui.call ${how}, ui/${module}), and its declares.calls ${listed}.`;
8780
+ const problem6 = `custom "${id}" calls handler "${handler}" (ui.call ${how}, ui/${module}), and its declares.calls ${listed}.`;
8771
8781
  const fix = `Add "${handler}" to declares.calls \u2014 and what it sends to declares.produces \u2014 or stop calling it.`;
8772
- return { problem: problem5, fix, message: `${problem5} ${fix}` };
8782
+ return { problem: problem6, fix, message: `${problem6} ${fix}` };
8773
8783
  }
8774
8784
  function unreadUploadKeys(contract, { handlerSource = () => void 0 } = {}) {
8775
8785
  const elements = isPlainObject2(contract?.elements) ? contract.elements : {};
@@ -8812,9 +8822,9 @@ function unreadUploadKeys(contract, { handlerSource = () => void 0 } = {}) {
8812
8822
  }
8813
8823
  if (read) continue;
8814
8824
  const id = idOf2(element, uid);
8815
- const problem5 = `file_upload "${id}" writes the stored file under "${field}" to handler "${handler}" (${via}), and "${handler}" never reads payload["${field}"] \u2014 the file is stored and nothing uses it.`;
8825
+ const problem6 = `file_upload "${id}" writes the stored file under "${field}" to handler "${handler}" (${via}), and "${handler}" never reads payload["${field}"] \u2014 the file is stored and nothing uses it.`;
8816
8826
  const fix = role === "custom" ? `Read payload["${field}"]["upload"] in "${handler}" and hand it to the parser (or write payload["${field}"] into a file column), or drop the upload.` : `Add "${field}" to "${handler}"'s fields (a file column), or drop the upload.`;
8817
- out.push({ uid, id, page: element.page ?? null, field, handler, via, problem: problem5, fix, message: `${problem5} ${fix}` });
8827
+ out.push({ uid, id, page: element.page ?? null, field, handler, via, problem: problem6, fix, message: `${problem6} ${fix}` });
8818
8828
  }
8819
8829
  }
8820
8830
  return out;
@@ -8830,9 +8840,9 @@ function stepsWithoutAction(contract) {
8830
8840
  const id = idOf2(element, uid);
8831
8841
  const name = named2(step.id) ? step.id : String(index);
8832
8842
  const next = named2(steps[index + 1]?.id) ? steps[index + 1].id : "the next step";
8833
- const problem5 = `stepper "${id}" step "${name}" names no action, so no handler moves the flow on from it \u2014 a flow that reaches it stays there.`;
8843
+ const problem6 = `stepper "${id}" step "${name}" names no action, so no handler moves the flow on from it \u2014 a flow that reaches it stays there.`;
8834
8844
  const fix = `Give it the handler that moves it to "${next}" (action, action_label), or make it the last step.`;
8835
- out.push({ uid, id, page: element.page ?? null, step: name, index, problem: problem5, fix, message: `${problem5} ${fix}` });
8845
+ out.push({ uid, id, page: element.page ?? null, step: name, index, problem: problem6, fix, message: `${problem6} ${fix}` });
8836
8846
  });
8837
8847
  }
8838
8848
  return out;
@@ -8905,7 +8915,7 @@ var MAX_HINT2 = 120;
8905
8915
  function validate2(instance, schema, options = {}) {
8906
8916
  const problems = [];
8907
8917
  check2(instance, schema, "", schema, problems, { quote: options.quote === true, inBranch: false });
8908
- return problems.map((problem5) => `${problem5.at}: ${problem5.why}`);
8918
+ return problems.map((problem6) => `${problem6.at}: ${problem6.why}`);
8909
8919
  }
8910
8920
  function problem2(at, why, kind = "other") {
8911
8921
  return { at: at || "/", why, kind };
@@ -9318,9 +9328,9 @@ function unreachablePages(contract, files = {}) {
9318
9328
  else if (roles.length > 0) why = `it is for ${both(quoted(roles))}, and no page ${either2(quoted(roles))} reaches links to it`;
9319
9329
  else if (gates.length > 0) why = `every nav entry to it is gated to ${either2(quoted(gates))}, and no role shown that entry reaches a page carrying it`;
9320
9330
  else why = "the only pages that link to it are pages no role reaches";
9321
- const problem5 = `${page.path} is a page of the app, and no role reaches it from the nav \u2014 ${why} \u2014 so only a typed URL opens it.`;
9331
+ const problem6 = `${page.path} is a page of the app, and no role reaches it from the nav \u2014 ${why} \u2014 so only a typed URL opens it.`;
9322
9332
  const fix = roles.length > 0 || gates.length > 0 ? `Gate its nav entry to a role that should open it (pages[].roles on the page, a role's home, or the element's role), or drop the page.` : "Give the pages a nav (a nav element on each page), or drop the page.";
9323
- out.push({ page: page.path, problem: problem5, fix, message: `${problem5} ${fix}` });
9333
+ out.push({ page: page.path, problem: problem6, fix, message: `${problem6} ${fix}` });
9324
9334
  }
9325
9335
  return out;
9326
9336
  }
@@ -9466,9 +9476,9 @@ function unconfirmedStateChanges(contract, files = {}) {
9466
9476
  named_.push(one.module ? `${one.what} (on ${one.page})` : `${one.what} "${one.id}" (on ${one.page})`);
9467
9477
  }
9468
9478
  const kind = handlers[handler]?.role === "decide" ? "decides a row" : "changes a record's state";
9469
- const problem5 = `handler "${handler}" ${kind}, and ${both(named_)} ${named_.length === 1 ? "calls" : "call"} it on one click, with no confirm and no dialog in front of it.`;
9479
+ const problem6 = `handler "${handler}" ${kind}, and ${both(named_)} ${named_.length === 1 ? "calls" : "call"} it on one click, with no confirm and no dialog in front of it.`;
9470
9480
  const fix = 'Put the decision behind a confirm: a dialog part whose on_confirm is the handler (with the reason as one of its inputs), a row action with "confirm", a map-form decide whose decision takes a note, or \u2014 in a custom module \u2014 ui.confirm before ui.call.';
9471
- out.push({ handler, callers: bare.map(({ page, uid, what }) => ({ page, uid, what })), problem: problem5, fix, message: `${problem5} ${fix}` });
9481
+ out.push({ handler, callers: bare.map(({ page, uid, what }) => ({ page, uid, what })), problem: problem6, fix, message: `${problem6} ${fix}` });
9472
9482
  }
9473
9483
  return out;
9474
9484
  }
@@ -9546,18 +9556,18 @@ function unreadReasons(contract, { handlerSource = () => void 0, moduleSource, p
9546
9556
  for (const [key, pages] of [...sent.entries()].sort()) {
9547
9557
  if (read.has(key)) continue;
9548
9558
  const where2 = pages.size === 0 ? "the page" : either2([...pages].sort());
9549
- const problem5 = `${where2} sends handler "${name}" a "${key}", and the handler never reads it \u2014 the reason a person types is thrown away.`;
9559
+ const problem6 = `${where2} sends handler "${name}" a "${key}", and the handler never reads it \u2014 the reason a person types is thrown away.`;
9550
9560
  const fix = `Read it (payload.get("${key}")) and keep it with the decision \u2014 a column, or the audit note \u2014 or stop asking for it.`;
9551
- out.push({ handler: name, key, kind: "unread", pages: [...pages].sort(), problem: problem5, fix, message: `${problem5} ${fix}` });
9561
+ out.push({ handler: name, key, kind: "unread", pages: [...pages].sort(), problem: problem6, fix, message: `${problem6} ${fix}` });
9552
9562
  }
9553
9563
  const lines = pyLines(source);
9554
9564
  if (!changesState(name, row, source) || !REJECTS.test(lines.join("\n"))) continue;
9555
9565
  for (const key of REASON_KEYS) {
9556
9566
  if (!reads.optional.includes(key) || reads.required.includes(key)) continue;
9557
9567
  if (requiresReason(lines, key, reasonVars(lines, key))) continue;
9558
- const problem5 = `handler "${name}" rejects (or overrides), and reads "${key}" without ever requiring it \u2014 a rejection goes through with no reason given.`;
9568
+ const problem6 = `handler "${name}" rejects (or overrides), and reads "${key}" without ever requiring it \u2014 a rejection goes through with no reason given.`;
9559
9569
  const fix = `Refuse the reject path without one: if decision == "rejected" and not ${key}: return {"error": "Give a reason to reject."}.`;
9560
- out.push({ handler: name, key, kind: "optional", pages: [], problem: problem5, fix, message: `${problem5} ${fix}` });
9570
+ out.push({ handler: name, key, kind: "optional", pages: [], problem: problem6, fix, message: `${problem6} ${fix}` });
9561
9571
  }
9562
9572
  }
9563
9573
  return out;
@@ -9594,9 +9604,9 @@ function unliftableRefusals(contract, { handlerSource = () => void 0 } = {}) {
9594
9604
  }
9595
9605
  if (!refuses) continue;
9596
9606
  const shown2 = line2.trim().replace(/:$/, "");
9597
- const problem5 = `handler "${name}" refuses outright at line ${si + 1} (${shown2}) on the same path where it demands a "${key}" (line ${gi + 1}), and no reason lifts that refusal \u2014 the page asks for a reason to go ahead, and the reason is ignored.`;
9607
+ const problem6 = `handler "${name}" refuses outright at line ${si + 1} (${shown2}) on the same path where it demands a "${key}" (line ${gi + 1}), and no reason lifts that refusal \u2014 the page asks for a reason to go ahead, and the reason is ignored.`;
9598
9608
  const fix = `Let the reason override it (check "${key}" before refusing, and record it), or tell the reader that this state cannot be overridden and stop asking for a reason.`;
9599
- out.push({ handler: name, key, line: si + 1, gateLine: gi + 1, problem: problem5, fix, message: `${problem5} ${fix}` });
9609
+ out.push({ handler: name, key, line: si + 1, gateLine: gi + 1, problem: problem6, fix, message: `${problem6} ${fix}` });
9600
9610
  }
9601
9611
  }
9602
9612
  }
@@ -9612,13 +9622,13 @@ function rolesLandingNowhere(contract) {
9612
9622
  const exists = pagesOf(contract).some((page) => page.path === landing);
9613
9623
  if (exists && opens(contract, model, role, landing)) continue;
9614
9624
  const stated2 = model.home.get(role);
9615
- const problem5 = !exists ? `role "${role}" lands on ${landing} (its home), which is not a page of the app.` : stated2 ? `role "${role}" lands on its home, ${landing}, which is for ${both(quoted(pageRoles(pagesOf(contract).find((page) => page.path === landing))))} \u2014 so the role opens the app on a 404.` : `role "${role}" states no home and lands on ${landing}, which is for ${both(quoted(pageRoles(pagesOf(contract).find((page) => page.path === landing))))} \u2014 so the role opens the app on a 404.`;
9625
+ const problem6 = !exists ? `role "${role}" lands on ${landing} (its home), which is not a page of the app.` : stated2 ? `role "${role}" lands on its home, ${landing}, which is for ${both(quoted(pageRoles(pagesOf(contract).find((page) => page.path === landing))))} \u2014 so the role opens the app on a 404.` : `role "${role}" states no home and lands on ${landing}, which is for ${both(quoted(pageRoles(pagesOf(contract).find((page) => page.path === landing))))} \u2014 so the role opens the app on a 404.`;
9616
9626
  const fix = `Give "${role}" a home it may open (backend.roles[].home: one of its pages), or add it to that page's pages[].roles.`;
9617
- out.push({ role, page: landing, problem: problem5, fix, message: `${problem5} ${fix}` });
9627
+ out.push({ role, page: landing, problem: problem6, fix, message: `${problem6} ${fix}` });
9618
9628
  }
9619
9629
  return out;
9620
9630
  }
9621
- function sourceRoles(contract, name) {
9631
+ function sourceRoles(contract, name, known = /* @__PURE__ */ new Set()) {
9622
9632
  const source = contract?.data?.sources?.[name];
9623
9633
  const stated2 = gateRoles(source?.requiresRole);
9624
9634
  const elements = isPlainObject3(contract?.elements) ? contract.elements : {};
@@ -9636,7 +9646,7 @@ function sourceRoles(contract, name) {
9636
9646
  for (const role of roles) if (!union.includes(role)) union.push(role);
9637
9647
  }
9638
9648
  for (const page of pagesOf(contract)) {
9639
- if (page.owned !== true || readers === 0) continue;
9649
+ if (page.owned !== true || readers === 0 || known.has(page.path)) continue;
9640
9650
  const roles = pageRoles(page);
9641
9651
  if (roles.length === 0) open2 = true;
9642
9652
  for (const role of roles) if (!union.includes(role)) union.push(role);
@@ -9653,8 +9663,8 @@ function openToEveryRole(contract) {
9653
9663
  const out = [];
9654
9664
  const say = (kind, names, what, fix) => {
9655
9665
  if (names.length === 0) return;
9656
- const problem5 = `${names.length === 1 ? `${what.one} ${names[0]} states` : `${names.length} ${what.many} \u2014 ${both(names)} \u2014 state`} no roles, so every role (${all}) may ${what.verb} ${names.length === 1 ? "it" : "them"}.`;
9657
- out.push({ kind, names, problem: problem5, fix, message: `${problem5} ${fix}` });
9666
+ const problem6 = `${names.length === 1 ? `${what.one} ${names[0]} states` : `${names.length} ${what.many} \u2014 ${both(names)} \u2014 state`} no roles, so every role (${all}) may ${what.verb} ${names.length === 1 ? "it" : "them"}.`;
9667
+ out.push({ kind, names, problem: problem6, fix, message: `${problem6} ${fix}` });
9658
9668
  };
9659
9669
  say(
9660
9670
  "page",
@@ -10572,7 +10582,7 @@ function compile(contract, { kit, extra = {} } = {}) {
10572
10582
  if (handler.unwritable !== null) return refuse2(handler.unwritable.code, handler.unwritable.where, handler.unwritable.message);
10573
10583
  for (const note of handler.notes) problems.push(note);
10574
10584
  if (handler.refusals.length > 0) {
10575
- for (const problem5 of handler.refusals) problems.push(problem5);
10585
+ for (const problem6 of handler.refusals) problems.push(problem6);
10576
10586
  return { files: {}, problems: problems.sort(byProblem2) };
10577
10587
  }
10578
10588
  const files = {};
@@ -10605,7 +10615,7 @@ function dashboardHandler(composed, handlerName, extra = {}) {
10605
10615
  out.notes.push(...missingExpressions(composed.data?.widgets));
10606
10616
  const carried = [...transformRowProblems(composed.data), ...transformFileProblems(composed.data, extra)];
10607
10617
  if (carried.length > 0) {
10608
- out.refusals.push(...carried.map((problem5) => ({ ...problem5, lossy: false })));
10618
+ out.refusals.push(...carried.map((problem6) => ({ ...problem6, lossy: false })));
10609
10619
  return out;
10610
10620
  }
10611
10621
  const transforms = transformTexts(composed.data, extra);
@@ -11141,7 +11151,7 @@ function compileDashboard(contract, { kit = null, extra = {}, uiKit, charts, fmt
11141
11151
  }
11142
11152
  problems.push(...handler.notes);
11143
11153
  if (handler.refusals.length > 0) {
11144
- for (const problem5 of handler.refusals) refuse2(problem5.code, problem5.where, problem5.message);
11154
+ for (const problem6 of handler.refusals) refuse2(problem6.code, problem6.where, problem6.message);
11145
11155
  return refused();
11146
11156
  }
11147
11157
  const restated = restateDashboard(composed, pages, handlerName, handler, { refuse: refuse2, note, stated: Array.isArray(source.pages) ? source.pages : [] });
@@ -11178,17 +11188,17 @@ function compileDashboard(contract, { kit = null, extra = {}, uiKit, charts, fmt
11178
11188
  for (const page of whole.pages) files[page.path] = pageHtml(whole, page, elements, tree, shelf2);
11179
11189
  return { files, problems, contract: whole, order: order2, warnings: narrativeOrderWarnings(composed, applied) };
11180
11190
  }
11181
- function isRefusal(problem5) {
11182
- return problem5.lossy !== false;
11191
+ function isRefusal(problem6) {
11192
+ return problem6.lossy !== false;
11183
11193
  }
11184
11194
  function restateDashboard(composed, pages, handlerName, handler, { refuse: refuse2, note, stated: stated2 }) {
11185
11195
  const app = isPlainObject7(composed.app) ? composed.app : {};
11186
11196
  const typed = isPlainObject7(composed.elements) ? composed.elements : {};
11187
11197
  const declared = treeOf(typed);
11188
- const conflicts = new Map(declared.problems.map((problem5) => [uidOfPointer(problem5.pointer), problem5]));
11198
+ const conflicts = new Map(declared.problems.map((problem6) => [uidOfPointer(problem6.pointer), problem6]));
11189
11199
  const paths = new Set(pages.map((page) => page.path));
11190
11200
  const unplaced = Object.fromEntries(Object.keys(typed).filter((uid) => isPlainObject7(typed[uid]) && !paths.has(typed[uid].page)).map((uid) => [uid, typed[uid]]));
11191
- for (const problem5 of placementProblems(unplaced, pages, declared, [])) refuse2(problem5.code, problem5.pointer, problem5.message);
11201
+ for (const problem6 of placementProblems(unplaced, pages, declared, [])) refuse2(problem6.code, problem6.pointer, problem6.message);
11192
11202
  const elements = {};
11193
11203
  const outPages = [];
11194
11204
  for (const page of pages) {
@@ -11996,8 +12006,8 @@ function timeModifierProblem(mod) {
11996
12006
  return `has a malformed time modifier "${mod}" (expected .SHIFT(<unit>, <offset>) or .AS_OF(<anchor>))`;
11997
12007
  }
11998
12008
  function checkModifier(mod) {
11999
- const problem5 = timeModifierProblem(mod);
12000
- if (problem5 !== null) refuse(problem5);
12009
+ const problem6 = timeModifierProblem(mod);
12010
+ if (problem6 !== null) refuse(problem6);
12001
12011
  }
12002
12012
  function namedItself(spelled) {
12003
12013
  return `names the metric ${spelled} itself; an operand is [@key], a key data.metrics declares, so the contract names no company's metric \u2014 declare it there (with "reuse": "name" and "create": false to bind the company's own) and write [@<its key>]`;
@@ -12144,6 +12154,27 @@ function metricFormulaProblems(formula, declared) {
12144
12154
  return { problems: [error.message], operands: [], windows: [] };
12145
12155
  }
12146
12156
  }
12157
+ function wireFormula(formula, names) {
12158
+ const known = names !== null && typeof names === "object" ? names : {};
12159
+ const { padded, stash } = scan(formula, Object.keys(known));
12160
+ const nameOf5 = (key) => {
12161
+ const name = known[key];
12162
+ if (typeof name !== "string" || name.trim() === "" || /['[\]]/.test(name)) {
12163
+ refuse(`reads [@${key}], bound to a metric named ${JSON.stringify(name ?? null)}, which a CALC formula cannot reference (an operand's name is non-empty, with no ' [ or ])`);
12164
+ }
12165
+ return name;
12166
+ };
12167
+ const wire = padded.replace(STASH_RE, (_m, index) => {
12168
+ const entry = stash[Number(index)];
12169
+ if (entry.kind === "token") return `'${entry.token}'`;
12170
+ if (entry.kind === "window") return `${entry.fn}('${nameOf5(entry.key)}', ${entry.unit}, ${entry.periods})`;
12171
+ return `'${nameOf5(entry.key)}'${entry.mod}`;
12172
+ });
12173
+ if (wire.length > MAX_FORMULA_LENGTH) {
12174
+ refuse(`is ${wire.length} characters with the company's metric names written in, over the server maximum of ${MAX_FORMULA_LENGTH}; shorten the formula or the names`);
12175
+ }
12176
+ return wire;
12177
+ }
12147
12178
 
12148
12179
  // ../flows/insights-builder/kit/contract/metricFormats.mjs
12149
12180
  var CURRENCY_PREFIXES = ['"$"', "\u20AC", "\xA3", "\u20AA"];
@@ -12593,6 +12624,34 @@ function servedSources(elements, sources) {
12593
12624
  }
12594
12625
  return [...out].sort(natural3);
12595
12626
  }
12627
+ function ownedReaders(pages, extra) {
12628
+ const readers = {};
12629
+ const known = /* @__PURE__ */ new Set();
12630
+ for (const page of Array.isArray(pages) ? pages : []) {
12631
+ if (page?.owned !== true || !named5(page.path)) continue;
12632
+ const text3 = extra?.[page.path];
12633
+ if (typeof text3 !== "string" || !isOwnedPage(text3)) continue;
12634
+ const embedded = embeddedContractOf({ text: text3 });
12635
+ if (embedded === null) continue;
12636
+ known.add(page.path);
12637
+ const elements = isPlainObject8(embedded.elements) ? embedded.elements : {};
12638
+ for (const uid of Object.keys(elements).sort(natural3)) {
12639
+ const element = elements[uid];
12640
+ if (!isPlainObject8(element) || element.page !== page.path) continue;
12641
+ const props = isPlainObject8(element.props) ? element.props : {};
12642
+ const kept = {};
12643
+ if (named5(props.source)) kept.source = props.source;
12644
+ if (Array.isArray(props.declares?.reads)) kept.declares = { reads: props.declares.reads.filter(named5) };
12645
+ if (Object.keys(kept).length > 0) readers[`owned:${uid}`] = { page: page.path, props: kept };
12646
+ }
12647
+ }
12648
+ return { readers, known };
12649
+ }
12650
+ function withOwnedReaders(contract, readers) {
12651
+ if (Object.keys(readers).length === 0) return contract;
12652
+ const elements = isPlainObject8(contract?.elements) ? contract.elements : {};
12653
+ return { ...contract, elements: { ...elements, ...readers } };
12654
+ }
12596
12655
  function sourceProblems(sources, app, handlers, extra) {
12597
12656
  const problems = [];
12598
12657
  const names = Object.keys(sources).sort(natural3);
@@ -12814,11 +12873,11 @@ function metricFilterProblem(filter) {
12814
12873
  if (filter.exclude !== void 0 && typeof filter.exclude !== "boolean") return `states exclude ${JSON.stringify(filter.exclude)}; exclude is true or false`;
12815
12874
  return null;
12816
12875
  }
12817
- function metricReaches(operandsOf, start, target, seen) {
12876
+ function metricReaches(operandsOf2, start, target, seen) {
12818
12877
  if (start === target) return true;
12819
12878
  if (seen.has(start)) return false;
12820
12879
  seen.add(start);
12821
- return (operandsOf.get(start) ?? []).some((one) => metricReaches(operandsOf, one, target, seen));
12880
+ return (operandsOf2.get(start) ?? []).some((one) => metricReaches(operandsOf2, one, target, seen));
12822
12881
  }
12823
12882
  function calcShaped(node) {
12824
12883
  if (node[0] === "num" || node[0] === "name") return true;
@@ -12947,12 +13006,12 @@ function metricProblems(metrics, sources) {
12947
13006
  }
12948
13007
  }
12949
13008
  }
12950
- const operandsOf = /* @__PURE__ */ new Map();
13009
+ const operandsOf2 = /* @__PURE__ */ new Map();
12951
13010
  for (const key of [...formulas.keys()].sort(natural3)) {
12952
13011
  const { problems: wrong, operands, windows } = metricFormulaProblems(formulas.get(key), declaredKeys);
12953
13012
  for (const one of wrong) say("metric_formula", at(key, "formula"), `metric "${key}"'s formula ${one}`);
12954
13013
  if (wrong.length > 0) continue;
12955
- operandsOf.set(key, operands);
13014
+ operandsOf2.set(key, operands);
12956
13015
  if (windows.length > 0) {
12957
13016
  advise(
12958
13017
  "metric_window",
@@ -12961,9 +13020,9 @@ function metricProblems(metrics, sources) {
12961
13020
  );
12962
13021
  }
12963
13022
  }
12964
- for (const key of [...operandsOf.keys()].sort(natural3)) {
12965
- for (const dep of operandsOf.get(key)) {
12966
- if (metricReaches(operandsOf, dep, key, /* @__PURE__ */ new Set())) {
13023
+ for (const key of [...operandsOf2.keys()].sort(natural3)) {
13024
+ for (const dep of operandsOf2.get(key)) {
13025
+ if (metricReaches(operandsOf2, dep, key, /* @__PURE__ */ new Set())) {
12967
13026
  say("metric_formula", at(key, "formula"), `metric "${key}"'s formula reads itself${dep === key ? "" : `, through [@${dep}]`}; a calc metric's operands are metrics that exist before it, so none may read it back`);
12968
13027
  break;
12969
13028
  }
@@ -13006,7 +13065,7 @@ function metricProblems(metrics, sources) {
13006
13065
  const visit = (key) => {
13007
13066
  if (used.has(key)) return;
13008
13067
  used.add(key);
13009
- for (const one of operandsOf.get(key) ?? []) visit(one);
13068
+ for (const one of operandsOf2.get(key) ?? []) visit(one);
13010
13069
  };
13011
13070
  for (const key of read) visit(key);
13012
13071
  for (const key of declaredKeys) {
@@ -13421,8 +13480,8 @@ function paramProblems(contract, sources) {
13421
13480
  }
13422
13481
  function numberFix(value, integer = false) {
13423
13482
  const text3 = typeof value === "string" ? value.trim() : null;
13424
- const parsed = text3 !== null && text3 !== "" ? Number(text3) : NaN;
13425
- if (Number.isFinite(parsed) && (!integer || Number.isInteger(parsed))) return `write it as the number ${parsed}, unquoted`;
13483
+ const parsed2 = text3 !== null && text3 !== "" ? Number(text3) : NaN;
13484
+ if (Number.isFinite(parsed2) && (!integer || Number.isInteger(parsed2))) return `write it as the number ${parsed2}, unquoted`;
13426
13485
  return `write ${integer ? "a whole number" : "a number"}, or leave the key out`;
13427
13486
  }
13428
13487
  function uidHint(value, why, elements) {
@@ -13624,7 +13683,8 @@ function compileApp(contract, { kit = null, runtime = RUNTIME, extra = {} } = {}
13624
13683
  problems.push(...exprProblems(stated2));
13625
13684
  const metricFindings = metricProblems(contract?.data?.metrics, stated2);
13626
13685
  problems.push(...metricFindings.filter((one) => !one.advisory));
13627
- problems.push(...paramProblems(contract, stated2));
13686
+ const owned = ownedReaders(contract.pages, extra);
13687
+ problems.push(...paramProblems(withOwnedReaders(contract, owned.readers), stated2));
13628
13688
  problems.push(...themeProblems(app));
13629
13689
  problems.push(...shellProblems(contract, typed, handlers, stated2));
13630
13690
  const sources = fillFilterable(stated2);
@@ -13743,10 +13803,10 @@ function compileApp(contract, { kit = null, runtime = RUNTIME, extra = {} } = {}
13743
13803
  problems.push(...placementProblems(elements, pages, tree, order2));
13744
13804
  if (problems.length > 0) return { files: {}, problems, contract, warnings };
13745
13805
  const whole = restate(contract, elements, pages, typed, order2);
13746
- const served = servedSources(elements, sources);
13806
+ const served = servedSources({ ...elements, ...owned.readers }, sources);
13747
13807
  const transforms = transformTexts2(sources, extra);
13748
13808
  const files = {};
13749
- files["manifest.json"] = json(manifestOf(contract, manifestApp, handlers, names, model, served));
13809
+ files["manifest.json"] = json(manifestOf(contract, manifestApp, handlers, names, model, served, withOwnedReaders(contract, owned.readers), owned.known));
13750
13810
  if (app.data_model) files["data_model.json"] = json(dataModelOf(app.data_model));
13751
13811
  for (const name of names) {
13752
13812
  const path3 = `handlers/${name}.py`;
@@ -16437,7 +16497,7 @@ function measureProblems(name, row, model, handlers = {}) {
16437
16497
  const found = whereProblems(spec.where, EVENTS_SHAPE, at, who, {
16438
16498
  noColumn: (column) => `filters on "${column}", which is no column of the events; filter on at, action, handler, actor or note`
16439
16499
  });
16440
- problems.push(...found, ...eventHandlerProblems(spec.where, row.table, handlers, at, who, new Set(found.map((problem5) => problem5.pointer))));
16500
+ problems.push(...found, ...eventHandlerProblems(spec.where, row.table, handlers, at, who, new Set(found.map((problem6) => problem6.pointer))));
16441
16501
  } else problems.push(...whereProblems(spec.where, table, at, who, { platform: true }));
16442
16502
  }
16443
16503
  }
@@ -16559,7 +16619,7 @@ function statusColumn(model, row) {
16559
16619
  const status = table.columns.status;
16560
16620
  return status !== void 0 && status.enum === null ? "status" : null;
16561
16621
  }
16562
- function manifestOf(contract, app, handlers, names, model, served = []) {
16622
+ function manifestOf(contract, app, handlers, names, model, served = [], readers = contract, known = /* @__PURE__ */ new Set()) {
16563
16623
  const manifest = {
16564
16624
  apiVersion: app.api_version ?? DEFAULT_API_VERSION,
16565
16625
  id: app.id ?? null,
@@ -16585,7 +16645,7 @@ function manifestOf(contract, app, handlers, names, model, served = []) {
16585
16645
  }
16586
16646
  for (const name of served) {
16587
16647
  const entry = { http: "POST", entrypoint: `handlers/${name}.py` };
16588
- const { roles: gate } = sourceRoles(contract, name);
16648
+ const { roles: gate } = sourceRoles(readers, name, known);
16589
16649
  if (gate.length > 0) entry.requiresRole = gate;
16590
16650
  manifest.handlers[name] = entry;
16591
16651
  }
@@ -17693,6 +17753,7 @@ function validateContract(contract, catalog) {
17693
17753
  checkKind(app, problems);
17694
17754
  checkPreset(app, problems);
17695
17755
  if (kind === "dashboard") checkSources(doc, catalog, problems, skipped);
17756
+ checkSourceMetricIds(doc, catalog, problems, skipped);
17696
17757
  checkVocabulary(doc, kind, problems, renderer);
17697
17758
  checkPages(doc, kind, problems);
17698
17759
  if (kind === "dashboard") {
@@ -17715,22 +17776,22 @@ function validateContract(contract, catalog) {
17715
17776
  return { problems: dedupe(problems), skipped: dedupeSkipped(skipped), warnings: dedupe(warnings) };
17716
17777
  }
17717
17778
  function checkCustomDeclares(doc, problems) {
17718
- for (const problem5 of declaresProblems2(doc)) {
17779
+ for (const problem6 of declaresProblems2(doc)) {
17719
17780
  problems.push({
17720
- code: problem5.code,
17721
- pointer: problem5.pointer,
17722
- message: `${problem5.message}.`,
17781
+ code: problem6.code,
17782
+ pointer: problem6.pointer,
17783
+ message: `${problem6.message}.`,
17723
17784
  fix: 'Name only data.sources entries and data.app.handlers in declares \u2014 {"reads": [...], "calls": [...], "produces": {"<handler>": ["<key>", ...]}, "shows": [{"field": ..., "format": ...}]} (docs/appspace/components/catalog.md, custom).'
17724
17785
  });
17725
17786
  }
17726
17787
  }
17727
17788
  function checkTransforms(doc, problems) {
17728
- for (const problem5 of transformRowProblems2(obj(doc["data"]))) {
17729
- const row = problem5.code === "transform_row";
17789
+ for (const problem6 of transformRowProblems2(obj(doc["data"]))) {
17790
+ const row = problem6.code === "transform_row";
17730
17791
  problems.push({
17731
17792
  code: row ? "transform-row" : "transform-missing",
17732
- pointer: problem5.where,
17733
- message: `${problem5.message}.`,
17793
+ pointer: problem6.where,
17794
+ message: `${problem6.message}.`,
17734
17795
  fix: row ? "Make each change the message names, on this row and its dataset's read; a KPI, a plain read or a computed card takes no transform in this release." : "Name the file transforms/<name>.py \u2014 lowercase letters, digits and underscores, at most 64 of them, with the worker prefix (transforms/w3_revenue_ma.py)."
17735
17796
  });
17736
17797
  }
@@ -18030,6 +18091,30 @@ function checkPreset(app, problems) {
18030
18091
  fix: `Name one of ${PRESETS2.join(", ")}, or drop the key and take the default.`
18031
18092
  });
18032
18093
  }
18094
+ function checkSourceMetricIds(doc, catalog, problems, skipped) {
18095
+ const sources = obj(obj(doc["data"])["sources"]);
18096
+ const byId = [];
18097
+ for (const [name, row] of Object.entries(sources)) {
18098
+ const id = obj(obj(obj(row)["from"])["financeos"])["metric_id"];
18099
+ if (typeof id === "number" || typeof id === "string" && id.trim() !== "") byId.push([name, String(id).trim()]);
18100
+ }
18101
+ if (byId.length === 0) return;
18102
+ if (!checksMetrics(catalog)) {
18103
+ const why = catalog.source === "none" ? "no --catalog file and no -s server to read the metric list from" : catalog.checks_skipped.find((one) => one.what === "metric ids")?.why ?? "this tenant's metric list could not be read";
18104
+ if (!skipped.some((one) => one.what === "source metric ids")) skipped.push({ what: "source metric ids", why });
18105
+ return;
18106
+ }
18107
+ const known = new Set(catalog.metrics.map((metric) => metric.id));
18108
+ for (const [name, id] of byId) {
18109
+ if (known.has(id)) continue;
18110
+ problems.push({
18111
+ code: "unknown-metric",
18112
+ pointer: pointer("data", "sources", name, "from", "financeos", "metric_id"),
18113
+ message: `${id} is not a metric this company has.`,
18114
+ fix: `Use an id from this company's metric list, or declare the metric in data.metrics and read it as "@<key>", which ship binds in each company.`
18115
+ });
18116
+ }
18117
+ }
18033
18118
  function checkSources(doc, catalog, problems, skipped) {
18034
18119
  const widgets = obj(obj(doc["data"])["widgets"]);
18035
18120
  if (Object.keys(widgets).length === 0) return;
@@ -18271,9 +18356,9 @@ function calendarDateIn(filter) {
18271
18356
  }
18272
18357
  function epochOf(date) {
18273
18358
  const day = date.slice(0, 10);
18274
- const parsed = Date.parse(`${day}T00:00:00Z`);
18275
- if (Number.isNaN(parsed)) return void 0;
18276
- return String(Math.floor(parsed / 1e3));
18359
+ const parsed2 = Date.parse(`${day}T00:00:00Z`);
18360
+ if (Number.isNaN(parsed2)) return void 0;
18361
+ return String(Math.floor(parsed2 / 1e3));
18277
18362
  }
18278
18363
  function addressOf(source, hasAlias, hasId) {
18279
18364
  const declared = source["address"];
@@ -19235,11 +19320,11 @@ function walk3(node, path3, visit) {
19235
19320
  function dedupe(problems) {
19236
19321
  const seen = /* @__PURE__ */ new Set();
19237
19322
  const out = [];
19238
- for (const problem5 of problems) {
19239
- const key = `${problem5.code}\0${problem5.pointer}\0${problem5.message}`;
19323
+ for (const problem6 of problems) {
19324
+ const key = `${problem6.code}\0${problem6.pointer}\0${problem6.message}`;
19240
19325
  if (seen.has(key)) continue;
19241
19326
  seen.add(key);
19242
- out.push(problem5);
19327
+ out.push(problem6);
19243
19328
  }
19244
19329
  return out;
19245
19330
  }
@@ -20551,16 +20636,16 @@ async function readManifest(dir) {
20551
20636
  if (raw === void 0) {
20552
20637
  throw new Error(`no manifest.json in ${dir} \u2014 is this an app folder?`);
20553
20638
  }
20554
- let parsed;
20639
+ let parsed2;
20555
20640
  try {
20556
- parsed = JSON.parse(raw);
20641
+ parsed2 = JSON.parse(raw);
20557
20642
  } catch {
20558
20643
  throw new Error(`manifest.json in ${dir} is not valid JSON.`);
20559
20644
  }
20560
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
20645
+ if (typeof parsed2 !== "object" || parsed2 === null || Array.isArray(parsed2)) {
20561
20646
  throw new Error("manifest.json is not a JSON object.");
20562
20647
  }
20563
- const manifest = parsed;
20648
+ const manifest = parsed2;
20564
20649
  const id = manifest["id"];
20565
20650
  if (typeof id !== "string" || id === "") {
20566
20651
  throw new Error('manifest.json has no "id".');
@@ -20574,9 +20659,9 @@ async function readState(dir) {
20574
20659
  const raw = await readIfPresent(join5(dir, ".appspace.json"));
20575
20660
  if (raw === void 0) return {};
20576
20661
  try {
20577
- const parsed = JSON.parse(raw);
20578
- if (typeof parsed === "object" && parsed !== null) {
20579
- return parsed;
20662
+ const parsed2 = JSON.parse(raw);
20663
+ if (typeof parsed2 === "object" && parsed2 !== null) {
20664
+ return parsed2;
20580
20665
  }
20581
20666
  } catch {
20582
20667
  }
@@ -20693,8 +20778,8 @@ function summarise(contract) {
20693
20778
  }
20694
20779
  function describeFindings(contract) {
20695
20780
  let out = "";
20696
- for (const problem5 of contract.problems) {
20697
- out += ` ${problem5.lossy ? "lossy" : "ok "} ${problem5.code} at ${problem5.where}: ${problem5.message}
20781
+ for (const problem6 of contract.problems) {
20782
+ out += ` ${problem6.lossy ? "lossy" : "ok "} ${problem6.code} at ${problem6.where}: ${problem6.message}
20698
20783
  `;
20699
20784
  }
20700
20785
  for (const entry of contract.unresolved) {
@@ -21607,6 +21692,1082 @@ async function previewOne(document, file, opts, shared = {}) {
21607
21692
  import { readFile as readFile7 } from "fs/promises";
21608
21693
  import { join as join8 } from "path";
21609
21694
  import { gunzipSync, gzipSync } from "zlib";
21695
+
21696
+ // src/scopes/matcher.ts
21697
+ var SCOPE_RE = /^(\*|[a-z][a-z0-9-]*):(\*|read|write|destructive)$/;
21698
+ var ACTION_RANK = new Map(
21699
+ ["read", "write", "destructive"].map((action, rank) => [action, rank])
21700
+ );
21701
+ function actionSatisfies(granted, wanted) {
21702
+ if (granted === "*" || granted === wanted) return true;
21703
+ const g = ACTION_RANK.get(granted);
21704
+ const w = ACTION_RANK.get(wanted);
21705
+ if (g === void 0 || w === void 0) return false;
21706
+ return g > w;
21707
+ }
21708
+ var WHITESPACE = "\\t\\n\\v\\f\\r \\x1c-\\x1f\\x85\\xa0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff";
21709
+ var TRIM_RE = new RegExp(`^[${WHITESPACE}]+|[${WHITESPACE}]+$`, "g");
21710
+ var SPLIT_RE = new RegExp(`[${WHITESPACE}]+`);
21711
+ function trimShared(value) {
21712
+ return value.replace(TRIM_RE, "");
21713
+ }
21714
+ function splitScopeString(value) {
21715
+ return value.split(SPLIT_RE).filter((part) => part !== "");
21716
+ }
21717
+ function parseScope(value) {
21718
+ if (typeof value !== "string") return null;
21719
+ const m = SCOPE_RE.exec(trimShared(value).toLowerCase());
21720
+ return m ? [m[1], m[2]] : null;
21721
+ }
21722
+ function parsed(scopes) {
21723
+ const list3 = scopes ?? [];
21724
+ const entries = typeof list3 === "string" ? splitScopeString(list3) : Array.isArray(list3) ? list3 : [];
21725
+ return entries.map(parseScope).filter((p) => p !== null);
21726
+ }
21727
+ function scopeEntries(scopes) {
21728
+ return parsed(scopes);
21729
+ }
21730
+ function isAgentScoped(scopes) {
21731
+ return parsed(scopes).length > 0;
21732
+ }
21733
+ function isAllowed(scopes, resource, action) {
21734
+ const entries = parsed(scopes);
21735
+ if (entries.length === 0) return true;
21736
+ const res = trimShared(resource).toLowerCase();
21737
+ const act = trimShared(action).toLowerCase();
21738
+ return entries.some(([r, a]) => (r === "*" || r === res) && actionSatisfies(a, act));
21739
+ }
21740
+
21741
+ // src/commands/appspace/metrics.ts
21742
+ function metricsTransport(client) {
21743
+ const host = (client.host ?? client.baseUrl.replace(/\/api\/app-space$/, "")).replace(/\/+$/, "");
21744
+ return {
21745
+ serverKey: client.serverKey,
21746
+ host,
21747
+ appSpace: client.baseUrl,
21748
+ overridden: client.baseUrl !== `${host}/api/app-space`,
21749
+ email: client.email ?? null,
21750
+ async call(method, url, body) {
21751
+ const headers = {};
21752
+ if (body !== void 0) headers["content-type"] = "application/json";
21753
+ const res = await client.raw({
21754
+ method,
21755
+ url,
21756
+ headers,
21757
+ ...body === void 0 ? {} : { body: JSON.stringify(body) }
21758
+ });
21759
+ const text3 = res.body.toString("utf8");
21760
+ let parsed2 = text3 === "" ? null : text3;
21761
+ try {
21762
+ parsed2 = text3 === "" ? null : JSON.parse(text3);
21763
+ } catch {
21764
+ }
21765
+ return { status: res.status, body: parsed2 };
21766
+ },
21767
+ claims: () => client.claims === void 0 ? null : client.claims()
21768
+ };
21769
+ }
21770
+ function declaredMetrics(contract) {
21771
+ const data = obj2(obj2(contract)["data"]);
21772
+ const metrics = obj2(data["metrics"]);
21773
+ const out = {};
21774
+ for (const [key, decl] of Object.entries(metrics)) {
21775
+ if (isObj3(decl) && typeof decl["name"] === "string") out[key] = decl;
21776
+ }
21777
+ return out;
21778
+ }
21779
+ function metricSources(contract) {
21780
+ const sources = obj2(obj2(obj2(contract)["data"])["sources"]);
21781
+ const out = {};
21782
+ for (const [name, row] of Object.entries(sources)) {
21783
+ const metric = obj2(obj2(obj2(row)["from"])["financeos"])["metric"];
21784
+ if (typeof metric === "string" && metric.startsWith("@") && metric.length > 1) out[name] = metric.slice(1);
21785
+ }
21786
+ return out;
21787
+ }
21788
+ var ENTITY_RE2 = /&(quot|amp|lt|gt|#x27|#39);/g;
21789
+ var ENTITIES = { quot: '"', amp: "&", lt: "<", gt: ">", "#x27": "'", "#39": "'" };
21790
+ function decodeEntities2(value) {
21791
+ if (typeof value === "string") return value.replace(ENTITY_RE2, (_m, name) => ENTITIES[name] ?? "");
21792
+ if (Array.isArray(value)) return value.map(decodeEntities2);
21793
+ if (isObj3(value)) return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, decodeEntities2(v)]));
21794
+ return value;
21795
+ }
21796
+ function listOf(body) {
21797
+ const rows2 = Array.isArray(body) ? body : isObj3(body) && Array.isArray(body["results"]) ? body["results"] : null;
21798
+ return rows2 === null ? [] : rows2.filter(isObj3);
21799
+ }
21800
+ function idIn(value) {
21801
+ if (typeof value === "number" && Number.isInteger(value)) return value;
21802
+ if (typeof value === "string" && /^\d+$/.test(value)) return Number(value);
21803
+ if (isObj3(value)) return idIn(value["id"]);
21804
+ return null;
21805
+ }
21806
+ function catalogRow(raw) {
21807
+ const id = idIn(raw["id"]);
21808
+ const name = typeof raw["name"] === "string" ? raw["name"] : null;
21809
+ if (id === null || name === null) return null;
21810
+ const configuration = obj2(raw["configuration"]);
21811
+ const status = obj2(raw["status_info"]);
21812
+ const filters = (Array.isArray(raw["filters"]) ? raw["filters"] : []).filter(isObj3).flatMap((one) => {
21813
+ const field = idIn(one["field"]);
21814
+ if (field === null) return [];
21815
+ const values = Array.isArray(one["values"]) ? one["values"].map(String) : one["values"] === void 0 || one["values"] === null ? [] : [String(one["values"])];
21816
+ return [{ field, values, is_excluded: one["is_excluded"] === true, allow_nulls: one["allow_nulls"] === true }];
21817
+ });
21818
+ return {
21819
+ id,
21820
+ name,
21821
+ kind: typeof raw["kind"] === "string" ? raw["kind"].toUpperCase() : "",
21822
+ template: idIn(raw["template"] ?? raw["template_id"]),
21823
+ value_field: idIn(raw["value_field"]),
21824
+ date_field: idIn(raw["date_field"]),
21825
+ aggregator: typeof raw["aggregator"] === "string" ? raw["aggregator"].toUpperCase() : idIn(raw["aggregator"]) === null && isObj3(raw["aggregator"]) && typeof raw["aggregator"]["name"] === "string" ? String(raw["aggregator"]["name"]).toUpperCase() : null,
21826
+ is_cumulative: raw["is_cumulative"] === true,
21827
+ time_aggregation: typeof raw["time_aggregation"] === "string" ? raw["time_aggregation"].toUpperCase() : null,
21828
+ filters,
21829
+ formula: typeof configuration["presented_formula"] === "string" && configuration["presented_formula"] !== "" ? configuration["presented_formula"] : null,
21830
+ format: typeof configuration["format"] === "string" ? configuration["format"] : null,
21831
+ base_metrics: (Array.isArray(raw["base_metrics"]) ? raw["base_metrics"] : []).map(idIn).filter((one) => one !== null),
21832
+ description: typeof raw["description"] === "string" ? raw["description"] : null,
21833
+ status: typeof status["status"] === "string" ? status["status"].toUpperCase() : null,
21834
+ created_at: typeof raw["created_at"] === "string" ? raw["created_at"] : null
21835
+ };
21836
+ }
21837
+ function authTableRow(raw) {
21838
+ const id = idIn(raw["id"]);
21839
+ if (id === null) return null;
21840
+ const fields = Array.isArray(raw["fields"]) ? raw["fields"].filter(isObj3).map(
21841
+ (one) => ({
21842
+ id: idIn(one["id"]),
21843
+ name: typeof one["name"] === "string" ? one["name"] : "",
21844
+ alias: typeof one["alias"] === "string" && one["alias"].trim() !== "" ? one["alias"].trim() : null
21845
+ })
21846
+ ) : null;
21847
+ return {
21848
+ id,
21849
+ name: typeof raw["name"] === "string" ? raw["name"] : String(id),
21850
+ alias: typeof raw["alias"] === "string" && raw["alias"].trim() !== "" ? raw["alias"].trim() : null,
21851
+ blocked: raw["authoring_blocked"] === true,
21852
+ fields: fields !== null && fields.length > 0 ? fields : null
21853
+ };
21854
+ }
21855
+ var MetricsReadError = class extends Error {
21856
+ };
21857
+ async function readJson2(t, url, what) {
21858
+ let answer;
21859
+ try {
21860
+ answer = await t.call("GET", url);
21861
+ } catch (err) {
21862
+ throw new MetricsReadError(`${what} could not be read (${firstLine(err)})`);
21863
+ }
21864
+ if (answer.status < 200 || answer.status >= 300) {
21865
+ throw new MetricsReadError(`${what} could not be read (HTTP ${String(answer.status)}${sentenceOf(answer.body) === "" ? "" : `: ${sentenceOf(answer.body)}`})`);
21866
+ }
21867
+ return decodeEntities2(answer.body);
21868
+ }
21869
+ async function readCatalog(t) {
21870
+ const body = await readJson2(t, `${t.host}/api/insights/metrics`, "this company's Datarails metrics");
21871
+ return listOf(body).map(catalogRow).filter((one) => one !== null);
21872
+ }
21873
+ async function readAuthTables(t) {
21874
+ const body = await readJson2(t, `${t.host}/api/insights/metrics/auth_tables`, "the tables you may build metrics on");
21875
+ return listOf(body).map(authTableRow).filter((one) => one !== null);
21876
+ }
21877
+ async function readTableFields(t, id) {
21878
+ const body = obj2(await readJson2(t, `${t.appSpace}/financeos/tables/${String(id)}`, `the fields of table ${String(id)}`));
21879
+ if (body["reachable"] !== true) throw new MetricsReadError(`the fields of table ${String(id)} could not be read (FinanceOS did not answer)`);
21880
+ return (Array.isArray(body["fields"]) ? body["fields"] : []).filter(isObj3).map(
21881
+ (one) => ({
21882
+ id: idIn(one["id"]),
21883
+ name: typeof one["name"] === "string" ? one["name"] : "",
21884
+ alias: typeof one["alias"] === "string" && one["alias"].trim() !== "" ? one["alias"].trim() : null
21885
+ })
21886
+ );
21887
+ }
21888
+ var AGGREGATES = {
21889
+ sum: ["SUM", false],
21890
+ count: ["COUNT", false],
21891
+ cumulative_sum: ["SUM", true],
21892
+ cumulative_count: ["COUNT", true]
21893
+ };
21894
+ function baseReuseKey(def) {
21895
+ const time = rollupOf(def);
21896
+ const filters = def.filters.map((one) => [one.field, [...one.values].map(String).sort(), one.is_excluded, one.allow_nulls]).sort((a, b) => a[0] - b[0] || JSON.stringify(a).localeCompare(JSON.stringify(b)));
21897
+ return `base:${JSON.stringify([def.template, def.value_field, def.date_field, def.aggregator.toUpperCase(), time, def.is_cumulative, filters])}`;
21898
+ }
21899
+ function rollupOf(def) {
21900
+ return (def.time_aggregation ?? (def.is_cumulative ? "EOP" : "SUM")).toUpperCase();
21901
+ }
21902
+ var OPERAND_RE = /'([^']*)'|\[([^[\]]*)\]/g;
21903
+ var MARK = "";
21904
+ function calcSkeleton(formula, idOf4) {
21905
+ let missing = false;
21906
+ const ids = /* @__PURE__ */ new Set();
21907
+ const marked2 = formula.replace(OPERAND_RE, (_m, quoted3, bracketed) => {
21908
+ const name = quoted3 ?? bracketed ?? "";
21909
+ if (SYSTEM_VARIABLES.includes(name)) return `${MARK}${name.toLowerCase()}${MARK}`;
21910
+ const id = idOf4(name);
21911
+ if (id === null) {
21912
+ missing = true;
21913
+ return MARK + MARK;
21914
+ }
21915
+ ids.add(id);
21916
+ return `${MARK}#${String(id)}${MARK}`;
21917
+ });
21918
+ if (missing) return null;
21919
+ const skeleton = marked2.split(MARK).map((part, index) => index % 2 === 1 ? part : part.toLowerCase().replace(/\s+/g, "")).join("");
21920
+ return { skeleton, ids: [...ids].sort((a, b) => a - b) };
21921
+ }
21922
+ function calcReuseKey(formula, idOf4) {
21923
+ const shape = calcSkeleton(formula, idOf4);
21924
+ return shape === null ? null : `calc:${shape.skeleton}|${shape.ids.join(",")}`;
21925
+ }
21926
+ function rowReuseKey(row, byName) {
21927
+ if (row.kind === "BASE") {
21928
+ if (row.template === null || row.value_field === null || row.date_field === null || row.aggregator === null) return null;
21929
+ return baseReuseKey({
21930
+ template: row.template,
21931
+ value_field: row.value_field,
21932
+ date_field: row.date_field,
21933
+ aggregator: row.aggregator,
21934
+ is_cumulative: row.is_cumulative,
21935
+ time_aggregation: row.time_aggregation,
21936
+ filters: row.filters
21937
+ });
21938
+ }
21939
+ if (row.kind === "CALC" && row.formula !== null) return calcReuseKey(row.formula, (name) => byName.get(name)?.id ?? null);
21940
+ return null;
21941
+ }
21942
+ var METRIC_CREATED_FOOTER = "Created by App Space for the app";
21943
+ var METRIC_CREATED_CATEGORY = "App Space";
21944
+ function createdFooter(appName) {
21945
+ return `${METRIC_CREATED_FOOTER} "${appName === "" ? "this app" : appName}".`;
21946
+ }
21947
+ var METRIC_SHIP_REFUSALS = {
21948
+ metric_name_taken: 'this company already has a metric of that name with a different definition, which App Space may not change in place (it did not create it through you, another app or key reads it, or the published version does) \u2014 use theirs with "reuse": "name", or name yours differently',
21949
+ metric_missing: 'a declaration with "create": false names a metric this company does not have',
21950
+ metric_scope: "creating or changing a metric needs the Datarails scope metrics:write, and this session's credential ceiling does not allow it",
21951
+ metric_table: "a BASE names a table you cannot build metrics on: not one of yours, or a permission filter applies to you (authoring blocked)",
21952
+ metric_field: "a BASE names a field its table does not have, or one with no numeric id",
21953
+ metric_preview: "Datarails could not evaluate a CALC's formula (preview_calc_data) \u2014 its own sentence",
21954
+ metric_operand: "a CALC reads a declaration this ship could not bind",
21955
+ metric_unchecked: "this company's metrics could not be read, so nothing could be bound: ship again shortly",
21956
+ metric_create: "Datarails refused to create or change a metric after the checks passed; the metrics already created are kept, and the next ship reuses them",
21957
+ metric_changed: "a metric this ship created or changed, read back from Datarails before the push, does not hold the definition it wrote (another ship or person changed it meanwhile, or Datarails did not take the whole change); nothing is pushed, and the metrics are kept"
21958
+ };
21959
+ var FIXES = {
21960
+ metric_name_taken: `Use theirs with "reuse": "name", or give the contract's metric another name.`,
21961
+ metric_missing: 'Create the metric in Datarails, or give the declaration a definition and drop "create": false.',
21962
+ metric_scope: `Ask the agent's owner to add metrics:write to its CLI scope ceiling, or create the metric in Datarails and declare it with "reuse": "name".`,
21963
+ metric_table: "Ask an admin for access to the table, or build the metric on a table you can author on.",
21964
+ metric_field: "Name a field of that table by its alias or its name.",
21965
+ metric_preview: "Fix the formula so Datarails can evaluate it.",
21966
+ metric_operand: "Fix the declaration it reads first.",
21967
+ metric_unchecked: "Ship again shortly; nothing was created.",
21968
+ metric_create: "Fix what it names and ship again; the metrics already created are kept, and the next ship reuses them.",
21969
+ metric_changed: "Ship again: the next plan starts from what Datarails holds now. If it happens again, someone else is changing this metric; check it in Datarails."
21970
+ };
21971
+ function problem4(code, key, message) {
21972
+ return {
21973
+ code,
21974
+ pointer: key === null ? pointer("data", "metrics") : pointer("data", "metrics", key),
21975
+ message: message.endsWith(".") ? message : `${message}.`,
21976
+ fix: FIXES[code] ?? "Change data.metrics so this company can bind it."
21977
+ };
21978
+ }
21979
+ function dependencyOrder(decls) {
21980
+ const keys2 = Object.keys(decls).sort();
21981
+ const out = [];
21982
+ const seen = /* @__PURE__ */ new Set();
21983
+ const visit = (key, path3) => {
21984
+ if (seen.has(key) || path3.has(key)) return;
21985
+ path3.add(key);
21986
+ for (const dep of operandsOf(decls, key)) if (dep in decls) visit(dep, path3);
21987
+ path3.delete(key);
21988
+ seen.add(key);
21989
+ out.push(key);
21990
+ };
21991
+ for (const key of keys2) visit(key, /* @__PURE__ */ new Set());
21992
+ return out;
21993
+ }
21994
+ function operandsOf(decls, key) {
21995
+ const decl = decls[key];
21996
+ if (decl === void 0 || decl.kind !== "calc" || typeof decl.formula !== "string") return [];
21997
+ const found = metricFormulaProblems(decl.formula, Object.keys(decls));
21998
+ return found.operands;
21999
+ }
22000
+ function credentialOf(claims) {
22001
+ const scopes = claims === null ? void 0 : claims["scopes"];
22002
+ const scoped = isAgentScoped(scopes);
22003
+ const entries = scopeEntries(scopes).map(([resource, action]) => `${resource}:${action}`);
22004
+ const canWrite = isAllowed(scopes, "metrics", "write");
22005
+ if (claims === null) {
22006
+ return { scoped: false, can_write: true, scopes: [], line: "scope ceiling not read (this session holds no JWT yet); Datarails decides on create" };
22007
+ }
22008
+ if (!scoped) return { scoped: false, can_write: true, scopes: [], line: "can create metrics (no scope ceiling on this session)" };
22009
+ if (canWrite) return { scoped: true, can_write: true, scopes: entries, line: `can create metrics (this session's scope ceiling, ${entries.join(", ")}, allows metrics:write)` };
22010
+ return { scoped: true, can_write: false, scopes: entries, line: `cannot create or change metrics: this session's scope ceiling (${entries.join(", ")}) does not allow metrics:write` };
22011
+ }
22012
+ function scopeSentence(key, name, credential, verb) {
22013
+ return `metric "${key}": ${verb} "${name}" needs the Datarails scope metrics:write; this build's credential allows ${credential.scopes.join(", ") || "no write scope"}. Ask the agent's owner to add metrics:write to its CLI scope ceiling, or create the metric in Datarails and declare it with "reuse": "name"`;
22014
+ }
22015
+ function findAuthTable(tables, ref) {
22016
+ if (ref === void 0) return void 0;
22017
+ if (typeof ref.id === "number") return tables.find((one) => one.id === ref.id);
22018
+ const want = (ref.alias ?? ref.name ?? "").trim();
22019
+ if (want === "") return void 0;
22020
+ const exact = tables.find((one) => ref.alias !== void 0 ? one.alias === want : one.name === want);
22021
+ if (exact !== void 0) return exact;
22022
+ const lower = want.toLowerCase();
22023
+ return tables.find((one) => ref.alias !== void 0 ? (one.alias ?? "").toLowerCase() === lower : one.name.toLowerCase() === lower);
22024
+ }
22025
+ function findField(fields, named13) {
22026
+ const want = named13.trim();
22027
+ const lower = want.toLowerCase();
22028
+ return fields.find((one) => one.alias === want) ?? fields.find((one) => one.name === want) ?? fields.find((one) => (one.alias ?? "").toLowerCase() === lower) ?? fields.find((one) => one.name.toLowerCase() === lower);
22029
+ }
22030
+ function tableLabel(ref, found) {
22031
+ if (found !== void 0) return found.name;
22032
+ return ref?.alias ?? ref?.name ?? (ref?.id === void 0 ? "?" : `table ${String(ref.id)}`);
22033
+ }
22034
+ function describeBase(decl, tableName) {
22035
+ const filters = (decl.filters ?? []).map((one) => `${one.field} ${one.exclude === true ? "not in" : "in"} [${one.values.map(String).join(", ")}]`).join("; ");
22036
+ return `BASE on "${tableName}" (${decl.value ?? "?"} by ${decl.date ?? "?"}, ${(decl.aggregate ?? "?").toUpperCase()}${decl.time_aggregation === void 0 ? "" : `, ${decl.time_aggregation.toUpperCase()} over time`})${filters === "" ? "" : ` where ${filters}`}`;
22037
+ }
22038
+ function describeRow(row, tables) {
22039
+ if (row.kind === "CALC") return `CALC ${row.formula ?? "(no formula)"}`;
22040
+ if (row.kind !== "BASE") return row.kind === "" ? "a metric" : `a ${row.kind} metric`;
22041
+ const table = tables?.find((one) => one.id === row.template);
22042
+ const field = (id) => {
22043
+ const found = table?.fields?.find((one) => one.id === id);
22044
+ return found === void 0 ? `field ${String(id)}` : found.alias ?? found.name;
22045
+ };
22046
+ const filters = row.filters.map((one) => `${field(one.field)} ${one.is_excluded ? "not in" : "in"} [${one.values.join(", ")}]`).join("; ");
22047
+ const aggregate = `${row.is_cumulative ? "CUMULATIVE_" : ""}${row.aggregator ?? "?"}`;
22048
+ return `BASE on "${table?.name ?? `table ${String(row.template)}`}" (${field(row.value_field)} by ${field(row.date_field)}, ${aggregate}${row.time_aggregation === null ? "" : `, ${row.time_aggregation} over time`})${filters === "" ? "" : ` where ${filters}`}`;
22049
+ }
22050
+ async function readUsage(t, appId, metricId) {
22051
+ let answer;
22052
+ try {
22053
+ answer = await t.call("GET", `${t.appSpace}/apps/${encodeURIComponent(appId)}/metric-bindings/usage?metric_id=${String(metricId)}`);
22054
+ } catch (err) {
22055
+ return `App Space could not be asked whether it may change it (${firstLine(err)})`;
22056
+ }
22057
+ if (answer.status === 404) return "App Space holds no binding of this app it could show you (a first ship, a server without the usage read, or an app that is not yours)";
22058
+ if (answer.status !== 200) return `App Space could not say whether it may change it (HTTP ${String(answer.status)})`;
22059
+ const one = obj2(obj2(obj2(answer.body)["metrics"])[String(metricId)]);
22060
+ const app = obj2(one["this_app"]);
22061
+ return {
22062
+ created_by_app_space: one["created_by_app_space"] === true,
22063
+ created_by_you: one["created_by_you"] === true,
22064
+ keys: Array.isArray(app["keys"]) ? app["keys"].map(String) : [],
22065
+ published: app["published"] === true,
22066
+ read_elsewhere: one["read_elsewhere"] === true,
22067
+ in_place_allowed: one["in_place_allowed"] === true
22068
+ };
22069
+ }
22070
+ function editRefusal(usage, key, row) {
22071
+ if (typeof usage === "string") return usage;
22072
+ if (!usage.created_by_app_space) return "App Space did not create it";
22073
+ if (!usage.created_by_you) return "App Space created it for someone else, and only its creator may change it in place";
22074
+ if (usage.read_elsewhere) return "another app reads it";
22075
+ if (usage.keys.some((one) => one !== key)) return `this app also reads it as ${usage.keys.filter((one) => one !== key).map((one) => `"${one}"`).join(", ")}`;
22076
+ if (usage.published) return "the app's published version reads it, and its viewers would see the change before you publish";
22077
+ if (!usage.in_place_allowed) return "App Space does not allow it";
22078
+ if (!(row.description ?? "").includes(METRIC_CREATED_FOOTER)) return "its description no longer carries App Space's marker, so the push would not keep it as App Space's";
22079
+ return null;
22080
+ }
22081
+ async function planMetrics(contract, t, opts) {
22082
+ const decls = declaredMetrics(contract);
22083
+ const catalog = await readCatalog(t);
22084
+ const claims = t.claims();
22085
+ const credential = credentialOf(claims);
22086
+ const byName = new Map(catalog.map((row) => [row.name, row]));
22087
+ const byId = new Map(catalog.map((row) => [row.id, row]));
22088
+ const rowKeys2 = new Map(catalog.map((row) => [row.id, rowReuseKey(row, byName)]));
22089
+ const plan = {
22090
+ server: t.serverKey,
22091
+ host: t.host,
22092
+ org: typeof claims?.["organization_id"] === "number" ? claims["organization_id"] : null,
22093
+ user: typeof claims?.["email"] === "string" ? claims["email"] : t.email,
22094
+ app_id: opts.appId,
22095
+ entries: [],
22096
+ credential,
22097
+ not_checked: [],
22098
+ notes: [],
22099
+ problems: [],
22100
+ internal: { decls, catalog, tables: null, base: {}, tableOf: {}, appName: opts.appName }
22101
+ };
22102
+ if (t.overridden) {
22103
+ plan.notes.push(`metrics are read and created on ${t.host} (the Datarails host of ${t.serverKey}), not on --app-space-host ${t.appSpace}`);
22104
+ }
22105
+ const needsTables = Object.values(decls).some((decl) => decl.kind === "base" && decl.table !== void 0);
22106
+ let tables = null;
22107
+ let tablesWhy = null;
22108
+ if (needsTables) {
22109
+ try {
22110
+ tables = await readAuthTables(t);
22111
+ } catch (err) {
22112
+ tablesWhy = err instanceof Error ? err.message : String(err);
22113
+ }
22114
+ }
22115
+ plan.internal.tables = tables;
22116
+ const fieldsRead = /* @__PURE__ */ new Map();
22117
+ const fieldsOf = async (table) => {
22118
+ if (table.fields !== null) return table.fields;
22119
+ const held = fieldsRead.get(table.id);
22120
+ if (held !== void 0) return held;
22121
+ let got;
22122
+ try {
22123
+ got = await readTableFields(t, table.id);
22124
+ } catch (err) {
22125
+ got = err instanceof Error ? err.message : String(err);
22126
+ }
22127
+ fieldsRead.set(table.id, got);
22128
+ return got;
22129
+ };
22130
+ const resolved = /* @__PURE__ */ new Map();
22131
+ for (const key of dependencyOrder(decls)) {
22132
+ const decl = decls[key];
22133
+ const kind = decl.kind === "base" || decl.kind === "calc" ? decl.kind : null;
22134
+ const entry = { key, name: decl.name, kind, action: "refuse", metric_id: null, metric_name: null, origin: null, reuse_key: null, definition: "", note: "" };
22135
+ const refuse2 = (code, message, note) => {
22136
+ entry.action = "refuse";
22137
+ entry.code = code;
22138
+ entry.message = message;
22139
+ entry.note = note ?? message;
22140
+ plan.problems.push(problem4(code, key, message));
22141
+ };
22142
+ resolved.set(key, entry);
22143
+ let wire = null;
22144
+ let pendingOperands = [];
22145
+ let defProblem = null;
22146
+ if (kind === "calc" && typeof decl.formula === "string") {
22147
+ const operands = operandsOf(decls, key);
22148
+ const names = {};
22149
+ const ids = {};
22150
+ for (const operand of operands) {
22151
+ const dep = resolved.get(operand);
22152
+ if (dep === void 0 || dep.action === "refuse") {
22153
+ defProblem = { code: "metric_operand", message: `metric "${key}" reads [@${operand}], which this ship cannot bind (see "${operand}")` };
22154
+ break;
22155
+ }
22156
+ if (dep.action === "create") {
22157
+ pendingOperands.push(operand);
22158
+ names[operand] = dep.name;
22159
+ } else {
22160
+ names[operand] = dep.metric_name ?? dep.name;
22161
+ if (dep.metric_id !== null) ids[operand] = dep.metric_id;
22162
+ }
22163
+ }
22164
+ if (defProblem === null) {
22165
+ try {
22166
+ wire = wireFormula(decl.formula, names);
22167
+ } catch (err) {
22168
+ defProblem = { code: "metric_preview", message: `metric "${key}"'s formula ${firstLine(err)}` };
22169
+ }
22170
+ }
22171
+ if (wire !== null) {
22172
+ entry.definition = `CALC ${wire}`;
22173
+ if (pendingOperands.length === 0) {
22174
+ const nameToId = new Map(Object.entries(names).map(([operand, name]) => [name, ids[operand] ?? null]));
22175
+ entry.reuse_key = calcReuseKey(wire, (name) => nameToId.get(name) ?? null);
22176
+ }
22177
+ }
22178
+ } else if (kind === "base") {
22179
+ const table = tables === null ? void 0 : findAuthTable(tables, decl.table);
22180
+ entry.definition = describeBase(decl, tableLabel(decl.table, table));
22181
+ if (tables === null) {
22182
+ defProblem = { code: "metric_table", message: `metric "${key}": ${tablesWhy ?? "the tables could not be read"}` };
22183
+ } else if (table === void 0) {
22184
+ defProblem = { code: "metric_table", message: `metric "${key}": this company has no table "${tableLabel(decl.table)}" you can build metrics on` };
22185
+ } else {
22186
+ plan.internal.tableOf[key] = table;
22187
+ const fields = await fieldsOf(table);
22188
+ if (typeof fields === "string") {
22189
+ defProblem = { code: "metric_field", message: `metric "${key}": ${fields}` };
22190
+ } else {
22191
+ const want = [decl.value ?? "", decl.date ?? "", ...(decl.filters ?? []).map((one) => one.field)];
22192
+ const found = want.map((named14) => findField(fields, named14));
22193
+ const missing = want.find((_named, index) => found[index] === void 0);
22194
+ const noId = found.find((one) => one !== void 0 && one.id === null);
22195
+ const aggregate = AGGREGATES[(decl.aggregate ?? "").toLowerCase()];
22196
+ if (missing !== void 0) {
22197
+ defProblem = { code: "metric_field", message: `metric "${key}": the table "${table.name}" has no field "${missing}" (by alias or name)` };
22198
+ } else if (noId !== void 0) {
22199
+ defProblem = { code: "metric_field", message: `metric "${key}": the field "${noId.alias ?? noId.name}" of "${table.name}" has no numeric id, so it cannot be a BASE operand` };
22200
+ } else if (aggregate === void 0) {
22201
+ defProblem = { code: "metric_field", message: `metric "${key}": ${JSON.stringify(decl.aggregate)} is not an aggregate Datarails takes` };
22202
+ } else {
22203
+ const def = {
22204
+ template: table.id,
22205
+ value_field: found[0]?.id,
22206
+ date_field: found[1]?.id,
22207
+ aggregator: aggregate[0],
22208
+ is_cumulative: aggregate[1],
22209
+ time_aggregation: decl.time_aggregation === void 0 ? null : decl.time_aggregation.toUpperCase(),
22210
+ filters: (decl.filters ?? []).map((one, index) => ({
22211
+ field: found[index + 2]?.id,
22212
+ values: one.values.map(String),
22213
+ is_excluded: one.exclude === true,
22214
+ allow_nulls: false
22215
+ }))
22216
+ };
22217
+ plan.internal.base[key] = def;
22218
+ entry.reuse_key = baseReuseKey(def);
22219
+ }
22220
+ }
22221
+ }
22222
+ }
22223
+ const named13 = byName.get(decl.name);
22224
+ if (entry.reuse_key !== null) {
22225
+ const same5 = catalog.filter((row) => rowKeys2.get(row.id) === entry.reuse_key).sort((a, b) => Number(b.name === decl.name) - Number(a.name === decl.name) || (a.created_at ?? "").localeCompare(b.created_at ?? "") || a.id - b.id);
22226
+ const hit = same5[0];
22227
+ if (hit !== void 0) {
22228
+ entry.action = "reuse";
22229
+ entry.metric_id = hit.id;
22230
+ entry.metric_name = hit.name;
22231
+ entry.origin = "reused";
22232
+ entry.note = hit.name === decl.name ? "the same definition" : `the same definition, named "${hit.name}" in this company (not a clash)`;
22233
+ continue;
22234
+ }
22235
+ }
22236
+ if (decl.reuse === "name" && named13 !== void 0) {
22237
+ entry.action = "bind";
22238
+ entry.metric_id = named13.id;
22239
+ entry.metric_name = named13.name;
22240
+ entry.origin = "named";
22241
+ const theirs = describeRow(named13, tables);
22242
+ entry.note = kind === null ? "by name" : entry.reuse_key === null ? `by name (the contract's definition could not be compared here: ${defProblem?.message ?? "an operand is created on push"})` : `by name; its definition differs: ${theirs} against the contract's ${entry.definition}`;
22243
+ continue;
22244
+ }
22245
+ if (defProblem !== null) {
22246
+ refuse2(defProblem.code, defProblem.message);
22247
+ continue;
22248
+ }
22249
+ if (named13 !== void 0 && kind !== null) {
22250
+ const theirs = describeRow(named13, tables);
22251
+ const sameKind = named13.kind === kind.toUpperCase();
22252
+ const previous = rowKeys2.get(named13.id) ?? null;
22253
+ let why = null;
22254
+ if (decl.create === false) why = 'the declaration says "create": false, so this ship writes nothing';
22255
+ else if (!sameKind) why = `it is a ${named13.kind || "different"} metric, and a ${kind.toUpperCase()} cannot replace it in place`;
22256
+ else if (opts.appId === null || opts.appId === "") why = "the contract names no app id to ask App Space about";
22257
+ else if (previous === null) why = "its current definition could not be read in full, so a change could not be recorded";
22258
+ else if (entry.reuse_key === null && pendingOperands.length === 0) why = "the contract's definition could not be compared";
22259
+ else why = editRefusal(await readUsage(t, opts.appId, named13.id), key, named13);
22260
+ if (why === null) {
22261
+ if (!credential.can_write) {
22262
+ refuse2("metric_scope", scopeSentence(key, decl.name, credential, "changing"), `would change in place, but ${credential.line}`);
22263
+ continue;
22264
+ }
22265
+ entry.action = "edit";
22266
+ entry.metric_id = named13.id;
22267
+ entry.metric_name = named13.name;
22268
+ entry.origin = "created";
22269
+ entry.previous_reuse_key = previous;
22270
+ entry.old_definition = theirs;
22271
+ entry.new_definition = entry.definition;
22272
+ entry.note = `in place (App Space made it for this app): was ${theirs}; becomes ${entry.definition}`;
22273
+ if (kind === "calc" && wire !== null && pendingOperands.length === 0) {
22274
+ const preview2 = await previewFormula(t, wire);
22275
+ if (preview2 !== null) {
22276
+ refuse2("metric_preview", `metric "${key}": Datarails could not evaluate ${wire}: ${preview2}`);
22277
+ continue;
22278
+ }
22279
+ entry.preview = "ok";
22280
+ } else if (pendingOperands.length > 0) {
22281
+ entry.after = pendingOperands;
22282
+ entry.preview = "not checked";
22283
+ plan.not_checked.push(`the formula of "${decl.name}" (its operand ${pendingOperands.map((one) => `"${resolved.get(one)?.name ?? one}"`).join(", ")} is created on push)`);
22284
+ }
22285
+ continue;
22286
+ }
22287
+ const message = `metric "${key}": this company already has a metric named "${named13.name}" (id ${String(named13.id)}) with a different definition: ${theirs} against the contract's ${entry.definition}. Either use theirs ("reuse": "name"), or give the contract's metric another name (App Space may not change it in place: ${why})`;
22288
+ refuse2("metric_name_taken", message, `name taken by id ${String(named13.id)} with another definition: see above`);
22289
+ continue;
22290
+ }
22291
+ if (decl.create === false || kind === null) {
22292
+ refuse2("metric_missing", `metric "${key}": this company has no metric named "${decl.name}"; create it in Datarails, or give the declaration a definition`);
22293
+ continue;
22294
+ }
22295
+ if (!credential.can_write) {
22296
+ refuse2("metric_scope", scopeSentence(key, decl.name, credential, "creating"), `would create, but ${credential.line}`);
22297
+ continue;
22298
+ }
22299
+ if (kind === "base") {
22300
+ const table = plan.internal.tableOf[key];
22301
+ if (table?.blocked === true) {
22302
+ refuse2("metric_table", `metric "${key}": you can't build metrics on the table "${table.name}" (a permission filter applies to you); ask an admin for access`);
22303
+ continue;
22304
+ }
22305
+ entry.action = "create";
22306
+ entry.note = `${entry.definition}; extracts after creation`;
22307
+ continue;
22308
+ }
22309
+ entry.action = "create";
22310
+ if (pendingOperands.length > 0) {
22311
+ entry.after = pendingOperands;
22312
+ entry.preview = "not checked";
22313
+ entry.note = `${entry.definition}; created after ${pendingOperands.map((one) => `"${one}"`).join(", ")}`;
22314
+ plan.not_checked.push(`the formula of "${decl.name}" (its operand ${pendingOperands.map((one) => `"${resolved.get(one)?.name ?? one}"`).join(", ")} is created on push)`);
22315
+ continue;
22316
+ }
22317
+ const preview = await previewFormula(t, wire);
22318
+ if (preview !== null) {
22319
+ refuse2("metric_preview", `metric "${key}": Datarails could not evaluate ${wire}: ${preview}`);
22320
+ continue;
22321
+ }
22322
+ entry.preview = "ok";
22323
+ entry.note = `${entry.definition} preview ok`;
22324
+ }
22325
+ const order2 = Object.keys(decls);
22326
+ plan.entries = [...resolved.values()].sort((a, b) => order2.indexOf(a.key) - order2.indexOf(b.key));
22327
+ for (const entry of plan.entries) {
22328
+ if (entry.metric_id !== null && !byId.has(entry.metric_id)) entry.metric_id = null;
22329
+ }
22330
+ return plan;
22331
+ }
22332
+ async function previewFormula(t, wire) {
22333
+ let answer;
22334
+ try {
22335
+ answer = await t.call("PUT", `${t.host}/api/insights/metrics/preview_calc_data`, { formula: wire, time_aggregation: "SUM" });
22336
+ } catch (err) {
22337
+ return `the preview could not be run (${firstLine(err)})`;
22338
+ }
22339
+ const body = obj2(decodeEntities2(answer.body));
22340
+ if (answer.status >= 200 && answer.status < 300 && body["success"] !== false) return null;
22341
+ return sentenceOf(body) || `HTTP ${String(answer.status)}`;
22342
+ }
22343
+ var METRICS_WAIT_CAP = 120;
22344
+ var METRICS_WAIT_DEFAULT = 60;
22345
+ var POLL_MS = 5e3;
22346
+ function metricsWaitOf(raw) {
22347
+ if (raw === void 0 || raw === "") return { seconds: METRICS_WAIT_DEFAULT, note: null };
22348
+ const value = typeof raw === "number" ? raw : Number(raw);
22349
+ if (!Number.isFinite(value) || value < 0) {
22350
+ throw new Error(`--metrics-wait ${String(raw)} is not a number of seconds; give 0 to ${String(METRICS_WAIT_CAP)}`);
22351
+ }
22352
+ if (value > METRICS_WAIT_CAP) {
22353
+ return { seconds: METRICS_WAIT_CAP, note: `--metrics-wait ${String(raw)} is over the cap; waiting at most ${String(METRICS_WAIT_CAP)} s` };
22354
+ }
22355
+ return { seconds: Math.floor(value), note: null };
22356
+ }
22357
+ async function applyPlan(plan, t, opts) {
22358
+ const out = { ok: true, bindings: {}, created: [], problems: [], notes: [] };
22359
+ if (plan.problems.length > 0) {
22360
+ out.ok = false;
22361
+ out.problems = [...plan.problems];
22362
+ return out;
22363
+ }
22364
+ const { decls } = plan.internal;
22365
+ const byKey2 = new Map(plan.entries.map((entry) => [entry.key, entry]));
22366
+ let category;
22367
+ const fail = (key, message) => {
22368
+ out.ok = false;
22369
+ out.problems.push(problem4("metric_create", key, message));
22370
+ return out;
22371
+ };
22372
+ const waitFor = [];
22373
+ for (const key of dependencyOrder(decls)) {
22374
+ const entry = byKey2.get(key);
22375
+ const decl = decls[key];
22376
+ if (entry === void 0 || decl === void 0) continue;
22377
+ if (entry.action === "reuse" || entry.action === "bind") continue;
22378
+ if (entry.action !== "create" && entry.action !== "edit") continue;
22379
+ let wire = null;
22380
+ if (entry.kind === "calc" && typeof decl.formula === "string") {
22381
+ const names = {};
22382
+ const ids = /* @__PURE__ */ new Map();
22383
+ for (const operand of operandsOf(decls, key)) {
22384
+ const dep = byKey2.get(operand);
22385
+ names[operand] = dep?.metric_name ?? dep?.name ?? operand;
22386
+ ids.set(names[operand], dep?.metric_id ?? null);
22387
+ }
22388
+ wire = wireFormula(decl.formula, names);
22389
+ entry.definition = `CALC ${wire}`;
22390
+ if (entry.action === "edit") entry.new_definition = entry.definition;
22391
+ entry.reuse_key = calcReuseKey(wire, (name) => ids.get(name) ?? null);
22392
+ if (entry.preview !== "ok") {
22393
+ const preview = await previewFormula(t, wire);
22394
+ if (preview !== null) return fail(key, `metric "${key}": Datarails could not evaluate ${wire}: ${preview}`);
22395
+ entry.preview = "ok";
22396
+ }
22397
+ }
22398
+ if (entry.action === "edit") {
22399
+ const body2 = entry.kind === "calc" ? { configuration: { presented_formula: wire, ...formatOf(decl, plan, entry) } } : baseBody(plan.internal.base[key]);
22400
+ const answer2 = await t.call("PATCH", `${t.host}/api/insights/metrics/${String(entry.metric_id)}`, body2).catch((err) => ({ status: 0, body: firstLine(err) }));
22401
+ if (answer2.status < 200 || answer2.status >= 300) {
22402
+ return fail(key, `metric "${key}": Datarails refused to change "${entry.metric_name ?? entry.name}" (id ${String(entry.metric_id)}) in place: ${refusalSentence(answer2, entry, plan)}`);
22403
+ }
22404
+ out.created.push({ key, id: entry.metric_id, name: entry.metric_name ?? entry.name, action: "edit" });
22405
+ if (entry.kind === "base") waitFor.push(entry);
22406
+ continue;
22407
+ }
22408
+ if (category === void 0) {
22409
+ category = await appSpaceCategory(t, out.notes);
22410
+ }
22411
+ const description = [decl.description ?? "", createdFooter(plan.internal.appName)].filter((one) => one !== "").join("\n\n");
22412
+ const body = entry.kind === "calc" ? { name: decl.name, kind: "CALC", configuration: { presented_formula: wire, ...decl.format === void 0 ? {} : { format: decl.format } } } : { name: decl.name, kind: "BASE", ...baseBody(plan.internal.base[key]), ...decl.format === void 0 ? {} : { configuration: { presented_formula: "", format: decl.format } } };
22413
+ body["description"] = description;
22414
+ body["is_approved_by_user"] = true;
22415
+ if (category !== null) body["category"] = category;
22416
+ const answer = await t.call("POST", `${t.host}/api/insights/metrics`, body).catch((err) => ({ status: 0, body: firstLine(err) }));
22417
+ if (answer.status === 422) {
22418
+ let again;
22419
+ try {
22420
+ again = await readCatalog(t);
22421
+ } catch (err) {
22422
+ return fail(key, `metric "${key}": Datarails says a metric named "${decl.name}" exists, and the catalog could not be read again to tell whose (${firstLine(err)})`);
22423
+ }
22424
+ const byName = new Map(again.map((row) => [row.name, row]));
22425
+ const holder = byName.get(decl.name);
22426
+ if (holder === void 0) {
22427
+ return fail(key, `metric "${key}": a metric named "${decl.name}" exists in this company, but its source tables are not shared with you`);
22428
+ }
22429
+ if (entry.reuse_key !== null && rowReuseKey(holder, byName) === entry.reuse_key) {
22430
+ entry.action = "reuse";
22431
+ entry.origin = "reused";
22432
+ entry.metric_id = holder.id;
22433
+ entry.metric_name = holder.name;
22434
+ entry.note = "the same definition (another ship created it a moment ago)";
22435
+ continue;
22436
+ }
22437
+ return fail(
22438
+ key,
22439
+ `metric "${key}": this company now has a metric named "${decl.name}" (id ${String(holder.id)}) with a different definition: ${describeRow(holder, plan.internal.tables)} against the contract's ${entry.definition}. Either use theirs ("reuse": "name"), or give the contract's metric another name`
22440
+ );
22441
+ }
22442
+ if (answer.status < 200 || answer.status >= 300) {
22443
+ return fail(key, `metric "${key}": Datarails refused to create "${decl.name}": ${refusalSentence(answer, entry, plan)}`);
22444
+ }
22445
+ const created = obj2(decodeEntities2(answer.body));
22446
+ const id = idIn(created["id"]);
22447
+ if (id === null || id === 0) return fail(key, `metric "${key}": Datarails created "${decl.name}" and answered no id for it`);
22448
+ entry.metric_id = id;
22449
+ entry.metric_name = typeof created["name"] === "string" && created["name"] !== "" ? created["name"] : decl.name;
22450
+ entry.origin = "created";
22451
+ out.created.push({ key, id, name: entry.metric_name, action: "create" });
22452
+ if (entry.kind === "base") waitFor.push(entry);
22453
+ }
22454
+ if (waitFor.length > 0) await waitForExtraction(t, waitFor, opts, out.notes);
22455
+ const written = plan.entries.filter((entry) => out.created.some((one) => one.key === entry.key));
22456
+ if (written.length > 0) {
22457
+ const drifted = await readBack(t, plan, written);
22458
+ if (drifted.length > 0) {
22459
+ out.ok = false;
22460
+ out.problems.push(...drifted);
22461
+ return out;
22462
+ }
22463
+ }
22464
+ for (const entry of plan.entries) {
22465
+ if (entry.metric_id === null || entry.origin === null) continue;
22466
+ out.bindings[entry.key] = {
22467
+ metric_id: entry.metric_id,
22468
+ origin: entry.origin,
22469
+ ...entry.reuse_key === null ? {} : { reuse_key: entry.reuse_key },
22470
+ ...entry.action === "edit" && typeof entry.previous_reuse_key === "string" ? { previous_reuse_key: entry.previous_reuse_key } : {}
22471
+ };
22472
+ }
22473
+ return out;
22474
+ }
22475
+ async function readBack(t, plan, written) {
22476
+ let catalog;
22477
+ try {
22478
+ catalog = await readCatalog(t);
22479
+ } catch (err) {
22480
+ return [
22481
+ problem4(
22482
+ "metric_changed",
22483
+ null,
22484
+ `${firstLine(err)} after this ship wrote ${written.map((entry) => `"${entry.metric_name ?? entry.name}"`).join(", ")}, so what Datarails holds could not be checked against the contract; nothing was pushed`
22485
+ )
22486
+ ];
22487
+ }
22488
+ const byName = new Map(catalog.map((row) => [row.name, row]));
22489
+ const problems = [];
22490
+ for (const entry of written) {
22491
+ const label = `"${entry.metric_name ?? entry.name}" (id ${String(entry.metric_id)})`;
22492
+ const row = catalog.find((one) => one.id === entry.metric_id);
22493
+ if (row === void 0) {
22494
+ problems.push(problem4("metric_changed", entry.key, `metric "${entry.key}": ${label} is not among this company's metrics you can read, just after this ship wrote it; nothing was pushed`));
22495
+ continue;
22496
+ }
22497
+ const held = rowReuseKey(row, byName);
22498
+ if (held === null || entry.reuse_key === null) {
22499
+ problems.push(
22500
+ problem4(
22501
+ "metric_changed",
22502
+ entry.key,
22503
+ `metric "${entry.key}": ${label} could not be read back in full after this ship wrote it (Datarails holds ${describeRow(row, plan.internal.tables)}), so it could not be compared with the contract's ${entry.definition}; nothing was pushed`
22504
+ )
22505
+ );
22506
+ continue;
22507
+ }
22508
+ if (held !== entry.reuse_key) {
22509
+ problems.push(
22510
+ problem4(
22511
+ "metric_changed",
22512
+ entry.key,
22513
+ `metric "${entry.key}": Datarails now holds a different definition for ${label} than this ship wrote: ${describeRow(row, plan.internal.tables)} against the contract's ${entry.definition}. Another ship or person changed it after this ship wrote it, or Datarails did not take the whole change; nothing was pushed`
22514
+ )
22515
+ );
22516
+ continue;
22517
+ }
22518
+ entry.reuse_key = held;
22519
+ entry.metric_name = row.name;
22520
+ }
22521
+ return problems;
22522
+ }
22523
+ function formatOf(decl, plan, entry) {
22524
+ if (decl.format !== void 0) return { format: decl.format };
22525
+ const row = plan.internal.catalog.find((one) => one.id === entry.metric_id);
22526
+ return row?.format === null || row?.format === void 0 ? {} : { format: row.format };
22527
+ }
22528
+ function baseBody(def) {
22529
+ return {
22530
+ template: def.template,
22531
+ value_field: def.value_field,
22532
+ date_field: def.date_field,
22533
+ aggregator: def.aggregator,
22534
+ is_cumulative: def.is_cumulative,
22535
+ time_aggregation: rollupOf(def),
22536
+ filters: def.filters.map((one) => ({
22537
+ field: one.field,
22538
+ values: one.values,
22539
+ is_excluded: one.is_excluded,
22540
+ allow_nulls: one.allow_nulls,
22541
+ override_global_filter: false
22542
+ }))
22543
+ };
22544
+ }
22545
+ async function appSpaceCategory(t, notes) {
22546
+ try {
22547
+ const listed = await t.call("GET", `${t.host}/api/insights/metric_categories`);
22548
+ if (listed.status >= 200 && listed.status < 300) {
22549
+ const found = listOf(decodeEntities2(listed.body)).find((one) => one["name"] === METRIC_CREATED_CATEGORY);
22550
+ const id = found === void 0 ? null : idIn(found["id"]);
22551
+ if (id !== null) return id;
22552
+ const made = await t.call("POST", `${t.host}/api/insights/metric_categories`, { name: METRIC_CREATED_CATEGORY });
22553
+ const madeId = made.status >= 200 && made.status < 300 ? idIn(obj2(made.body)["id"]) : null;
22554
+ if (madeId !== null) return madeId;
22555
+ notes.push(`the metrics are not filed under the "${METRIC_CREATED_CATEGORY}" category (creating it answered HTTP ${String(made.status)}); their description still says App Space made them`);
22556
+ return null;
22557
+ }
22558
+ notes.push(`the metrics are not filed under the "${METRIC_CREATED_CATEGORY}" category (the categories answered HTTP ${String(listed.status)}); their description still says App Space made them`);
22559
+ } catch (err) {
22560
+ notes.push(`the metrics are not filed under the "${METRIC_CREATED_CATEGORY}" category (${firstLine(err)}); their description still says App Space made them`);
22561
+ }
22562
+ return null;
22563
+ }
22564
+ function refusalSentence(answer, entry, plan) {
22565
+ const said2 = sentenceOf(decodeEntities2(answer.body));
22566
+ const table = plan.internal.tableOf[entry.key];
22567
+ if (answer.status === 400 && /invalid pk/i.test(JSON.stringify(answer.body ?? "")) && /template/.test(JSON.stringify(answer.body ?? ""))) {
22568
+ return `you can't build metrics on the table "${table?.name ?? "?"}"; ask an admin for access`;
22569
+ }
22570
+ if (answer.status === 403 && /do not have access to this metric/i.test(said2)) {
22571
+ return entry.kind === "base" ? `the table "${table?.name ?? "?"}" has a permission filter on you (authoring blocked); ask an admin for access` : "one of its operands reads data you cannot see; ask an admin to share its source tables";
22572
+ }
22573
+ if (answer.status === 403) {
22574
+ return `this credential was refused (403${said2 === "" ? "" : `: ${said2}`}); it may lack the Datarails scope metrics:write \u2014 ask the agent's owner to add it to its CLI scope ceiling, or create the metric in Datarails and declare it with "reuse": "name"`;
22575
+ }
22576
+ if (answer.status === 404) return "it no longer exists in this company";
22577
+ if (answer.status === 0) return `the request did not complete (${String(answer.body)})`;
22578
+ return said2 === "" ? `HTTP ${String(answer.status)}` : `${said2} (HTTP ${String(answer.status)})`;
22579
+ }
22580
+ async function waitForExtraction(t, entries, opts, notes) {
22581
+ const sleep3 = opts.sleep ?? ((ms) => new Promise((done) => setTimeout(done, ms)));
22582
+ const now = opts.now ?? (() => Date.now());
22583
+ const deadline = now() + Math.max(0, Math.min(opts.waitSeconds, METRICS_WAIT_CAP)) * 1e3;
22584
+ let waiting = entries;
22585
+ for (; ; ) {
22586
+ const still = [];
22587
+ for (const entry of waiting) {
22588
+ const answer = await t.call("GET", `${t.host}/api/insights/metrics/${String(entry.metric_id)}`).catch(() => null);
22589
+ const row = answer !== null && answer.status === 200 && isObj3(answer.body) ? catalogRow(answer.body) : null;
22590
+ entry.status = row?.status ?? entry.status ?? null;
22591
+ if (entry.status === null || entry.status === "PROCESSING") still.push(entry);
22592
+ }
22593
+ waiting = still;
22594
+ if (waiting.length === 0 || now() + POLL_MS > deadline) break;
22595
+ await sleep3(POLL_MS);
22596
+ }
22597
+ for (const entry of waiting) {
22598
+ entry.status = entry.status ?? "PROCESSING";
22599
+ notes.push(`"${entry.metric_name ?? entry.name}" is still being prepared in Datarails; its cards say so until it is ready`);
22600
+ }
22601
+ }
22602
+ function bindingWarnings(answer) {
22603
+ const block = obj2(answer);
22604
+ const rows2 = (value) => (Array.isArray(value) ? value : []).map(
22605
+ (one) => isObj3(one) ? {
22606
+ key: String(one["key"] ?? "?"),
22607
+ field: typeof one["field"] === "string" ? one["field"] : null,
22608
+ reason: typeof one["reason"] === "string" && one["reason"] !== "" ? one["reason"] : null
22609
+ } : { key: String(one), field: null, reason: null }
22610
+ );
22611
+ const lines = [];
22612
+ for (const one of rows2(block["downgraded"])) {
22613
+ lines.push(
22614
+ `metric "${one.key}" was sent as created by App Space and kept as reused${one.reason === null ? "" : ` (${one.reason})`}: App Space will not change it in place later`
22615
+ );
22616
+ }
22617
+ for (const one of rows2(block["ignored"])) {
22618
+ if (one.field === null || one.field === "previous_reuse_key") {
22619
+ lines.push(
22620
+ `metric "${one.key}" was changed in place in Datarails, and the platform did NOT record the change${one.reason === null ? "" : ` (${one.reason})`}: the metric holds the new definition; check it in Datarails`
22621
+ );
22622
+ } else {
22623
+ lines.push(`metric "${one.key}": the platform did not record ${one.field}${one.reason === null ? "" : ` (${one.reason})`}`);
22624
+ }
22625
+ }
22626
+ return lines;
22627
+ }
22628
+ function renderPlan(plan) {
22629
+ const head = `Metrics on ${plan.server}${plan.org === null ? "" : ` (org ${String(plan.org)})`}${plan.user === null ? "" : `, as ${plan.user}`}:
22630
+ `;
22631
+ const label = (entry) => `${entry.metric_name ?? entry.name}${entry.metric_id === null ? "" : ` (id ${String(entry.metric_id)})`}`;
22632
+ const keyWidth = Math.max(...plan.entries.map((entry) => entry.key.length), 3) + 2;
22633
+ const nameWidth = Math.max(...plan.entries.map((entry) => label(entry).length), 4) + 2;
22634
+ let out = head;
22635
+ for (const entry of plan.entries) {
22636
+ const action = entry.action === "edit" ? "change" : entry.action;
22637
+ out += ` ${entry.key.padEnd(keyWidth)}${label(entry).padEnd(nameWidth)}${action.padEnd(9)}${entry.note}
22638
+ `;
22639
+ if (entry.action === "edit") {
22640
+ out += ` ${" ".repeat(keyWidth + nameWidth + 9)}was: ${entry.old_definition ?? "?"}
22641
+ `;
22642
+ out += ` ${" ".repeat(keyWidth + nameWidth + 9)}now: ${entry.new_definition ?? "?"}
22643
+ `;
22644
+ }
22645
+ }
22646
+ out += `Credential: ${plan.credential.line}
22647
+ `;
22648
+ for (const note of plan.notes) out += `note: ${note}
22649
+ `;
22650
+ for (const one of plan.not_checked) out += `Not checked: ${one}
22651
+ `;
22652
+ return out;
22653
+ }
22654
+ function renderApplied(plan, applied) {
22655
+ let out = "";
22656
+ for (const one of applied.created) {
22657
+ const entry = plan.entries.find((candidate) => candidate.key === one.key);
22658
+ if (one.action === "edit") {
22659
+ out += `metric "${one.key}": changed "${one.name}" (id ${String(one.id)}) in place
22660
+ was: ${entry?.old_definition ?? "?"}
22661
+ now: ${entry?.new_definition ?? "?"}
22662
+ `;
22663
+ } else {
22664
+ out += `metric "${one.key}": created "${one.name}" (id ${String(one.id)})${entry?.status === void 0 || entry.status === null ? "" : `, ${entry.status.toLowerCase()}`}
22665
+ `;
22666
+ }
22667
+ }
22668
+ for (const note of applied.notes) out += `note: ${note}
22669
+ `;
22670
+ return out;
22671
+ }
22672
+ function planReport(plan, applied) {
22673
+ return {
22674
+ server: plan.server,
22675
+ host: plan.host,
22676
+ org: plan.org,
22677
+ user: plan.user,
22678
+ credential: plan.credential,
22679
+ entries: plan.entries.map((entry) => ({
22680
+ key: entry.key,
22681
+ name: entry.name,
22682
+ kind: entry.kind,
22683
+ action: entry.action,
22684
+ metric_id: entry.metric_id,
22685
+ metric_name: entry.metric_name,
22686
+ origin: entry.origin,
22687
+ definition: entry.definition,
22688
+ note: entry.note,
22689
+ ...entry.preview === void 0 ? {} : { preview: entry.preview },
22690
+ ...entry.after === void 0 ? {} : { after: entry.after },
22691
+ ...entry.code === void 0 ? {} : { code: entry.code, message: entry.message },
22692
+ ...entry.action === "edit" ? { old_definition: entry.old_definition, new_definition: entry.new_definition } : {},
22693
+ ...entry.status === void 0 ? {} : { status: entry.status }
22694
+ })),
22695
+ not_checked: plan.not_checked,
22696
+ notes: [...plan.notes, ...applied?.notes ?? []],
22697
+ ...applied === void 0 ? {} : { created: applied.created, bindings: applied.bindings, ok: applied.ok }
22698
+ };
22699
+ }
22700
+ async function checkMetricSources(t, appId, contract) {
22701
+ const out = [];
22702
+ for (const [source, key] of Object.entries(metricSources(contract))) {
22703
+ let answer;
22704
+ try {
22705
+ answer = await t.call("POST", `${t.appSpace}/preview/apps/${encodeURIComponent(appId)}/${encodeURIComponent(source)}`, { op: "load" });
22706
+ } catch (err) {
22707
+ out.push({ source, key, state: "unchecked", message: `could not be run (${firstLine(err)})` });
22708
+ continue;
22709
+ }
22710
+ if (answer.status < 200 || answer.status >= 300) {
22711
+ out.push({ source, key, ...previewRefusal(answer) });
22712
+ continue;
22713
+ }
22714
+ if (!isObj3(answer.body)) {
22715
+ out.push({ source, key, state: "unchecked", message: `could not be read (the preview answered HTTP ${String(answer.status)} with no source answer)` });
22716
+ continue;
22717
+ }
22718
+ const body = answer.body;
22719
+ const held = isObj3(body["result"]) && !("meta" in body) ? obj2(body["result"]) : body;
22720
+ if (!("error" in held) && !("meta" in held) && !("rows" in held)) {
22721
+ out.push({ source, key, state: "unchecked", message: `could not be read (the preview answered HTTP ${String(answer.status)} with no source answer)` });
22722
+ continue;
22723
+ }
22724
+ const status = obj2(obj2(held["meta"])["source"])["status"];
22725
+ const error = typeof held["error"] === "string" ? held["error"] : "";
22726
+ if (status === "processing") out.push({ source, key, state: "preparing", message: error || "Datarails is preparing the metric" });
22727
+ else if (error !== "" || status === "unbound" || status === "gone" || status === "error") {
22728
+ out.push({ source, key, state: "failed", message: error || `the metric is ${String(status)}` });
22729
+ } else out.push({ source, key, state: "figure", message: "read" });
22730
+ }
22731
+ return out;
22732
+ }
22733
+ var PREVIEW_UNAVAILABLE = /* @__PURE__ */ new Set([401, 403, 407, 408, 429, 502, 503, 504]);
22734
+ function previewRefusal(answer) {
22735
+ const said2 = sentenceOf(answer.body);
22736
+ const http = `HTTP ${String(answer.status)}${said2 === "" ? "" : `: ${said2}`}`;
22737
+ if (PREVIEW_UNAVAILABLE.has(answer.status) || !isObj3(answer.body)) {
22738
+ return { state: "unchecked", message: `could not be run (${http})` };
22739
+ }
22740
+ if (answer.status >= 500) {
22741
+ return { state: "failed", message: `its handler failed once pushed (${http}); \`app-space logs <app-id>\` says why` };
22742
+ }
22743
+ return { state: "failed", message: `the preview refused it once pushed (${http})` };
22744
+ }
22745
+ function sentenceOf(body) {
22746
+ if (typeof body === "string") return /^<(!doctype html|html)/i.test(body.trimStart()) ? "" : body.trim().slice(0, 300);
22747
+ if (!isObj3(body)) return "";
22748
+ const error = body["error"];
22749
+ if (typeof error === "string") return error;
22750
+ if (isObj3(error) && typeof error["message"] === "string") return error["message"];
22751
+ if (typeof body["detail"] === "string") return body["detail"];
22752
+ const parts = [];
22753
+ for (const [field, value] of Object.entries(body)) {
22754
+ if (field === "success") continue;
22755
+ const said2 = Array.isArray(value) ? value.map((one) => typeof one === "string" ? one : JSON.stringify(one)).join(" ") : typeof value === "string" ? value : "";
22756
+ if (said2 !== "") parts.push(`${field}: ${said2}`);
22757
+ }
22758
+ return parts.join("; ").slice(0, 300);
22759
+ }
22760
+ function firstLine(err) {
22761
+ return (err instanceof Error ? err.message : String(err)).split("\n")[0] ?? "";
22762
+ }
22763
+ function isObj3(value) {
22764
+ return typeof value === "object" && value !== null && !Array.isArray(value);
22765
+ }
22766
+ function obj2(value) {
22767
+ return isObj3(value) ? value : {};
22768
+ }
22769
+
22770
+ // src/commands/appspace/push.ts
21610
22771
  var KIT_ENV = "DR_APPSPACE_KIT";
21611
22772
  function kitForPush(opts, env = process.env) {
21612
22773
  const flag = (opts.kit ?? "").trim();
@@ -21775,12 +22936,39 @@ function describePostCommit(result) {
21775
22936
  }
21776
22937
  return lines.join("");
21777
22938
  }
22939
+ async function bindForPush(client, appId, contract, opts) {
22940
+ const transport = metricsTransport(client);
22941
+ const app = contract?.app;
22942
+ const appName = typeof app?.name === "string" ? app.name : appId;
22943
+ const plan = await planMetrics(contract, transport, { appId, appName });
22944
+ process.stderr.write(renderPlan(plan));
22945
+ if (plan.problems.length > 0) {
22946
+ throw new Error(`the Datarails metrics this app declares cannot be bound here; nothing was created or pushed:
22947
+ ${formatProblems(plan.problems)}`);
22948
+ }
22949
+ const wait = metricsWaitOf(opts.metricsWait);
22950
+ if (wait.note !== null) process.stderr.write(`note: ${wait.note}
22951
+ `);
22952
+ const applied = await applyPlan(plan, transport, { waitSeconds: wait.seconds });
22953
+ process.stderr.write(renderApplied(plan, applied));
22954
+ if (!applied.ok) {
22955
+ const kept = applied.created.map((one) => `"${one.name}" (id ${String(one.id)})`).join(", ");
22956
+ throw new Error(
22957
+ `a Datarails metric could not be created, so nothing was pushed${kept === "" ? "" : `; kept, and reused by the next push: ${kept}`}:
22958
+ ${formatProblems(applied.problems)}`
22959
+ );
22960
+ }
22961
+ return applied.bindings;
22962
+ }
21778
22963
  function registerPushCommand(parent) {
21779
22964
  withJsonOption(
21780
22965
  withServerOptions(
21781
22966
  parent.command("push [dir]").description("Push a folder to App Space as a draft.").option("--publish", "Publish it too (needs an admin role).").option("--seed <file>", "Seed rows, as JSON. Only fills empty tables.").option("--force", "Push even if the app changed since you fetched it.").option("--no-contract", "Do not extract and attach the app contract.").option(
21782
22967
  "--kit <asset>",
21783
22968
  `The builder kit that made this app, e.g. appspace-app-kit@12 (default: $${KIT_ENV}). Recorded as the contract row's kit_version; omitted when neither is set.`
22969
+ ).option(
22970
+ "--metrics-wait <seconds>",
22971
+ "When the contract declares Datarails metrics (data.metrics) and this push creates a BASE one, how long to wait for it to extract before pushing anyway (default 60, at most 120)."
21784
22972
  )
21785
22973
  )
21786
22974
  ).action(async (dirArg, opts) => {
@@ -21822,6 +23010,9 @@ async function pushBody(dir, opts) {
21822
23010
  if (opts.force !== true && typeof state.revision === "number") {
21823
23011
  body["expected_revision"] = state.revision;
21824
23012
  }
23013
+ if (opts.metricBindings !== void 0 && Object.keys(opts.metricBindings).length > 0) {
23014
+ body["metric_bindings"] = opts.metricBindings;
23015
+ }
21825
23016
  return { appId, body, handlers };
21826
23017
  }
21827
23018
  function chartsWithoutEcharts(files) {
@@ -21843,6 +23034,9 @@ async function pushFolder(dir, opts) {
21843
23034
  const { appId, body, handlers } = await pushBody(dir, opts);
21844
23035
  const client = await connect(opts);
21845
23036
  try {
23037
+ if (opts.metricBindings === void 0 && Object.keys(declaredMetrics(body["contract"])).length > 0) {
23038
+ body["metric_bindings"] = await bindForPush(client, appId, body["contract"], opts);
23039
+ }
21846
23040
  let result;
21847
23041
  try {
21848
23042
  result = await client.request(`/apps/${encodeURIComponent(appId)}/package`, {
@@ -21859,7 +23053,8 @@ async function pushFolder(dir, opts) {
21859
23053
  const outcome = {
21860
23054
  revision: result.revision,
21861
23055
  stage: result.stage,
21862
- code_version: typeof result.code_version === "number" ? result.code_version : null
23056
+ code_version: typeof result.code_version === "number" ? result.code_version : null,
23057
+ ...result.metric_bindings === void 0 ? {} : { metric_bindings: result.metric_bindings }
21863
23058
  };
21864
23059
  await recordRevision(dir, result.revision, result.stage, handlers, {
21865
23060
  id: appId,
@@ -21875,6 +23070,8 @@ async function pushFolder(dir, opts) {
21875
23070
  `
21876
23071
  );
21877
23072
  process.stdout.write(describePostCommit(result));
23073
+ for (const line2 of bindingWarnings(result.metric_bindings)) process.stdout.write(`warning: ${line2}
23074
+ `);
21878
23075
  process.stdout.write(describeContract(result.contract));
21879
23076
  process.stdout.write(describeIgnoredName(result.name_ignored));
21880
23077
  process.stdout.write(describeSeeding(result));
@@ -21954,9 +23151,9 @@ async function writeReport(dir, report) {
21954
23151
  }
21955
23152
  async function ownedFiles(dir) {
21956
23153
  try {
21957
- const parsed = JSON.parse(await readFile8(reportPath(dir), "utf8"));
21958
- if (!Array.isArray(parsed.files)) return /* @__PURE__ */ new Set();
21959
- return new Set(parsed.files.map(String));
23154
+ const parsed2 = JSON.parse(await readFile8(reportPath(dir), "utf8"));
23155
+ if (!Array.isArray(parsed2.files)) return /* @__PURE__ */ new Set();
23156
+ return new Set(parsed2.files.map(String));
21960
23157
  } catch {
21961
23158
  return /* @__PURE__ */ new Set();
21962
23159
  }
@@ -21966,8 +23163,8 @@ function digestOf(text3) {
21966
23163
  }
21967
23164
  async function recordedDigests(dir) {
21968
23165
  try {
21969
- const parsed = JSON.parse(await readFile8(reportPath(dir), "utf8"));
21970
- const held = parsed.digests;
23166
+ const parsed2 = JSON.parse(await readFile8(reportPath(dir), "utf8"));
23167
+ const held = parsed2.digests;
21971
23168
  if (typeof held !== "object" || held === null || Array.isArray(held)) return {};
21972
23169
  return Object.fromEntries(Object.entries(held).filter((entry) => typeof entry[1] === "string"));
21973
23170
  } catch {
@@ -21975,21 +23172,21 @@ async function recordedDigests(dir) {
21975
23172
  }
21976
23173
  }
21977
23174
  async function recordAsInput(dir, path3) {
21978
- let parsed;
23175
+ let parsed2;
21979
23176
  try {
21980
- parsed = JSON.parse(await readFile8(reportPath(dir), "utf8"));
23177
+ parsed2 = JSON.parse(await readFile8(reportPath(dir), "utf8"));
21981
23178
  } catch {
21982
23179
  return;
21983
23180
  }
21984
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return;
21985
- const list3 = (key) => Array.isArray(parsed[key]) ? parsed[key].map(String) : [];
21986
- parsed["files"] = list3("files").filter((one) => one !== path3);
21987
- parsed["inputs"] = [.../* @__PURE__ */ new Set([...list3("inputs"), path3])].sort();
21988
- const digests = parsed["digests"];
23181
+ if (typeof parsed2 !== "object" || parsed2 === null || Array.isArray(parsed2)) return;
23182
+ const list3 = (key) => Array.isArray(parsed2[key]) ? parsed2[key].map(String) : [];
23183
+ parsed2["files"] = list3("files").filter((one) => one !== path3);
23184
+ parsed2["inputs"] = [.../* @__PURE__ */ new Set([...list3("inputs"), path3])].sort();
23185
+ const digests = parsed2["digests"];
21989
23186
  if (typeof digests === "object" && digests !== null && !Array.isArray(digests)) {
21990
23187
  delete digests[path3];
21991
23188
  }
21992
- await writeFile4(reportPath(dir), JSON.stringify(parsed, null, 2) + "\n");
23189
+ await writeFile4(reportPath(dir), JSON.stringify(parsed2, null, 2) + "\n");
21993
23190
  }
21994
23191
 
21995
23192
  // src/commands/appspace/get.ts
@@ -22010,8 +23207,8 @@ function requireBundlePath(name) {
22010
23207
  async function unpushedInputs(dir, pkg) {
22011
23208
  let inputs = [];
22012
23209
  try {
22013
- const parsed = JSON.parse(await readFile9(reportPath(dir), "utf8"));
22014
- if (Array.isArray(parsed.inputs)) inputs = parsed.inputs.map(String);
23210
+ const parsed2 = JSON.parse(await readFile9(reportPath(dir), "utf8"));
23211
+ if (Array.isArray(parsed2.inputs)) inputs = parsed2.inputs.map(String);
22015
23212
  } catch {
22016
23213
  return [];
22017
23214
  }
@@ -22276,7 +23473,7 @@ import { cp, mkdir as mkdir5, mkdtemp as mkdtemp2, rm as rm3, writeFile as write
22276
23473
  import { tmpdir as tmpdir3 } from "os";
22277
23474
  import { dirname as dirname4, join as join11, resolve as resolve5, sep as sep2 } from "path";
22278
23475
  var WHAT = "package";
22279
- async function checkPackage(dir, files, contract, opts, online) {
23476
+ async function checkPackage(dir, files, contract, opts, online, pendingMetricKeys = []) {
22280
23477
  if (!online) {
22281
23478
  return skip("no -s server to run the push's own checks against; the push is the first to run them");
22282
23479
  }
@@ -22288,13 +23485,13 @@ async function checkPackage(dir, files, contract, opts, online) {
22288
23485
  try {
22289
23486
  payload = await pushBody(scratch, { ...opts, contract: true });
22290
23487
  } catch (err) {
22291
- return skip(`the push body could not be built (${firstLine(err)})`);
23488
+ return skip(`the push body could not be built (${firstLine2(err)})`);
22292
23489
  }
22293
23490
  let client;
22294
23491
  try {
22295
23492
  client = await connect(opts);
22296
23493
  } catch (err) {
22297
- return skip(`could not reach the server (${firstLine(err)})`);
23494
+ return skip(`could not reach the server (${firstLine2(err)})`);
22298
23495
  }
22299
23496
  try {
22300
23497
  const path3 = `/apps/${encodeURIComponent(payload.appId)}/package/validate`;
@@ -22307,9 +23504,9 @@ async function checkPackage(dir, files, contract, opts, online) {
22307
23504
  body: JSON.stringify(payload.body)
22308
23505
  });
22309
23506
  } catch (err) {
22310
- return skip(`the server did not answer (${firstLine(err)})`);
23507
+ return skip(`the server did not answer (${firstLine2(err)})`);
22311
23508
  }
22312
- return interpret(res.status, parse(res.body), path3, contract);
23509
+ return setAsidePending(interpret(res.status, parse(res.body), path3, contract), pendingMetricKeys);
22313
23510
  } finally {
22314
23511
  client.cleanup();
22315
23512
  }
@@ -22317,11 +23514,28 @@ async function checkPackage(dir, files, contract, opts, online) {
22317
23514
  await rm3(scratch, { recursive: true, force: true });
22318
23515
  }
22319
23516
  }
23517
+ function setAsidePending(check5, pending) {
23518
+ if (pending.length === 0) return check5;
23519
+ const mentions3 = (message) => pending.some((key) => new RegExp(`metric_bindings\\.${key.replace(/[^a-z0-9_]/g, "")}(?![a-z0-9_])`).test(message));
23520
+ const kept = check5.problems.filter((one) => !mentions3(one.message));
23521
+ if (kept.length === check5.problems.length) return check5;
23522
+ const keys2 = pending.map((key) => `"${key}"`).join(", ");
23523
+ return {
23524
+ problems: kept,
23525
+ skipped: [
23526
+ ...check5.skipped,
23527
+ {
23528
+ what: "metric bindings",
23529
+ why: `${keys2} ${pending.length === 1 ? "is" : "are"} created on push and ${pending.length === 1 ? "has" : "have"} no id to bind yet` + (kept.length === 0 ? "; the platform's checks after its metric check (a data-model change, the revision) did not run" : "")
23530
+ }
23531
+ ]
23532
+ };
23533
+ }
22320
23534
  function interpret(status, body, path3, contract) {
22321
23535
  if (typeof body === "string" && /^<(!doctype html|html)/i.test(body.trimStart())) {
22322
23536
  return skip(`the request was intercepted before reaching App Space (HTTP ${String(status)}, an HTML page)`);
22323
23537
  }
22324
- if (status === 200 && isObj3(body)) {
23538
+ if (status === 200 && isObj4(body)) {
22325
23539
  const answer = body;
22326
23540
  if (answer.ok === true) return { problems: [], skipped: [] };
22327
23541
  if (answer.ok === false) {
@@ -22370,7 +23584,7 @@ function fromRefusal(status, detail, contract) {
22370
23584
  };
22371
23585
  });
22372
23586
  }
22373
- if (isObj3(detail) && detail["error"] === "stale_revision") {
23587
+ if (isObj4(detail) && detail["error"] === "stale_revision") {
22374
23588
  return [
22375
23589
  {
22376
23590
  code: "server:stale-revision",
@@ -22380,7 +23594,7 @@ function fromRefusal(status, detail, contract) {
22380
23594
  }
22381
23595
  ];
22382
23596
  }
22383
- if (isObj3(detail) && Array.isArray(detail["seed"])) {
23597
+ if (isObj4(detail) && Array.isArray(detail["seed"])) {
22384
23598
  return detail["seed"].map((line2) => ({
22385
23599
  code: "server:seed",
22386
23600
  pointer: "/",
@@ -22388,7 +23602,7 @@ function fromRefusal(status, detail, contract) {
22388
23602
  fix: "Fix the --seed file: every key a declared table, every row an object."
22389
23603
  }));
22390
23604
  }
22391
- if (isObj3(detail) && Array.isArray(detail["problems"])) {
23605
+ if (isObj4(detail) && Array.isArray(detail["problems"])) {
22392
23606
  const hint = typeof detail["hint"] === "string" ? detail["hint"] : null;
22393
23607
  return detail["problems"].map((line2) => fromLine(String(line2), refused, tables, hint));
22394
23608
  }
@@ -22439,11 +23653,11 @@ async function stage(dir, scratch, files) {
22439
23653
  }
22440
23654
  function tablesOf2(contract) {
22441
23655
  const out = /* @__PURE__ */ new Map();
22442
- const app = isObj3(contract) && isObj3(contract["app"]) ? contract["app"] : {};
22443
- const model = isObj3(app["data_model"]) ? app["data_model"] : {};
22444
- const tables = isObj3(model["tables"]) ? model["tables"] : {};
23656
+ const app = isObj4(contract) && isObj4(contract["app"]) ? contract["app"] : {};
23657
+ const model = isObj4(app["data_model"]) ? app["data_model"] : {};
23658
+ const tables = isObj4(model["tables"]) ? model["tables"] : {};
22445
23659
  for (const [name, table] of Object.entries(tables)) {
22446
- const columns = isObj3(table) && isObj3(table["columns"]) ? table["columns"] : {};
23660
+ const columns = isObj4(table) && isObj4(table["columns"]) ? table["columns"] : {};
22447
23661
  out.set(name, new Set(Object.keys(columns)));
22448
23662
  }
22449
23663
  return out;
@@ -22461,15 +23675,15 @@ function parse(bytes) {
22461
23675
  }
22462
23676
  }
22463
23677
  function detailOf(body) {
22464
- return isObj3(body) ? body["detail"] : void 0;
23678
+ return isObj4(body) ? body["detail"] : void 0;
22465
23679
  }
22466
23680
  function sentence(text3) {
22467
23681
  return text3.endsWith(".") ? text3 : `${text3}.`;
22468
23682
  }
22469
- function firstLine(err) {
23683
+ function firstLine2(err) {
22470
23684
  return (err instanceof Error ? err.message : String(err)).split("\n")[0] ?? "";
22471
23685
  }
22472
- function isObj3(value) {
23686
+ function isObj4(value) {
22473
23687
  return typeof value === "object" && value !== null && !Array.isArray(value);
22474
23688
  }
22475
23689
 
@@ -22511,8 +23725,8 @@ function stripPositional(contract, { keepTree = true, order: order2 } = {}) {
22511
23725
  else delete page.tree;
22512
23726
  }
22513
23727
  if (out.bindings !== void 0) out.bindings = renameDeep(out.bindings, rename2);
22514
- for (const problem5 of Array.isArray(out.problems) ? out.problems : []) {
22515
- if (typeof problem5.where === "string") problem5.where = fileOf(problem5.where);
23728
+ for (const problem6 of Array.isArray(out.problems) ? out.problems : []) {
23729
+ if (typeof problem6.where === "string") problem6.where = fileOf(problem6.where);
22516
23730
  }
22517
23731
  return out;
22518
23732
  }
@@ -22917,7 +24131,7 @@ function specSource(data, card) {
22917
24131
  if (!isPlainObject11(row)) return;
22918
24132
  cite(row.source);
22919
24133
  if (typeof row.data === "string") readsOf(row.data);
22920
- for (const next of [...listOf(row.depends), ...listOf(row.about)]) visit(next);
24134
+ for (const next of [...listOf2(row.depends), ...listOf2(row.about)]) visit(next);
22921
24135
  };
22922
24136
  visit(card);
22923
24137
  return { refs, sentence: parts.join("; ") };
@@ -22926,7 +24140,7 @@ function lookupOf(step) {
22926
24140
  if (!isPlainObject11(step)) return [];
22927
24141
  return Object.values(step).flatMap((value) => isPlainObject11(value) && typeof value.lookup === "string" ? [value.lookup] : []);
22928
24142
  }
22929
- function listOf(value) {
24143
+ function listOf2(value) {
22930
24144
  return Array.isArray(value) ? value : [];
22931
24145
  }
22932
24146
  function named6(value) {
@@ -23229,7 +24443,7 @@ function checkPage2(app, page, kit, runtime = null) {
23229
24443
  const missing = runtime === null ? [] : missingLibraries(page).map((one) => one.module);
23230
24444
  const library = runtime === null ? loadsModule(page.path, page.text, WIDGET_LIBRARY2) : !missing.includes("dr-app-runtime") && !missing.includes("dr-app-widgets");
23231
24445
  violations.push(...checkLibraryDraws(page, library, scripts, runtime === null ? null : missing));
23232
- if (runtime !== null) for (const problem5 of libraryOrderProblems(page)) violations.push({ rule: "R2.6", where: page.path, message: problem5 });
24446
+ if (runtime !== null) for (const problem6 of libraryOrderProblems(page)) violations.push({ rule: "R2.6", where: page.path, message: problem6 });
23233
24447
  if (widgets.length > 0 && !library && !/drInsights\.setData\s*\(/.test(scriptText)) {
23234
24448
  violations.push({
23235
24449
  rule: "R3.4",
@@ -24021,10 +25235,10 @@ var PAGE_PATH4 = /\.html?$/i;
24021
25235
  var AppLoadError2 = class extends Error {
24022
25236
  };
24023
25237
  function loadApp2(dir) {
24024
- const manifest = readJson2(dir, "manifest.json", true);
24025
- const dataModel = existsSync2(join13(dir, DATA_MODEL_PATH2)) ? readJson2(dir, DATA_MODEL_PATH2, false) : null;
25238
+ const manifest = readJson3(dir, "manifest.json", true);
25239
+ const dataModel = existsSync2(join13(dir, DATA_MODEL_PATH2)) ? readJson3(dir, DATA_MODEL_PATH2, false) : null;
24026
25240
  const handlersSource = existsSync2(join13(dir, "handlers.json")) ? "handlers.json" : "manifest.json";
24027
- const handlersIndex = handlersSource === "handlers.json" ? readJson2(dir, "handlers.json", false) : manifest.handlers ?? {};
25241
+ const handlersIndex = handlersSource === "handlers.json" ? readJson3(dir, "handlers.json", false) : manifest.handlers ?? {};
24028
25242
  if (handlersIndex === null || typeof handlersIndex !== "object" || Array.isArray(handlersIndex)) {
24029
25243
  throw new AppLoadError2(`${handlersSource} must be an object mapping handler names to {http, file}`);
24030
25244
  }
@@ -24053,7 +25267,7 @@ function readHandlerSource2(dir, file) {
24053
25267
  if (!existsSync2(full) || !statSync2(full).isFile()) return "";
24054
25268
  return readFileSync4(full, "utf8");
24055
25269
  }
24056
- function readJson2(dir, path3, required) {
25270
+ function readJson3(dir, path3, required) {
24057
25271
  const full = join13(dir, path3);
24058
25272
  if (!existsSync2(full)) {
24059
25273
  if (required) throw new AppLoadError2(`${path3} is missing from the app version`);
@@ -25756,7 +26970,7 @@ function labelFor2(node, text3, tree) {
25756
26970
  }
25757
26971
  function textOf2(text3, node) {
25758
26972
  const next = text3.indexOf("<", node.end);
25759
- return decodeEntities2(text3.slice(node.end, next === -1 ? void 0 : next).replace(/\s+/g, " ").trim());
26973
+ return decodeEntities3(text3.slice(node.end, next === -1 ? void 0 : next).replace(/\s+/g, " ").trim());
25760
26974
  }
25761
26975
  function slotsOf2(node, tree) {
25762
26976
  if (!(WIDGET_ATTR2 in node.attrs)) return {};
@@ -26223,7 +27437,7 @@ function specTransforms2(handlers, spec, problems) {
26223
27437
  out.push({ path: path3, text: text3 });
26224
27438
  }
26225
27439
  }
26226
- const unreadable = problems.some((problem5) => problem5.code === "transform_unreadable");
27440
+ const unreadable = problems.some((problem6) => problem6.code === "transform_unreadable");
26227
27441
  for (const key of Object.keys(spec.table).filter(isWidgetKey2).sort(natural5)) {
26228
27442
  const path3 = isPlainObject12(spec.table[key]) ? spec.table[key].transform : void 0;
26229
27443
  if (typeof path3 !== "string" || unreadable || carried !== null && carried.has(path3)) continue;
@@ -26519,7 +27733,7 @@ function natural5(a, b) {
26519
27733
  }
26520
27734
  var NAMED_ENTITIES2 = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
26521
27735
  var ENTITY2 = /&(#[0-9]{1,7}|#[xX][0-9a-fA-F]{1,6}|[a-zA-Z][a-zA-Z0-9]{1,31});/g;
26522
- function decodeEntities2(value) {
27736
+ function decodeEntities3(value) {
26523
27737
  const text3 = String(value);
26524
27738
  if (!text3.includes("&")) return text3;
26525
27739
  return text3.replace(ENTITY2, (whole, body) => {
@@ -26532,7 +27746,7 @@ function decodeEntities2(value) {
26532
27746
  function decodedTree2(html) {
26533
27747
  const tree = parseTree2(html);
26534
27748
  for (const node of tree.nodes) {
26535
- for (const [name, value] of Object.entries(node.attrs)) node.attrs[name] = decodeEntities2(value);
27749
+ for (const [name, value] of Object.entries(node.attrs)) node.attrs[name] = decodeEntities3(value);
26536
27750
  }
26537
27751
  return tree;
26538
27752
  }
@@ -26569,9 +27783,9 @@ var MAX_HINT3 = 120;
26569
27783
  function validate3(instance, schema, options = {}) {
26570
27784
  const problems = [];
26571
27785
  check3(instance, schema, "", schema, problems, { quote: options.quote === true, inBranch: false });
26572
- return problems.map((problem5) => `${problem5.at}: ${problem5.why}`);
27786
+ return problems.map((problem6) => `${problem6.at}: ${problem6.why}`);
26573
27787
  }
26574
- function problem4(at, why, kind = "other") {
27788
+ function problem5(at, why, kind = "other") {
26575
27789
  return { at: at || "/", why, kind };
26576
27790
  }
26577
27791
  function describe4(node, ctx) {
@@ -26606,7 +27820,7 @@ function check3(instance, node, at, root, problems, ctx) {
26606
27820
  if (typeof node.$ref === "string") {
26607
27821
  const target = resolve10(node.$ref, root);
26608
27822
  if (target === null) {
26609
- problems.push(problem4(at, "the schema names a reference it cannot resolve"));
27823
+ problems.push(problem5(at, "the schema names a reference it cannot resolve"));
26610
27824
  return;
26611
27825
  }
26612
27826
  check3(instance, target, at, root, problems, ctx);
@@ -26614,29 +27828,29 @@ function check3(instance, node, at, root, problems, ctx) {
26614
27828
  if ("type" in node) {
26615
27829
  const names = Array.isArray(node.type) ? node.type : [node.type];
26616
27830
  if (!names.some((name) => isType3(instance, name))) {
26617
- problems.push(problem4(at, `not of type ${names.join(" or ")}${hint}`, "type"));
27831
+ problems.push(problem5(at, `not of type ${names.join(" or ")}${hint}`, "type"));
26618
27832
  return;
26619
27833
  }
26620
27834
  }
26621
- if ("const" in node && !same3(instance, node.const)) problems.push(problem4(at, `not the constant this contract fixes${hint}`, "value"));
27835
+ if ("const" in node && !same3(instance, node.const)) problems.push(problem5(at, `not the constant this contract fixes${hint}`, "value"));
26622
27836
  if (Array.isArray(node.enum) && !node.enum.some((one) => same3(instance, one))) {
26623
- problems.push(problem4(at, `not one of the values this contract allows${hint}`, "value"));
27837
+ problems.push(problem5(at, `not one of the values this contract allows${hint}`, "value"));
26624
27838
  }
26625
27839
  if (typeof node.minimum === "number" && isType3(instance, "number") && instance < node.minimum) {
26626
- problems.push(problem4(at, `below the minimum this contract sets${hint}`));
27840
+ problems.push(problem5(at, `below the minimum this contract sets${hint}`));
26627
27841
  }
26628
27842
  if (typeof node.pattern === "string" && typeof instance === "string" && !new RegExp(node.pattern, "u").test(instance)) {
26629
27843
  if (ctx.inBranch) {
26630
- problems.push(problem4(at, `${ctx.quote ? `${JSON.stringify(instance)} ` : ""}does not match ${node.pattern}${hint}`));
27844
+ problems.push(problem5(at, `${ctx.quote ? `${JSON.stringify(instance)} ` : ""}does not match ${node.pattern}${hint}`));
26631
27845
  } else {
26632
- problems.push(problem4(at, "not in the form this contract requires"));
27846
+ problems.push(problem5(at, "not in the form this contract requires"));
26633
27847
  }
26634
27848
  }
26635
27849
  if (typeof node.minLength === "number" && typeof instance === "string" && [...instance].length < node.minLength) {
26636
- problems.push(problem4(at, `shorter than this contract allows${hint}`));
27850
+ problems.push(problem5(at, `shorter than this contract allows${hint}`));
26637
27851
  }
26638
27852
  if (typeof node.maxLength === "number" && typeof instance === "string" && [...instance].length > node.maxLength) {
26639
- problems.push(problem4(at, `longer than this contract allows${hint}`));
27853
+ problems.push(problem5(at, `longer than this contract allows${hint}`));
26640
27854
  }
26641
27855
  if (isObject5(instance)) checkObject3(instance, node, at, root, problems, ctx);
26642
27856
  if (Array.isArray(instance)) checkArray3(instance, node, at, root, problems, ctx, hint);
@@ -26672,16 +27886,16 @@ function checkBranches3(instance, branches, at, root, problems, ctx, wanted, sta
26672
27886
  if (kept.length > MAX_PER_PLACE3) {
26673
27887
  const more = kept.length - MAX_PER_PLACE3;
26674
27888
  kept = kept.slice(0, MAX_PER_PLACE3);
26675
- kept.push(problem4(at, `and ${more} more problems here`, "more"));
27889
+ kept.push(problem5(at, `and ${more} more problems here`, "more"));
26676
27890
  }
26677
27891
  for (const one of kept) problems.push(one);
26678
27892
  } else {
26679
- problems.push(problem4(at, `matches none of the shapes this contract has (one of: ${shapes3(branches, root)})`));
27893
+ problems.push(problem5(at, `matches none of the shapes this contract has (one of: ${shapes3(branches, root)})`));
26680
27894
  }
26681
27895
  } else if (matched === 0) {
26682
- problems.push(problem4(at, "matches none of the shapes this contract has"));
27896
+ problems.push(problem5(at, "matches none of the shapes this contract has"));
26683
27897
  } else if (wanted !== null && matched > wanted) {
26684
- problems.push(problem4(at, "matches more than one shape this contract has"));
27898
+ problems.push(problem5(at, "matches more than one shape this contract has"));
26685
27899
  }
26686
27900
  }
26687
27901
  function shapeMisses3(found, at) {
@@ -26723,7 +27937,7 @@ function checkObject3(instance, node, at, root, problems, ctx) {
26723
27937
  for (const name of Array.isArray(node.required) ? node.required : []) {
26724
27938
  if (Object.prototype.hasOwnProperty.call(instance, name)) continue;
26725
27939
  const wanted = Object.prototype.hasOwnProperty.call(properties, name) ? properties[name] : null;
26726
- problems.push(problem4(`${at}/${escape3(name)}`, `required and absent${describe4(wanted, ctx)}`, "required"));
27940
+ problems.push(problem5(`${at}/${escape3(name)}`, `required and absent${describe4(wanted, ctx)}`, "required"));
26727
27941
  }
26728
27942
  for (const [name, value] of Object.entries(instance)) {
26729
27943
  const where2 = `${at}/${escape3(name)}`;
@@ -26732,12 +27946,12 @@ function checkObject3(instance, node, at, root, problems, ctx) {
26732
27946
  continue;
26733
27947
  }
26734
27948
  const extra = node.additionalProperties ?? true;
26735
- if (extra === false) problems.push(problem4(where2, "not a key this contract carries"));
27949
+ if (extra === false) problems.push(problem5(where2, "not a key this contract carries"));
26736
27950
  else if (isObject5(extra)) check3(value, extra, node["x-tenant-keys"] === true && !ctx.quote ? `${at}/*` : where2, root, problems, ctx);
26737
27951
  }
26738
27952
  }
26739
27953
  function checkArray3(instance, node, at, root, problems, ctx, hint) {
26740
- if (typeof node.minItems === "number" && instance.length < node.minItems) problems.push(problem4(at, `shorter than this contract allows${hint}`));
27954
+ if (typeof node.minItems === "number" && instance.length < node.minItems) problems.push(problem5(at, `shorter than this contract allows${hint}`));
26741
27955
  if (isObject5(node.items)) instance.forEach((value, index) => check3(value, node.items, `${at}/${index}`, root, problems, ctx));
26742
27956
  }
26743
27957
  function isType3(instance, name) {
@@ -27224,8 +28438,8 @@ function unproducedPayloadKeys2(contract, { handlerSource = () => void 0, module
27224
28438
  if (produced.includes(key)) continue;
27225
28439
  const there = produced.length === 0 ? "the controls there produce no payload keys" : `the controls there produce ${produced.join(", ")}`;
27226
28440
  const fix = reads.generated ? `Add a control that sets it, or take "${key}" out of the handler's required in contract.json.` : `Add a control that sets it, or read it with payload.get("${key}", <default>).`;
27227
- const problem5 = `handler "${name}" reads payload "${key}", and nothing on ${either4(pages)} that calls it produces that key \u2014 ${there}.`;
27228
- out.push({ handler: name, key, pages, produced, generated: reads.generated, problem: problem5, fix, message: `${problem5} ${fix}` });
28441
+ const problem6 = `handler "${name}" reads payload "${key}", and nothing on ${either4(pages)} that calls it produces that key \u2014 ${there}.`;
28442
+ out.push({ handler: name, key, pages, produced, generated: reads.generated, problem: problem6, fix, message: `${problem6} ${fix}` });
27229
28443
  }
27230
28444
  }
27231
28445
  return out;
@@ -27259,9 +28473,9 @@ function undeclaredCustomCalls2(contract, { moduleSource = () => void 0 } = {})
27259
28473
  }
27260
28474
  function undeclaredCallMessage2(id, module, handler, calls, how) {
27261
28475
  const listed = calls.length === 0 ? "lists none" : `lists ${calls.join(", ")}`;
27262
- const problem5 = `custom "${id}" calls handler "${handler}" (ui.call ${how}, ui/${module}), and its declares.calls ${listed}.`;
28476
+ const problem6 = `custom "${id}" calls handler "${handler}" (ui.call ${how}, ui/${module}), and its declares.calls ${listed}.`;
27263
28477
  const fix = `Add "${handler}" to declares.calls \u2014 and what it sends to declares.produces \u2014 or stop calling it.`;
27264
- return { problem: problem5, fix, message: `${problem5} ${fix}` };
28478
+ return { problem: problem6, fix, message: `${problem6} ${fix}` };
27265
28479
  }
27266
28480
  function unreadUploadKeys2(contract, { handlerSource = () => void 0 } = {}) {
27267
28481
  const elements = isPlainObject13(contract?.elements) ? contract.elements : {};
@@ -27304,9 +28518,9 @@ function unreadUploadKeys2(contract, { handlerSource = () => void 0 } = {}) {
27304
28518
  }
27305
28519
  if (read) continue;
27306
28520
  const id = idOf3(element, uid);
27307
- const problem5 = `file_upload "${id}" writes the stored file under "${field}" to handler "${handler}" (${via}), and "${handler}" never reads payload["${field}"] \u2014 the file is stored and nothing uses it.`;
28521
+ const problem6 = `file_upload "${id}" writes the stored file under "${field}" to handler "${handler}" (${via}), and "${handler}" never reads payload["${field}"] \u2014 the file is stored and nothing uses it.`;
27308
28522
  const fix = role === "custom" ? `Read payload["${field}"]["upload"] in "${handler}" and hand it to the parser (or write payload["${field}"] into a file column), or drop the upload.` : `Add "${field}" to "${handler}"'s fields (a file column), or drop the upload.`;
27309
- out.push({ uid, id, page: element.page ?? null, field, handler, via, problem: problem5, fix, message: `${problem5} ${fix}` });
28523
+ out.push({ uid, id, page: element.page ?? null, field, handler, via, problem: problem6, fix, message: `${problem6} ${fix}` });
27310
28524
  }
27311
28525
  }
27312
28526
  return out;
@@ -27322,9 +28536,9 @@ function stepsWithoutAction2(contract) {
27322
28536
  const id = idOf3(element, uid);
27323
28537
  const name = named8(step.id) ? step.id : String(index);
27324
28538
  const next = named8(steps[index + 1]?.id) ? steps[index + 1].id : "the next step";
27325
- const problem5 = `stepper "${id}" step "${name}" names no action, so no handler moves the flow on from it \u2014 a flow that reaches it stays there.`;
28539
+ const problem6 = `stepper "${id}" step "${name}" names no action, so no handler moves the flow on from it \u2014 a flow that reaches it stays there.`;
27326
28540
  const fix = `Give it the handler that moves it to "${next}" (action, action_label), or make it the last step.`;
27327
- out.push({ uid, id, page: element.page ?? null, step: name, index, problem: problem5, fix, message: `${problem5} ${fix}` });
28541
+ out.push({ uid, id, page: element.page ?? null, step: name, index, problem: problem6, fix, message: `${problem6} ${fix}` });
27328
28542
  });
27329
28543
  }
27330
28544
  return out;
@@ -27825,9 +29039,9 @@ function unboundControls(contract, files = {}) {
27825
29039
  const param = page.params.find((one) => one?.name === name) ?? {};
27826
29040
  const kind = named9(param.kind) ? param.kind : null;
27827
29041
  const opens3 = param.default === void 0 ? "no value at all" : `its default (${JSON.stringify(param.default)})`;
27828
- const problem5 = `${page.path} declares param "${name}"${kind === null ? "" : ` (${kind})`}, and no control on the page writes it \u2014 no part in param mode, no custom that calls ui.setParam("${name}", \u2026) or declares it in declares.produces \u2014 so the page only ever shows ${opens3}.`;
29042
+ const problem6 = `${page.path} declares param "${name}"${kind === null ? "" : ` (${kind})`}, and no control on the page writes it \u2014 no part in param mode, no custom that calls ui.setParam("${name}", \u2026) or declares it in declares.produces \u2014 so the page only ever shows ${opens3}.`;
27829
29043
  const fix = `Give it a control \u2014 a part in param mode (props.props.param "${name}": a slider, a number_input, a select, a date_picker or a toggle), or a custom whose module calls ui.setParam("${name}", value) \u2014 or drop the param.`;
27830
- out.push({ page: page.path, param: name, kind, problem: problem5, fix, message: `${problem5} ${fix}` });
29044
+ out.push({ page: page.path, param: name, kind, problem: problem6, fix, message: `${problem6} ${fix}` });
27831
29045
  }
27832
29046
  }
27833
29047
  return out;
@@ -27881,9 +29095,9 @@ function unreadParams(contract, { moduleSource = () => void 0, handlerSource = (
27881
29095
  continue;
27882
29096
  }
27883
29097
  const there = named_.length === 0 ? "no component there reads a source" : `its sources are ${both2(named_.map((one) => `"${one}"`))}`;
27884
- const problem5 = `${page.path} declares param "${name}", and no source on the page honours it (${there}) \u2014 moving it changes nothing on the page.`;
29098
+ const problem6 = `${page.path} declares param "${name}", and no source on the page honours it (${there}) \u2014 moving it changes nothing on the page.`;
27885
29099
  const fix = `Add "${name}" to the honors of the source it should move${named_.length === 1 ? ` ("${named_[0]}")` : ""}, or drop the param.`;
27886
- out.push({ page: page.path, param: name, sources: [...named_], problem: problem5, fix, message: `${problem5} ${fix}` });
29100
+ out.push({ page: page.path, param: name, sources: [...named_], problem: problem6, fix, message: `${problem6} ${fix}` });
27887
29101
  }
27888
29102
  }
27889
29103
  return out;
@@ -29107,9 +30321,9 @@ function unreachablePages2(contract, files = {}) {
29107
30321
  else if (roles.length > 0) why = `it is for ${both3(quoted2(roles))}, and no page ${either5(quoted2(roles))} reaches links to it`;
29108
30322
  else if (gates.length > 0) why = `every nav entry to it is gated to ${either5(quoted2(gates))}, and no role shown that entry reaches a page carrying it`;
29109
30323
  else why = "the only pages that link to it are pages no role reaches";
29110
- const problem5 = `${page.path} is a page of the app, and no role reaches it from the nav \u2014 ${why} \u2014 so only a typed URL opens it.`;
30324
+ const problem6 = `${page.path} is a page of the app, and no role reaches it from the nav \u2014 ${why} \u2014 so only a typed URL opens it.`;
29111
30325
  const fix = roles.length > 0 || gates.length > 0 ? `Gate its nav entry to a role that should open it (pages[].roles on the page, a role's home, or the element's role), or drop the page.` : "Give the pages a nav (a nav element on each page), or drop the page.";
29112
- out.push({ page: page.path, problem: problem5, fix, message: `${problem5} ${fix}` });
30326
+ out.push({ page: page.path, problem: problem6, fix, message: `${problem6} ${fix}` });
29113
30327
  }
29114
30328
  return out;
29115
30329
  }
@@ -29255,9 +30469,9 @@ function unconfirmedStateChanges2(contract, files = {}) {
29255
30469
  named_.push(one.module ? `${one.what} (on ${one.page})` : `${one.what} "${one.id}" (on ${one.page})`);
29256
30470
  }
29257
30471
  const kind = handlers[handler]?.role === "decide" ? "decides a row" : "changes a record's state";
29258
- const problem5 = `handler "${handler}" ${kind}, and ${both3(named_)} ${named_.length === 1 ? "calls" : "call"} it on one click, with no confirm and no dialog in front of it.`;
30472
+ const problem6 = `handler "${handler}" ${kind}, and ${both3(named_)} ${named_.length === 1 ? "calls" : "call"} it on one click, with no confirm and no dialog in front of it.`;
29259
30473
  const fix = 'Put the decision behind a confirm: a dialog part whose on_confirm is the handler (with the reason as one of its inputs), a row action with "confirm", a map-form decide whose decision takes a note, or \u2014 in a custom module \u2014 ui.confirm before ui.call.';
29260
- out.push({ handler, callers: bare.map(({ page, uid, what }) => ({ page, uid, what })), problem: problem5, fix, message: `${problem5} ${fix}` });
30474
+ out.push({ handler, callers: bare.map(({ page, uid, what }) => ({ page, uid, what })), problem: problem6, fix, message: `${problem6} ${fix}` });
29261
30475
  }
29262
30476
  return out;
29263
30477
  }
@@ -29335,18 +30549,18 @@ function unreadReasons2(contract, { handlerSource = () => void 0, moduleSource,
29335
30549
  for (const [key, pages] of [...sent.entries()].sort()) {
29336
30550
  if (read.has(key)) continue;
29337
30551
  const where2 = pages.size === 0 ? "the page" : either5([...pages].sort());
29338
- const problem5 = `${where2} sends handler "${name}" a "${key}", and the handler never reads it \u2014 the reason a person types is thrown away.`;
30552
+ const problem6 = `${where2} sends handler "${name}" a "${key}", and the handler never reads it \u2014 the reason a person types is thrown away.`;
29339
30553
  const fix = `Read it (payload.get("${key}")) and keep it with the decision \u2014 a column, or the audit note \u2014 or stop asking for it.`;
29340
- out.push({ handler: name, key, kind: "unread", pages: [...pages].sort(), problem: problem5, fix, message: `${problem5} ${fix}` });
30554
+ out.push({ handler: name, key, kind: "unread", pages: [...pages].sort(), problem: problem6, fix, message: `${problem6} ${fix}` });
29341
30555
  }
29342
30556
  const lines = pyLines2(source);
29343
30557
  if (!changesState2(name, row, source) || !REJECTS2.test(lines.join("\n"))) continue;
29344
30558
  for (const key of REASON_KEYS2) {
29345
30559
  if (!reads.optional.includes(key) || reads.required.includes(key)) continue;
29346
30560
  if (requiresReason2(lines, key, reasonVars2(lines, key))) continue;
29347
- const problem5 = `handler "${name}" rejects (or overrides), and reads "${key}" without ever requiring it \u2014 a rejection goes through with no reason given.`;
30561
+ const problem6 = `handler "${name}" rejects (or overrides), and reads "${key}" without ever requiring it \u2014 a rejection goes through with no reason given.`;
29348
30562
  const fix = `Refuse the reject path without one: if decision == "rejected" and not ${key}: return {"error": "Give a reason to reject."}.`;
29349
- out.push({ handler: name, key, kind: "optional", pages: [], problem: problem5, fix, message: `${problem5} ${fix}` });
30563
+ out.push({ handler: name, key, kind: "optional", pages: [], problem: problem6, fix, message: `${problem6} ${fix}` });
29350
30564
  }
29351
30565
  }
29352
30566
  return out;
@@ -29383,9 +30597,9 @@ function unliftableRefusals2(contract, { handlerSource = () => void 0 } = {}) {
29383
30597
  }
29384
30598
  if (!refuses) continue;
29385
30599
  const shown2 = line2.trim().replace(/:$/, "");
29386
- const problem5 = `handler "${name}" refuses outright at line ${si + 1} (${shown2}) on the same path where it demands a "${key}" (line ${gi + 1}), and no reason lifts that refusal \u2014 the page asks for a reason to go ahead, and the reason is ignored.`;
30600
+ const problem6 = `handler "${name}" refuses outright at line ${si + 1} (${shown2}) on the same path where it demands a "${key}" (line ${gi + 1}), and no reason lifts that refusal \u2014 the page asks for a reason to go ahead, and the reason is ignored.`;
29387
30601
  const fix = `Let the reason override it (check "${key}" before refusing, and record it), or tell the reader that this state cannot be overridden and stop asking for a reason.`;
29388
- out.push({ handler: name, key, line: si + 1, gateLine: gi + 1, problem: problem5, fix, message: `${problem5} ${fix}` });
30602
+ out.push({ handler: name, key, line: si + 1, gateLine: gi + 1, problem: problem6, fix, message: `${problem6} ${fix}` });
29389
30603
  }
29390
30604
  }
29391
30605
  }
@@ -29401,13 +30615,13 @@ function rolesLandingNowhere2(contract) {
29401
30615
  const exists = pagesOf2(contract).some((page) => page.path === landing);
29402
30616
  if (exists && opens2(contract, model, role, landing)) continue;
29403
30617
  const stated2 = model.home.get(role);
29404
- const problem5 = !exists ? `role "${role}" lands on ${landing} (its home), which is not a page of the app.` : stated2 ? `role "${role}" lands on its home, ${landing}, which is for ${both3(quoted2(pageRoles2(pagesOf2(contract).find((page) => page.path === landing))))} \u2014 so the role opens the app on a 404.` : `role "${role}" states no home and lands on ${landing}, which is for ${both3(quoted2(pageRoles2(pagesOf2(contract).find((page) => page.path === landing))))} \u2014 so the role opens the app on a 404.`;
30618
+ const problem6 = !exists ? `role "${role}" lands on ${landing} (its home), which is not a page of the app.` : stated2 ? `role "${role}" lands on its home, ${landing}, which is for ${both3(quoted2(pageRoles2(pagesOf2(contract).find((page) => page.path === landing))))} \u2014 so the role opens the app on a 404.` : `role "${role}" states no home and lands on ${landing}, which is for ${both3(quoted2(pageRoles2(pagesOf2(contract).find((page) => page.path === landing))))} \u2014 so the role opens the app on a 404.`;
29405
30619
  const fix = `Give "${role}" a home it may open (backend.roles[].home: one of its pages), or add it to that page's pages[].roles.`;
29406
- out.push({ role, page: landing, problem: problem5, fix, message: `${problem5} ${fix}` });
30620
+ out.push({ role, page: landing, problem: problem6, fix, message: `${problem6} ${fix}` });
29407
30621
  }
29408
30622
  return out;
29409
30623
  }
29410
- function sourceRoles2(contract, name) {
30624
+ function sourceRoles2(contract, name, known = /* @__PURE__ */ new Set()) {
29411
30625
  const source = contract?.data?.sources?.[name];
29412
30626
  const stated2 = gateRoles2(source?.requiresRole);
29413
30627
  const elements = isPlainObject17(contract?.elements) ? contract.elements : {};
@@ -29425,7 +30639,7 @@ function sourceRoles2(contract, name) {
29425
30639
  for (const role of roles) if (!union.includes(role)) union.push(role);
29426
30640
  }
29427
30641
  for (const page of pagesOf2(contract)) {
29428
- if (page.owned !== true || readers === 0) continue;
30642
+ if (page.owned !== true || readers === 0 || known.has(page.path)) continue;
29429
30643
  const roles = pageRoles2(page);
29430
30644
  if (roles.length === 0) open2 = true;
29431
30645
  for (const role of roles) if (!union.includes(role)) union.push(role);
@@ -29442,8 +30656,8 @@ function openToEveryRole2(contract) {
29442
30656
  const out = [];
29443
30657
  const say = (kind, names, what, fix) => {
29444
30658
  if (names.length === 0) return;
29445
- const problem5 = `${names.length === 1 ? `${what.one} ${names[0]} states` : `${names.length} ${what.many} \u2014 ${both3(names)} \u2014 state`} no roles, so every role (${all}) may ${what.verb} ${names.length === 1 ? "it" : "them"}.`;
29446
- out.push({ kind, names, problem: problem5, fix, message: `${problem5} ${fix}` });
30659
+ const problem6 = `${names.length === 1 ? `${what.one} ${names[0]} states` : `${names.length} ${what.many} \u2014 ${both3(names)} \u2014 state`} no roles, so every role (${all}) may ${what.verb} ${names.length === 1 ? "it" : "them"}.`;
30660
+ out.push({ kind, names, problem: problem6, fix, message: `${problem6} ${fix}` });
29447
30661
  };
29448
30662
  say(
29449
30663
  "page",
@@ -29830,7 +31044,7 @@ async function runCompile(dir, opts) {
29830
31044
  for (const fill of layoutFills(contract)) report.compose.push(fill);
29831
31045
  }
29832
31046
  const built = await compileFolder(dir, kind, contract, kit, loaded.overlay);
29833
- const refused = built.problems.filter((problem5) => REFUSALS.has(problem5.code));
31047
+ const refused = built.problems.filter((problem6) => REFUSALS.has(problem6.code));
29834
31048
  if (Object.keys(built.files).length === 0 || refused.length > 0) {
29835
31049
  const lines = (refused.length > 0 ? refused : built.problems).map(asProblem);
29836
31050
  report.validate.problems = lines;
@@ -29840,6 +31054,30 @@ async function runCompile(dir, opts) {
29840
31054
  else process.stdout.write(problemHeading(lines.length) + formatProblems(lines) + formatSkipped(skipped));
29841
31055
  return 1;
29842
31056
  }
31057
+ let metricsPlan = null;
31058
+ if (Object.keys(declaredMetrics(contract)).length > 0) {
31059
+ if (kind !== "app") {
31060
+ skipped.push({ what: "metric bindings", why: "data.metrics binds an app's sources; a dashboard's are not bound yet (design 06 \xA77, step 8)" });
31061
+ } else if (!hasServer(opts)) {
31062
+ skipped.push({ what: "metric bindings", why: "no -s server: data.metrics is bound in each company when the app ships" });
31063
+ } else {
31064
+ const planned = await planMetricsFor(contract, opts);
31065
+ metricsPlan = planned.plan;
31066
+ report.metrics = planned.plan === null ? null : planReport(planned.plan);
31067
+ if (planned.problems.length > 0) {
31068
+ report.validate.problems = planned.problems;
31069
+ report.failure = "validation";
31070
+ await writeReport(dir, report);
31071
+ if (opts.json === true) printJson(report);
31072
+ else {
31073
+ process.stdout.write(
31074
+ (planned.plan === null ? "" : renderPlan(planned.plan)) + problemHeading(planned.problems.length) + formatProblems(planned.problems) + formatSkipped(skipped)
31075
+ );
31076
+ }
31077
+ return 1;
31078
+ }
31079
+ }
31080
+ }
29843
31081
  const paths = Object.keys(built.files).sort();
29844
31082
  const escaping = paths.filter((path3) => !insideFolder(dir, path3));
29845
31083
  if (escaping.length > 0) {
@@ -29886,12 +31124,12 @@ async function runCompile(dir, opts) {
29886
31124
  report.digests = Object.fromEntries(plan.owned.map((path3) => [path3, digestOf(built.files[path3] ?? "")]));
29887
31125
  report.inputs = plan.inputs;
29888
31126
  }
29889
- return await afterWrite(dir, target, kind, contract, built, report, kit, skipped, opts);
31127
+ return await afterWrite(dir, target, kind, contract, built, report, kit, skipped, opts, metricsPlan);
29890
31128
  } finally {
29891
31129
  if (check5) await rm4(target, { recursive: true, force: true });
29892
31130
  }
29893
31131
  }
29894
- async function afterWrite(dir, target, kind, contract, built, report, kit, skipped, opts) {
31132
+ async function afterWrite(dir, target, kind, contract, built, report, kit, skipped, opts, metricsPlan = null) {
29895
31133
  const composed = kind === "dashboard" && !isRuntimeDashboard(contract) ? composeOf(contract) : built.contract ?? contract;
29896
31134
  const extracted = await extractContract(target);
29897
31135
  const diffs = roundTripDiffs(composed, extracted, built.problems, { order: built.order });
@@ -29928,21 +31166,29 @@ async function afterWrite(dir, target, kind, contract, built, report, kit, skipp
29928
31166
  return 4;
29929
31167
  }
29930
31168
  if (target !== dir) {
29931
- const server = await checkPackage(dir, built.files, contract, opts, hasServer(opts));
31169
+ const server = await checkPackage(
31170
+ dir,
31171
+ built.files,
31172
+ contract,
31173
+ metricsPlan === null ? opts : { ...opts, metricBindings: plannedBindings(metricsPlan) },
31174
+ hasServer(opts),
31175
+ metricsPlan === null ? [] : metricsPlan.entries.filter((entry) => entry.action === "create").map((entry) => entry.key)
31176
+ );
29932
31177
  const notChecked3 = [...skipped, ...server.skipped];
29933
31178
  if ((opts.expect ?? []).length > 0) notChecked3.push({ what: "headline", why: "--check runs no load" });
29934
31179
  report.validate.problems = server.problems;
29935
31180
  report.validate.checks_skipped = notChecked3;
29936
31181
  if (server.problems.length > 0) report.failure = "push-would-be-refused";
29937
31182
  await writeReport(dir, report);
31183
+ const planned = metricsPlan === null ? "" : renderPlan(metricsPlan);
29938
31184
  if (opts.json === true) printJson(report);
29939
31185
  else if (server.problems.length > 0) {
29940
31186
  process.stdout.write(
29941
- problemHeading(server.problems.length) + formatProblems(server.problems) + formatSkipped(notChecked3)
31187
+ planned + problemHeading(server.problems.length) + formatProblems(server.problems) + formatSkipped(notChecked3)
29942
31188
  );
29943
31189
  } else {
29944
31190
  process.stdout.write(
29945
- problemHeading(0) + checkedLine(report, Object.keys(built.files).length) + warningLines(report) + formatSkipped(notChecked3)
31191
+ planned + problemHeading(0) + checkedLine(report, Object.keys(built.files).length) + warningLines(report) + formatSkipped(notChecked3)
29946
31192
  );
29947
31193
  }
29948
31194
  return server.problems.length > 0 ? 1 : 0;
@@ -29951,9 +31197,31 @@ async function afterWrite(dir, target, kind, contract, built, report, kit, skipp
29951
31197
  await liveness(dir, kind, report, extracted, opts);
29952
31198
  return finish(dir, report, skipped, opts);
29953
31199
  }
31200
+ let metricBindings;
31201
+ if (metricsPlan !== null) {
31202
+ const created = await applyMetricsFor(metricsPlan, opts);
31203
+ report.metrics = planReport(metricsPlan, created.applied ?? void 0);
31204
+ if (opts.json !== true) process.stdout.write(renderPlan(metricsPlan) + (created.applied === null ? "" : renderApplied(metricsPlan, created.applied)));
31205
+ if (created.problems.length > 0) {
31206
+ report.failure = "metrics-create";
31207
+ report.push = { pushed: false, stage: null, revision: null, contract_source: null };
31208
+ await writeReport(dir, report);
31209
+ if (opts.json === true) printJson(report);
31210
+ else {
31211
+ const kept = (created.applied?.created ?? []).map((one) => `"${one.name}" (id ${String(one.id)})`);
31212
+ const drifted = created.problems.some((one) => one.code === "metric_changed");
31213
+ process.stderr.write(
31214
+ (drifted ? "a Datarails metric this ship wrote does not hold what it wrote when read back, so nothing was pushed" : "a Datarails metric could not be created or changed, so nothing was pushed") + (kept.length === 0 ? ".\n" : `; kept, and reused by the next ship: ${kept.join(", ")}.
31215
+ `) + formatProblems(created.problems)
31216
+ );
31217
+ }
31218
+ return 1;
31219
+ }
31220
+ metricBindings = created.applied?.bindings ?? {};
31221
+ }
29954
31222
  let pushed = null;
29955
31223
  try {
29956
- pushed = await pushFolder(dir, { ...opts, force: opts.force });
31224
+ pushed = await pushFolder(dir, { ...opts, force: opts.force, ...metricBindings === void 0 ? {} : { metricBindings } });
29957
31225
  } catch (err) {
29958
31226
  report.push = { pushed: false, stage: null, revision: null, contract_source: null };
29959
31227
  report.failure = isStaleRevision(err) ? "stale-revision" : isLostRace(err) ? "concurrent-publish" : "push-rejected";
@@ -29977,7 +31245,11 @@ async function afterWrite(dir, target, kind, contract, built, report, kit, skipp
29977
31245
  contract_source: "cli",
29978
31246
  ...typeof pushed?.code_version === "number" ? { code_version: pushed.code_version } : {}
29979
31247
  };
31248
+ if (report.metrics !== void 0 && report.metrics !== null && pushed?.metric_bindings !== void 0) {
31249
+ report.metrics["push"] = pushed.metric_bindings;
31250
+ }
29980
31251
  await liveness(dir, kind, report, extracted, opts);
31252
+ await metricsLiveness(report, extracted, opts);
29981
31253
  return finish(dir, report, skipped, opts);
29982
31254
  }
29983
31255
  async function finish(dir, report, skipped, opts) {
@@ -30178,25 +31450,25 @@ async function writePlan(dir, kind, contract, files, paths, force) {
30178
31450
  }
30179
31451
  function ownedPaths(contract) {
30180
31452
  const out = customHandlerPaths(contract);
30181
- const data = obj2(obj2(contract)["data"]);
30182
- for (const row of [...Object.values(obj2(data["sources"])), ...Object.values(obj2(data["widgets"]))]) {
31453
+ const data = obj3(obj3(contract)["data"]);
31454
+ for (const row of [...Object.values(obj3(data["sources"])), ...Object.values(obj3(data["widgets"]))]) {
30183
31455
  const transform = isPlain(row) ? row["transform"] : void 0;
30184
31456
  if (typeof transform === "string" && TRANSFORM_PATH2.test(transform)) out.add(transform);
30185
31457
  }
30186
- for (const element of Object.values(obj2(obj2(contract)["elements"]))) {
31458
+ for (const element of Object.values(obj3(obj3(contract)["elements"]))) {
30187
31459
  if (isPlain(element) && element["type"] === "custom") {
30188
- const module = obj2(element["props"])["module"];
31460
+ const module = obj3(element["props"])["module"];
30189
31461
  if (typeof module === "string" && MODULE_RE.test(module)) out.add(`ui/${module}`);
30190
31462
  }
30191
31463
  }
30192
- const pages = obj2(contract)["pages"];
31464
+ const pages = obj3(contract)["pages"];
30193
31465
  for (const page of Array.isArray(pages) ? pages : []) {
30194
31466
  if (isPlain(page) && page["owned"] === true && typeof page["path"] === "string") out.add(page["path"]);
30195
31467
  }
30196
31468
  return out;
30197
31469
  }
30198
31470
  function customHandlerPaths(contract) {
30199
- const handlers = obj2(obj2(obj2(obj2(contract)["data"])["app"])["handlers"]);
31471
+ const handlers = obj3(obj3(obj3(obj3(contract)["data"])["app"])["handlers"]);
30200
31472
  const out = /* @__PURE__ */ new Set();
30201
31473
  for (const [name, row] of Object.entries(handlers)) {
30202
31474
  if (isPlain(row) && row["role"] === "custom") out.add(`handlers/${name}.py`);
@@ -30239,7 +31511,7 @@ function roundTripDiffs(input, extracted, compilerProblems = [], { order: order2
30239
31511
  const read = order2 === void 0 ? {} : { order: "keys" };
30240
31512
  const left = stripPositional(normaliseApp(input), stated2);
30241
31513
  const right = stripPositional(normaliseApp(withoutUnstatedChrome(input, extracted)), read);
30242
- const declared = compilerProblems.some((problem5) => problem5.code === DECLARED_LOSS);
31514
+ const declared = compilerProblems.some((problem6) => problem6.code === DECLARED_LOSS);
30243
31515
  const diffs = [];
30244
31516
  for (const section of ["app", "pages", "elements", "data"]) {
30245
31517
  if (!(section in left)) continue;
@@ -30314,7 +31586,7 @@ function normaliseApp(contract) {
30314
31586
  }
30315
31587
  const model = record2["data_model"];
30316
31588
  if (isPlain(model)) {
30317
- for (const table of Object.values(obj2(model["tables"]))) {
31589
+ for (const table of Object.values(obj3(model["tables"]))) {
30318
31590
  if (!isPlain(table)) continue;
30319
31591
  const columns = table["columns"];
30320
31592
  if (!isPlain(columns)) continue;
@@ -30334,7 +31606,7 @@ function normaliseApp(contract) {
30334
31606
  }
30335
31607
  }
30336
31608
  }
30337
- const roles = obj2(out["backend"])["roles"];
31609
+ const roles = obj3(out["backend"])["roles"];
30338
31610
  if (Array.isArray(roles)) {
30339
31611
  for (const role of roles) {
30340
31612
  if (isPlain(role) && Array.isArray(role["sees"]) && role["sees"].length === 0) delete role["sees"];
@@ -30342,7 +31614,7 @@ function normaliseApp(contract) {
30342
31614
  }
30343
31615
  return out;
30344
31616
  }
30345
- function obj2(value) {
31617
+ function obj3(value) {
30346
31618
  return isPlain(value) ? value : {};
30347
31619
  }
30348
31620
  function withoutLibrary(section) {
@@ -30686,7 +31958,7 @@ async function recordPulled(dir, contractWritten) {
30686
31958
  } catch {
30687
31959
  return forget();
30688
31960
  }
30689
- if (Object.keys(built.files).length === 0 || built.problems.some((problem5) => REFUSALS.has(problem5.code))) {
31961
+ if (Object.keys(built.files).length === 0 || built.problems.some((problem6) => REFUSALS.has(problem6.code))) {
30690
31962
  return forget();
30691
31963
  }
30692
31964
  const custom = ownedPaths(contract);
@@ -30792,21 +32064,21 @@ async function pushedState(dir) {
30792
32064
  const raw = await readIfPresent(join15(dir, ".appspace.json"));
30793
32065
  if (raw === void 0) return { stage: null, revision: null };
30794
32066
  try {
30795
- const parsed = JSON.parse(raw);
32067
+ const parsed2 = JSON.parse(raw);
30796
32068
  return {
30797
- stage: typeof parsed.stage === "string" ? parsed.stage : null,
30798
- revision: typeof parsed.revision === "number" ? parsed.revision : null
32069
+ stage: typeof parsed2.stage === "string" ? parsed2.stage : null,
32070
+ revision: typeof parsed2.revision === "number" ? parsed2.revision : null
30799
32071
  };
30800
32072
  } catch {
30801
32073
  return { stage: null, revision: null };
30802
32074
  }
30803
32075
  }
30804
- function asProblem(problem5) {
32076
+ function asProblem(problem6) {
30805
32077
  return {
30806
- code: problem5.code,
30807
- pointer: problem5.pointer ?? problem5.where ?? "/",
30808
- message: problem5.message.endsWith(".") ? problem5.message : `${problem5.message}.`,
30809
- fix: problem5.code in PART_REFUSALS ? `Make the part what its entry in ${CATALOG} says: its props, what it reads, what it may hold.` : problem5.code in PARAM_REFUSALS ? `Fix pages[].params or the source's honors: ${PARAM_REFUSALS[problem5.code] ?? ""}.` : problem5.code in APP_FIXES ? APP_FIXES[problem5.code] ?? "" : problem5.code in TRANSFORM_FIXES ? TRANSFORM_FIXES[problem5.code] ?? "" : "The compiler refuses to guess here: state the fact it names in contract.json."
32078
+ code: problem6.code,
32079
+ pointer: problem6.pointer ?? problem6.where ?? "/",
32080
+ message: problem6.message.endsWith(".") ? problem6.message : `${problem6.message}.`,
32081
+ fix: problem6.code in PART_REFUSALS ? `Make the part what its entry in ${CATALOG} says: its props, what it reads, what it may hold.` : problem6.code in PARAM_REFUSALS ? `Fix pages[].params or the source's honors: ${PARAM_REFUSALS[problem6.code] ?? ""}.` : problem6.code in APP_FIXES ? APP_FIXES[problem6.code] ?? "" : problem6.code in TRANSFORM_FIXES ? TRANSFORM_FIXES[problem6.code] ?? "" : "The compiler refuses to guess here: state the fact it names in contract.json."
30810
32082
  };
30811
32083
  }
30812
32084
  function livenessLine(report) {
@@ -30844,6 +32116,7 @@ function describe5(report, skipped) {
30844
32116
  `;
30845
32117
  out += warningLines(report);
30846
32118
  out += livenessLine(report);
32119
+ out += metricsLines(report);
30847
32120
  if (report.push !== null && report.push.pushed) {
30848
32121
  out += `pushed \u2014 ${report.push.stage ?? "draft"}, revision ${String(report.push.revision ?? 0)}
30849
32122
  `;
@@ -30851,10 +32124,149 @@ function describe5(report, skipped) {
30851
32124
  out += formatSkipped(skipped);
30852
32125
  return out;
30853
32126
  }
32127
+ async function planMetricsFor(contract, opts) {
32128
+ const unchecked2 = (why) => ({
32129
+ plan: null,
32130
+ problems: [
32131
+ {
32132
+ code: "metric_unchecked",
32133
+ pointer: "/data/metrics",
32134
+ message: `this company's Datarails metrics could not be checked, so data.metrics cannot be bound: ${why}.`,
32135
+ fix: "Ship again shortly; nothing was created or pushed."
32136
+ }
32137
+ ]
32138
+ });
32139
+ let client;
32140
+ try {
32141
+ client = await connect(opts);
32142
+ } catch (err) {
32143
+ return unchecked2(`no session for ${opts.server ?? "this server"} (${err instanceof Error ? err.message.split("\n")[0] : String(err)})`);
32144
+ }
32145
+ try {
32146
+ const plan = await planMetrics(contract, metricsTransport(client), {
32147
+ appId: appIdOf(contract),
32148
+ appName: isPlain(contract) ? appNameOf(contract) : ""
32149
+ });
32150
+ return { plan, problems: plan.problems };
32151
+ } catch (err) {
32152
+ if (err instanceof MetricsReadError) return unchecked2(err.message);
32153
+ throw err;
32154
+ } finally {
32155
+ client.cleanup();
32156
+ }
32157
+ }
32158
+ async function applyMetricsFor(plan, opts) {
32159
+ let wait;
32160
+ try {
32161
+ wait = metricsWaitOf(opts.metricsWait);
32162
+ } catch (err) {
32163
+ return {
32164
+ applied: null,
32165
+ problems: [{ code: "metric_create", pointer: "/", message: `${err instanceof Error ? err.message : String(err)}.`, fix: "Pass --metrics-wait a number of seconds from 0 to 120." }]
32166
+ };
32167
+ }
32168
+ if (wait.note !== null && opts.json !== true) process.stderr.write(`note: ${wait.note}
32169
+ `);
32170
+ let client;
32171
+ try {
32172
+ client = await connect(opts);
32173
+ } catch (err) {
32174
+ return {
32175
+ applied: null,
32176
+ problems: [
32177
+ {
32178
+ code: "metric_create",
32179
+ pointer: "/data/metrics",
32180
+ message: `no session to create the metrics with (${err instanceof Error ? err.message.split("\n")[0] : String(err)}).`,
32181
+ fix: "Log in again and ship again; nothing was created or pushed."
32182
+ }
32183
+ ]
32184
+ };
32185
+ }
32186
+ try {
32187
+ const applied = await applyPlan(plan, metricsTransport(client), { waitSeconds: wait.seconds });
32188
+ return { applied, problems: applied.ok ? [] : applied.problems };
32189
+ } finally {
32190
+ client.cleanup();
32191
+ }
32192
+ }
32193
+ function plannedBindings(plan) {
32194
+ const out = {};
32195
+ for (const entry of plan.entries) {
32196
+ if (entry.metric_id === null || entry.origin === null) continue;
32197
+ out[entry.key] = {
32198
+ metric_id: entry.metric_id,
32199
+ origin: entry.origin,
32200
+ ...entry.reuse_key === null ? {} : { reuse_key: entry.reuse_key }
32201
+ };
32202
+ }
32203
+ return out;
32204
+ }
32205
+ async function metricsLiveness(report, contract, opts) {
32206
+ if (report.metrics === void 0 || report.metrics === null || report.app_id === null) return;
32207
+ if (Object.keys(metricSources(contract)).length === 0) return;
32208
+ let checks;
32209
+ try {
32210
+ const client = await connect(opts);
32211
+ try {
32212
+ checks = await checkMetricSources(metricsTransport(client), report.app_id, contract);
32213
+ } finally {
32214
+ client.cleanup();
32215
+ }
32216
+ } catch (err) {
32217
+ checks = Object.entries(metricSources(contract)).map(([source, key]) => ({
32218
+ source,
32219
+ key,
32220
+ state: "unchecked",
32221
+ message: `could not be run (${err instanceof Error ? err.message.split("\n")[0] : String(err)})`
32222
+ }));
32223
+ }
32224
+ report.metrics["live"] = checks;
32225
+ const failed = checks.filter((one) => one.state === "failed");
32226
+ if (failed.length === 0) return;
32227
+ const problems = failed.map((one) => ({
32228
+ code: "metric-live",
32229
+ pointer: `/data/sources/${one.source}`,
32230
+ message: `source "${one.source}" (metric @${one.key}) drew no figure once pushed: ${one.message}`,
32231
+ fix: "Check the metric in Datarails and the binding ship printed; a card over a declared metric never falls back to another figure."
32232
+ }));
32233
+ report.liveness = {
32234
+ problems: [...report.liveness?.problems ?? [], ...problems],
32235
+ skipped: report.liveness?.skipped ?? [],
32236
+ widgets: report.liveness?.widgets ?? null,
32237
+ ...report.liveness?.others === void 0 ? {} : { others: report.liveness.others }
32238
+ };
32239
+ report.verify.problems = [...report.verify.problems, ...problems];
32240
+ report.verify.violations = (report.verify.violations ?? 0) + problems.length;
32241
+ if (report.failure === null) report.failure = "liveness";
32242
+ }
32243
+ function metricsLines(report) {
32244
+ const block = report.metrics;
32245
+ if (block === void 0 || block === null) return "";
32246
+ let out = "";
32247
+ for (const line2 of bindingWarnings(block["push"])) out += `warning: ${line2}
32248
+ `;
32249
+ const live = Array.isArray(block["live"]) ? block["live"] : [];
32250
+ for (const one of live) {
32251
+ const said2 = one.state === "figure" ? "read its figure" : one.state === "preparing" ? `preparing \u2014 ${one.message} (neither blank nor a pass: run ship again once Datarails has it)` : one.state === "failed" ? `NO figure \u2014 ${one.message}` : `not checked \u2014 ${one.message}`;
32252
+ out += `metric source "${one.source}" (@${one.key}): ${said2}
32253
+ `;
32254
+ }
32255
+ return out;
32256
+ }
32257
+ function metricHelpLines() {
32258
+ return [
32259
+ "Shipped (with -s, --check or --push), each data.metrics key is bound in that company: reused where a metric computes the same thing,",
32260
+ 'bound by name with "reuse": "name", changed in place where App Space made it for this app (and says no one else reads it), else created',
32261
+ '(as you, with metrics:write; filed under "App Space", its description naming the app). --check prints the plan and creates nothing. Refused with:',
32262
+ ...Object.entries(METRIC_SHIP_REFUSALS).map(([code, what]) => ` ${code.padEnd(22)}${what}`),
32263
+ ' metric-live once pushed, a source over a declared metric drew a sentence and no figure (exit 5); "preparing" is said and is neither blank nor a pass'
32264
+ ];
32265
+ }
30854
32266
  function registerCompileCommand(parent) {
30855
32267
  withJsonOption(
30856
32268
  withServerOptions(
30857
- withAssemblyOptions(parent.command("compile [dir]").description("Compile contract.json into the package, and prove it round-trips.").option("--check", "Validate, compile and round-trip into a scratch folder, and verify \u2014 everything a push runs first. Write nothing.").option("--push", "Push the compiled package, contract attached.").option("--seed <file>", "Seed rows, as JSON. Forwarded to the push.").option("--catalog <file>", "Check source ids against this catalog instead of the tenant.").option("--force", "Write over files this compiler did not write (never a custom handler, custom module or owned page).").option("--update-block", "After a push that passed, print the appspace_app_update block a build reply ends with.").addHelpText(
32269
+ withAssemblyOptions(parent.command("compile [dir]").description("Compile contract.json into the package, and prove it round-trips.").option("--check", "Validate, compile and round-trip into a scratch folder, and verify \u2014 everything a push runs first. Write nothing.").option("--push", "Push the compiled package, contract attached.").option("--seed <file>", "Seed rows, as JSON. Forwarded to the push.").option("--catalog <file>", "Check source ids against this catalog instead of the tenant.").option("--force", "Write over files this compiler did not write (never a custom handler, custom module or owned page).").option("--update-block", "After a push that passed, print the appspace_app_update block a build reply ends with.").option("--metrics-wait <seconds>", "With --push: how long to wait for a BASE metric this ship created to extract before pushing anyway (default 60, at most 120).").addHelpText(
30858
32270
  "after",
30859
32271
  [
30860
32272
  "",
@@ -30876,6 +32288,7 @@ function registerCompileCommand(parent) {
30876
32288
  'A declared Datarails metric (data.metrics, read by a source as from.financeos.metric "@<key>") is refused with:',
30877
32289
  ...Object.entries(METRIC_REFUSALS).map(([code, what]) => ` ${code.padEnd(22)}${what}`),
30878
32290
  " (warnings, never refusals: metric_window, a window function Datarails gates by company; metric_unused, a declaration no source reads; expr_could_be_metric, an expr a calc metric could say)",
32291
+ ...metricHelpLines(),
30879
32292
  "",
30880
32293
  "A transform (a dashboard dataset series row's transforms/<name>.py, or an app source's) is refused with:",
30881
32294
  ...Object.entries(TRANSFORM_REFUSALS).map(([code, what]) => ` ${code.padEnd(22)}${what}`)
@@ -30907,7 +32320,7 @@ function registerShipCommand(parent) {
30907
32320
  parent.command("ship [dir]").description("Assemble, compile, push and prove a dashboard pass; print the update block the reply ends with.").option("--seed <file>", "Seed rows, as JSON. Forwarded to the push.").option("--force", "Write over files this compiler did not write (never a custom handler). Refused with --onto-live.").option(
30908
32321
  "--onto-live",
30909
32322
  "Land this pass's cards on the app as it is live now, keeping every card already there: pull it, add them, push against its revision, and start over when another push lands first. Needs --app-id and --skeleton, and a folder of its own."
30910
- )
32323
+ ).option("--metrics-wait <seconds>", "How long to wait for a BASE metric this ship created to extract before pushing anyway (default 60, at most 120).").addHelpText("after", ["", ...metricHelpLines()].join("\n"))
30911
32324
  )
30912
32325
  )
30913
32326
  ).action(async (dirArg, opts) => {
@@ -31086,7 +32499,7 @@ async function runEject(target, opts) {
31086
32499
  }
31087
32500
  }
31088
32501
  async function ejectHandler(dir, contract, name) {
31089
- const handlers = obj3(obj3(obj3(contract["data"])["app"])["handlers"]);
32502
+ const handlers = obj4(obj4(obj4(contract["data"])["app"])["handlers"]);
31090
32503
  const row = handlers[name];
31091
32504
  if (!isPlain2(row)) {
31092
32505
  const known = Object.keys(handlers).sort();
@@ -31144,7 +32557,7 @@ function literalEnd2(text3, open2) {
31144
32557
  return -1;
31145
32558
  }
31146
32559
  async function ejectSource(dir, contract, name) {
31147
- const sources = obj3(obj3(contract["data"])["sources"]);
32560
+ const sources = obj4(obj4(contract["data"])["sources"]);
31148
32561
  if (!isPlain2(sources[name])) {
31149
32562
  const known = Object.keys(sources).sort();
31150
32563
  throw new Refused(
@@ -31154,7 +32567,7 @@ async function ejectSource(dir, contract, name) {
31154
32567
  const path3 = `handlers/${name}.py`;
31155
32568
  if (!insideFolder(dir, path3)) throw new Refused(`${JSON.stringify(path3)} is not a path inside ${dir}.`);
31156
32569
  const dependents = Object.keys(sources).filter((other) => {
31157
- const from = obj3(sources[other])["from"];
32570
+ const from = obj4(sources[other])["from"];
31158
32571
  return Array.isArray(from) && from.includes(name);
31159
32572
  }).sort();
31160
32573
  if (dependents.length > 0) {
@@ -31180,9 +32593,9 @@ async function ejectSource(dir, contract, name) {
31180
32593
  throw new Refused(`${path3} as compiled carries no SOURCES = {\u2026} table, so it is not a generated source handler.`);
31181
32594
  }
31182
32595
  await mayPlace(dir, path3, body, [compiled]);
31183
- const data = obj3(contract["data"]);
31184
- const app = obj3(data["app"]);
31185
- const handlers = obj3(app["handlers"]);
32596
+ const data = obj4(contract["data"]);
32597
+ const app = obj4(data["app"]);
32598
+ const handlers = obj4(app["handlers"]);
31186
32599
  handlers[name] = { role: "custom" };
31187
32600
  app["handlers"] = handlers;
31188
32601
  data["app"] = app;
@@ -31210,8 +32623,8 @@ async function ejectPage(dir, contract, path3) {
31210
32623
  throw new Refused(`${path3} as compiled has no <head> to carry ${OWNED_META}.`);
31211
32624
  }
31212
32625
  await mayPlace(dir, path3, marked2, [compiled]);
31213
- const elements = obj3(contract["elements"]);
31214
- const gone = new Set(Object.keys(elements).filter((uid) => obj3(elements[uid])["page"] === path3));
32626
+ const elements = obj4(contract["elements"]);
32627
+ const gone = new Set(Object.keys(elements).filter((uid) => obj4(elements[uid])["page"] === path3));
31215
32628
  for (const uid of gone) delete elements[uid];
31216
32629
  for (const element of Object.values(elements)) {
31217
32630
  if (!isPlain2(element) || !Array.isArray(element["children"])) continue;
@@ -31222,11 +32635,11 @@ async function ejectPage(dir, contract, path3) {
31222
32635
  kept["owned"] = true;
31223
32636
  if (gate !== null && kept["roles"] === void 0) kept["role"] = gate;
31224
32637
  pages[index] = kept;
31225
- const app = obj3(obj3(contract["data"])["app"]);
32638
+ const app = obj4(obj4(contract["data"])["app"]);
31226
32639
  if (Array.isArray(app["screens"])) {
31227
32640
  app["screens"] = app["screens"].filter((screen) => !(isPlain2(screen) && screen["page"] === path3));
31228
32641
  }
31229
- const sources = Object.keys(obj3(obj3(contract["data"])["sources"]));
32642
+ const sources = Object.keys(obj4(obj4(contract["data"])["sources"]));
31230
32643
  const served = sources.filter((name) => files[`handlers/${name}.py`] !== void 0);
31231
32644
  const wrote = await commit(dir, contract, path3, marked2, (after) => {
31232
32645
  const lost = served.filter((name) => after[`handlers/${name}.py`] === void 0).sort();
@@ -31242,7 +32655,7 @@ function unservedAfter(path3, lost) {
31242
32655
  const one = lost.length === 1;
31243
32656
  const names = lost.map((name) => JSON.stringify(name)).join(", ");
31244
32657
  const files = lost.map((name) => `handlers/${name}.py`).join(", ");
31245
- return `${path3} is the only page whose cards read source${one ? "" : "s"} ${names}. Owned, the page has no elements in the contract, and the compiler writes a handler only for a source some element reads, so ${files} would go and the page would call ${one ? "a handler" : "handlers"} that ${one ? "is" : "are"} not there. Keep a card that reads ${one ? "it" : "each"} on a compiled page, or eject ${one ? "the source" : "each source"} first (app-space eject ${lost.map((name) => `source:${name}`).join(", ")}), which makes it a custom handler the compiler always carries, where that eject is allowed: eject source: says when it is not.`;
32658
+ return `after this eject the compiler would write no handler for source${one ? "" : "s"} ${names}, which ${path3} reads, so ${files} would go and the page would call ${one ? "a handler" : "handlers"} that ${one ? "is" : "are"} not there. The compiler counts an owned page as a reader of what the contract it embeds reads, and eject keeps that contract on the page, so this is a bug in the compiler or in eject, not in your contract. Meanwhile, keep a card that reads ${one ? "it" : "each"} on a compiled page, or eject ${one ? "the source" : "each source"} first (app-space eject ${lost.map((name) => `source:${name}`).join(", ")}), which makes it a custom handler the compiler always carries, where that eject is allowed: eject source: says when it is not.`;
31246
32659
  }
31247
32660
  function navGateOf(files, path3) {
31248
32661
  const gates = navGates;
@@ -31267,19 +32680,19 @@ ${OWNED_META}${html.slice(at2)}`;
31267
32680
  ${OWNED_META}${html.slice(at)}`;
31268
32681
  }
31269
32682
  async function ejectElement(dir, contract, uid) {
31270
- const elements = obj3(contract["elements"]);
32683
+ const elements = obj4(contract["elements"]);
31271
32684
  const element = elements[uid];
31272
32685
  if (!isPlain2(element)) {
31273
32686
  throw new Refused(`the contract has no element ${JSON.stringify(uid)}; an element's uid is its key in contract.elements.`);
31274
32687
  }
31275
32688
  const type = String(element["type"] ?? "");
31276
- if (type === "custom") throw new Refused(`${uid} is already a custom element, drawn by ${String(obj3(element["props"])["module"] ?? "its module")}.`);
32689
+ if (type === "custom") throw new Refused(`${uid} is already a custom element, drawn by ${String(obj4(element["props"])["module"] ?? "its module")}.`);
31277
32690
  if (!EJECTABLE.has(type)) {
31278
32691
  throw new Refused(
31279
32692
  `${uid} is a ${type || "typeless element"}, and element: ejects a chart or a stat \u2014 a root that draws one source's answer. To own a ${type || "screen"}'s markup, eject its page: page:${String(element["page"] ?? "<ui/path.html>")}.`
31280
32693
  );
31281
32694
  }
31282
- const props = obj3(element["props"]);
32695
+ const props = obj4(element["props"]);
31283
32696
  const name = freeName(moduleName(props["id"], type, uid), ownedPaths(contract));
31284
32697
  const module = `components/${name}.js`;
31285
32698
  const path3 = `ui/${module}`;
@@ -31360,9 +32773,9 @@ ${lines}
31360
32773
  return built.files;
31361
32774
  }
31362
32775
  function refusedLines(built) {
31363
- const refused = built.problems.filter((problem5) => REFUSALS.has(problem5.code));
32776
+ const refused = built.problems.filter((problem6) => REFUSALS.has(problem6.code));
31364
32777
  if (Object.keys(built.files).length > 0 && refused.length === 0) return null;
31365
- return (refused.length > 0 ? refused : built.problems).map((problem5) => ` [${problem5.code}] ${problem5.message}`).join("\n");
32778
+ return (refused.length > 0 ? refused : built.problems).map((problem6) => ` [${problem6.code}] ${problem6.message}`).join("\n");
31366
32779
  }
31367
32780
  async function mayPlace(dir, path3, text3, same5) {
31368
32781
  const existing = await readIfPresent(join16(dir, path3));
@@ -31407,7 +32820,7 @@ ${lines}
31407
32820
  function isPlain2(value) {
31408
32821
  return typeof value === "object" && value !== null && !Array.isArray(value);
31409
32822
  }
31410
- function obj3(value) {
32823
+ function obj4(value) {
31411
32824
  return isPlain2(value) ? value : {};
31412
32825
  }
31413
32826
  function registerEjectCommand(parent) {
@@ -31639,13 +33052,13 @@ function parseFileSpec(spec) {
31639
33052
  return { field, path: path3 };
31640
33053
  }
31641
33054
  function parseFileSpecs(specs) {
31642
- const parsed = specs.map(parseFileSpec);
33055
+ const parsed2 = specs.map(parseFileSpec);
31643
33056
  const seen = /* @__PURE__ */ new Set();
31644
- for (const one of parsed) {
33057
+ for (const one of parsed2) {
31645
33058
  if (seen.has(one.field)) throw new Error(`--file ${one.field} is given twice; give each field one file.`);
31646
33059
  seen.add(one.field);
31647
33060
  }
31648
- return parsed;
33061
+ return parsed2;
31649
33062
  }
31650
33063
  var MIME_TYPES = {
31651
33064
  ".pdf": "application/pdf",
@@ -32836,15 +34249,15 @@ function answerOf(res) {
32836
34249
  if (res.body.length === 0 || res.body.length > ROWS_BODY_LIMIT) return {};
32837
34250
  const type = Object.entries(res.headers).find(([key]) => key.toLowerCase() === "content-type")?.[1];
32838
34251
  if (type !== void 0 && !/json/i.test(type)) return {};
32839
- let parsed;
34252
+ let parsed2;
32840
34253
  try {
32841
- parsed = JSON.parse(res.body.toString("utf8"));
34254
+ parsed2 = JSON.parse(res.body.toString("utf8"));
32842
34255
  } catch {
32843
34256
  return {};
32844
34257
  }
32845
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
34258
+ if (typeof parsed2 !== "object" || parsed2 === null || Array.isArray(parsed2)) return {};
32846
34259
  const out = { object: true };
32847
- const { rows: rows2, error, needs } = parsed;
34260
+ const { rows: rows2, error, needs } = parsed2;
32848
34261
  if (needs && typeof needs === "object" && !Array.isArray(needs)) {
32849
34262
  const reason = needs.reason;
32850
34263
  if (reason === true || reason && typeof reason === "object" && !Array.isArray(reason)) {
@@ -32925,8 +34338,8 @@ function harvestNodes(document) {
32925
34338
  const filter = el.getAttribute(FILTER);
32926
34339
  if (filter !== null && filter !== "") props.filter = readFilter(filter);
32927
34340
  const args = el.getAttribute(ARGS);
32928
- const parsed = args !== null && args !== "" ? readArgs(args) : null;
32929
- if (parsed !== null) props.args = parsed;
34341
+ const parsed2 = args !== null && args !== "" ? readArgs(args) : null;
34342
+ if (parsed2 !== null) props.args = parsed2;
32930
34343
  const typed = el.getAttribute(PROPS);
32931
34344
  const typedProps = typed !== null && typed !== "" ? readProps(typed) : null;
32932
34345
  if (typedProps !== null) props.props = typedProps;
@@ -33222,8 +34635,8 @@ function createCanvas2dStub(canvas) {
33222
34635
  for (const name of CANVAS_2D_VOID_METHODS) target[name] = () => void 0;
33223
34636
  const spares = /* @__PURE__ */ new Map();
33224
34637
  return new Proxy(target, {
33225
- get(obj4, prop, receiver) {
33226
- if (typeof prop === "symbol" || prop in obj4) return Reflect.get(obj4, prop, receiver);
34638
+ get(obj5, prop, receiver) {
34639
+ if (typeof prop === "symbol" || prop in obj5) return Reflect.get(obj5, prop, receiver);
33227
34640
  if (prop === "then") return void 0;
33228
34641
  let spare = spares.get(prop);
33229
34642
  if (spare === void 0) {
@@ -33507,13 +34920,13 @@ async function runUiSmoke(opts) {
33507
34920
  const graceUntil = Date.now() + graceMs;
33508
34921
  while (Date.now() < deadline) {
33509
34922
  if (drivePrompt?.()) lastActivity = Date.now();
33510
- const parsed = dom.window.document.readyState === "complete";
34923
+ const parsed2 = dom.window.document.readyState === "complete";
33511
34924
  const started = calls.length > since || inFlight > 0;
33512
34925
  if (!started && Date.now() < graceUntil) {
33513
34926
  await sleep(25);
33514
34927
  continue;
33515
34928
  }
33516
- if (parsed && inFlight === 0 && Date.now() - lastActivity >= quietMs) break;
34929
+ if (parsed2 && inFlight === 0 && Date.now() - lastActivity >= quietMs) break;
33517
34930
  await sleep(25);
33518
34931
  }
33519
34932
  }
@@ -34788,9 +36201,9 @@ function unboundControls2(contract, files = {}) {
34788
36201
  const param = page.params.find((one) => one?.name === name) ?? {};
34789
36202
  const kind = named12(param.kind) ? param.kind : null;
34790
36203
  const opens3 = param.default === void 0 ? "no value at all" : `its default (${JSON.stringify(param.default)})`;
34791
- const problem5 = `${page.path} declares param "${name}"${kind === null ? "" : ` (${kind})`}, and no control on the page writes it \u2014 no part in param mode, no custom that calls ui.setParam("${name}", \u2026) or declares it in declares.produces \u2014 so the page only ever shows ${opens3}.`;
36204
+ const problem6 = `${page.path} declares param "${name}"${kind === null ? "" : ` (${kind})`}, and no control on the page writes it \u2014 no part in param mode, no custom that calls ui.setParam("${name}", \u2026) or declares it in declares.produces \u2014 so the page only ever shows ${opens3}.`;
34792
36205
  const fix = `Give it a control \u2014 a part in param mode (props.props.param "${name}": a slider, a number_input, a select, a date_picker or a toggle), or a custom whose module calls ui.setParam("${name}", value) \u2014 or drop the param.`;
34793
- out.push({ page: page.path, param: name, kind, problem: problem5, fix, message: `${problem5} ${fix}` });
36206
+ out.push({ page: page.path, param: name, kind, problem: problem6, fix, message: `${problem6} ${fix}` });
34794
36207
  }
34795
36208
  }
34796
36209
  return out;
@@ -34844,9 +36257,9 @@ function unreadParams2(contract, { moduleSource = () => void 0, handlerSource =
34844
36257
  continue;
34845
36258
  }
34846
36259
  const there = named_.length === 0 ? "no component there reads a source" : `its sources are ${both4(named_.map((one) => `"${one}"`))}`;
34847
- const problem5 = `${page.path} declares param "${name}", and no source on the page honours it (${there}) \u2014 moving it changes nothing on the page.`;
36260
+ const problem6 = `${page.path} declares param "${name}", and no source on the page honours it (${there}) \u2014 moving it changes nothing on the page.`;
34848
36261
  const fix = `Add "${name}" to the honors of the source it should move${named_.length === 1 ? ` ("${named_[0]}")` : ""}, or drop the param.`;
34849
- out.push({ page: page.path, param: name, sources: [...named_], problem: problem5, fix, message: `${problem5} ${fix}` });
36262
+ out.push({ page: page.path, param: name, sources: [...named_], problem: problem6, fix, message: `${problem6} ${fix}` });
34850
36263
  }
34851
36264
  }
34852
36265
  return out;
@@ -34888,9 +36301,9 @@ function pushedContract(bundle, pages) {
34888
36301
  const found = contractBlockText(html);
34889
36302
  if (found === null) continue;
34890
36303
  try {
34891
- const parsed = JSON.parse(found);
34892
- if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
34893
- return parsed;
36304
+ const parsed2 = JSON.parse(found);
36305
+ if (typeof parsed2 === "object" && parsed2 !== null && !Array.isArray(parsed2)) {
36306
+ return parsed2;
34894
36307
  }
34895
36308
  } catch {
34896
36309
  }
@@ -35823,11 +37236,11 @@ function customDeclaresFindings(contract, bundle, runs) {
35823
37236
  if (contract === null || !isAppContract3(contract)) return [];
35824
37237
  const out = [];
35825
37238
  const said2 = /* @__PURE__ */ new Set();
35826
- const undeclared = (uid, page, module, handler, problem5, fix) => {
37239
+ const undeclared = (uid, page, module, handler, problem6, fix) => {
35827
37240
  const key = `${uid}|${handler}`;
35828
37241
  if (said2.has(key)) return;
35829
37242
  said2.add(key);
35830
- out.push({ code: "custom-undeclared-call", where: `${page ?? "ui/"} \u2192 ${module}`, message: problem5, fix, see: SEE.declares });
37243
+ out.push({ code: "custom-undeclared-call", where: `${page ?? "ui/"} \u2192 ${module}`, message: problem6, fix, see: SEE.declares });
35831
37244
  };
35832
37245
  for (const found of DECLARES.undeclaredCustomCalls(contract, pushedFiles(bundle, {}))) {
35833
37246
  undeclared(found.uid, found.page, found.module, found.handler, found.problem, found.fix);
@@ -35840,8 +37253,8 @@ function customDeclaresFindings(contract, bundle, runs) {
35840
37253
  if (element?.type !== "custom" || declares === null || typeof module !== "string") continue;
35841
37254
  if (declares.calls.includes(entry.handler)) continue;
35842
37255
  const id = typeof element.props?.["id"] === "string" ? element.props["id"] : entry.uid;
35843
- const { problem: problem5, fix } = DECLARES.undeclaredCallMessage(id, module, entry.handler, declares.calls, "observed at load");
35844
- undeclared(entry.uid, element.page ?? null, module, entry.handler, problem5, fix);
37256
+ const { problem: problem6, fix } = DECLARES.undeclaredCallMessage(id, module, entry.handler, declares.calls, "observed at load");
37257
+ undeclared(entry.uid, element.page ?? null, module, entry.handler, problem6, fix);
35845
37258
  }
35846
37259
  }
35847
37260
  for (const run of runs) {
@@ -36171,13 +37584,13 @@ async function loadFixtures(dir, explicit) {
36171
37584
  if (explicit !== void 0) throw new Error(`no fixtures file at ${path3}.`);
36172
37585
  return { fixtures: { cases: [], ui: { entry: "index.html" } } };
36173
37586
  }
36174
- let parsed;
37587
+ let parsed2;
36175
37588
  try {
36176
- parsed = JSON.parse(raw);
37589
+ parsed2 = JSON.parse(raw);
36177
37590
  } catch {
36178
37591
  throw new FixtureError(`${path3} is not valid JSON.`);
36179
37592
  }
36180
- return { fixtures: parseFixtures(parsed, path3), source: path3 };
37593
+ return { fixtures: parseFixtures(parsed2, path3), source: path3 };
36181
37594
  }
36182
37595
  async function runHandlerCases(cases, dir, appId, client, roleOf = () => void 0) {
36183
37596
  const code = /* @__PURE__ */ new Map();
@@ -36505,7 +37918,7 @@ ${attached}`;
36505
37918
  }
36506
37919
  out += ` FAIL ${head}
36507
37920
  ${attached}`;
36508
- for (const problem5 of outcome.problems) out += ` ${problem5}
37921
+ for (const problem6 of outcome.problems) out += ` ${problem6}
36509
37922
  `;
36510
37923
  }
36511
37924
  return out;
@@ -36570,7 +37983,7 @@ function rederiveComponentBindings(contract) {
36570
37983
  model
36571
37984
  });
36572
37985
  contract.problems = [
36573
- ...contract.problems.filter((problem5) => !COMPONENT_PROBLEM_CODES.has(problem5.code)),
37986
+ ...contract.problems.filter((problem6) => !COMPONENT_PROBLEM_CODES.has(problem6.code)),
36574
37987
  ...problems
36575
37988
  ].sort(byProblem4);
36576
37989
  }
@@ -36626,7 +38039,7 @@ function mergeDomComponents(contract, components, page) {
36626
38039
  contract.elements[uid] = element;
36627
38040
  }
36628
38041
  contract.problems = contract.problems.filter(
36629
- (problem5) => !(problem5.code === "duplicate_dom_id" && problem5.where.startsWith(`${page}@dom#`))
38042
+ (problem6) => !(problem6.code === "duplicate_dom_id" && problem6.where.startsWith(`${page}@dom#`))
36630
38043
  );
36631
38044
  for (const { id, uid } of duplicates) {
36632
38045
  contract.problems.push({
@@ -36739,15 +38152,15 @@ function untouchedHandlers(declared, cases, interactions, runs = []) {
36739
38152
  return declared.filter((name) => !touched.has(name) && !calls.some((call) => callsHandler(call.url, name))).sort();
36740
38153
  }
36741
38154
  async function readSeed(dir) {
36742
- let parsed;
38155
+ let parsed2;
36743
38156
  try {
36744
- parsed = JSON.parse(await readFile13(join18(dir, "seed.json"), "utf8"));
38157
+ parsed2 = JSON.parse(await readFile13(join18(dir, "seed.json"), "utf8"));
36745
38158
  } catch {
36746
38159
  return null;
36747
38160
  }
36748
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
38161
+ if (typeof parsed2 !== "object" || parsed2 === null || Array.isArray(parsed2)) return null;
36749
38162
  const seed = {};
36750
- for (const [table, rows2] of Object.entries(parsed)) {
38163
+ for (const [table, rows2] of Object.entries(parsed2)) {
36751
38164
  if (!Array.isArray(rows2)) continue;
36752
38165
  seed[table] = rows2.filter(
36753
38166
  (row) => typeof row === "object" && row !== null && !Array.isArray(row)
@@ -37241,11 +38654,16 @@ function describeTryRole(one) {
37241
38654
  function rolesAllowed(handler, view) {
37242
38655
  return view.roles.filter((role) => view.allowed[role]?.[handler] === true);
37243
38656
  }
38657
+ function unboundNote(result) {
38658
+ const meta = result?.meta;
38659
+ if (meta?.source?.status !== "unbound") return null;
38660
+ return 'this source reads a metric the contract declares (data.metrics), and a dry-run carries no metric bindings \u2014 they belong to a pushed version \u2014 so it always answers "isn\'t set up" here. `app-space ship` binds the metric and runs this source against the draft after its push.';
38661
+ }
37244
38662
  function registerTryCommand(parent) {
37245
38663
  withJsonOption(
37246
38664
  withServerOptions(
37247
38665
  parent.command("try <handler>").description(
37248
- "Run one handler against the app's real ctx, using the code on disk, without publishing."
38666
+ `Run one handler against the app's real ctx, using the code on disk, without publishing. A source over a declared Datarails metric (data.metrics) answers "isn't set up" here: a dry-run carries no metric bindings; ship checks it after its push.`
37249
38667
  ).option("--dir <path>", "The app folder (default: current directory).", ".").option(
37250
38668
  "--payload <json>",
37251
38669
  "JSON payload for the handler, or @<path> to read it from a JSON file (relative to the working directory).",
@@ -37325,6 +38743,9 @@ function registerTryCommand(parent) {
37325
38743
  if (res.ok) {
37326
38744
  process.stdout.write(`ok
37327
38745
  ${JSON.stringify(res.result, null, 2)}
38746
+ `);
38747
+ const note = unboundNote(res.result);
38748
+ if (note !== null) process.stdout.write(`note: ${note}
37328
38749
  `);
37329
38750
  return;
37330
38751
  }