@datarails-ai/dr-appspace-cli 0.12.38 → 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 (2) hide show
  1. package/dist/cli.js +1706 -314
  2. 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.38",
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-26T15:07:50.401Z";
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,9 +9622,9 @@ 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
  }
@@ -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"];
@@ -12842,11 +12873,11 @@ function metricFilterProblem(filter) {
12842
12873
  if (filter.exclude !== void 0 && typeof filter.exclude !== "boolean") return `states exclude ${JSON.stringify(filter.exclude)}; exclude is true or false`;
12843
12874
  return null;
12844
12875
  }
12845
- function metricReaches(operandsOf, start, target, seen) {
12876
+ function metricReaches(operandsOf2, start, target, seen) {
12846
12877
  if (start === target) return true;
12847
12878
  if (seen.has(start)) return false;
12848
12879
  seen.add(start);
12849
- return (operandsOf.get(start) ?? []).some((one) => metricReaches(operandsOf, one, target, seen));
12880
+ return (operandsOf2.get(start) ?? []).some((one) => metricReaches(operandsOf2, one, target, seen));
12850
12881
  }
12851
12882
  function calcShaped(node) {
12852
12883
  if (node[0] === "num" || node[0] === "name") return true;
@@ -12975,12 +13006,12 @@ function metricProblems(metrics, sources) {
12975
13006
  }
12976
13007
  }
12977
13008
  }
12978
- const operandsOf = /* @__PURE__ */ new Map();
13009
+ const operandsOf2 = /* @__PURE__ */ new Map();
12979
13010
  for (const key of [...formulas.keys()].sort(natural3)) {
12980
13011
  const { problems: wrong, operands, windows } = metricFormulaProblems(formulas.get(key), declaredKeys);
12981
13012
  for (const one of wrong) say("metric_formula", at(key, "formula"), `metric "${key}"'s formula ${one}`);
12982
13013
  if (wrong.length > 0) continue;
12983
- operandsOf.set(key, operands);
13014
+ operandsOf2.set(key, operands);
12984
13015
  if (windows.length > 0) {
12985
13016
  advise(
12986
13017
  "metric_window",
@@ -12989,9 +13020,9 @@ function metricProblems(metrics, sources) {
12989
13020
  );
12990
13021
  }
12991
13022
  }
12992
- for (const key of [...operandsOf.keys()].sort(natural3)) {
12993
- for (const dep of operandsOf.get(key)) {
12994
- 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())) {
12995
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`);
12996
13027
  break;
12997
13028
  }
@@ -13034,7 +13065,7 @@ function metricProblems(metrics, sources) {
13034
13065
  const visit = (key) => {
13035
13066
  if (used.has(key)) return;
13036
13067
  used.add(key);
13037
- for (const one of operandsOf.get(key) ?? []) visit(one);
13068
+ for (const one of operandsOf2.get(key) ?? []) visit(one);
13038
13069
  };
13039
13070
  for (const key of read) visit(key);
13040
13071
  for (const key of declaredKeys) {
@@ -13449,8 +13480,8 @@ function paramProblems(contract, sources) {
13449
13480
  }
13450
13481
  function numberFix(value, integer = false) {
13451
13482
  const text3 = typeof value === "string" ? value.trim() : null;
13452
- const parsed = text3 !== null && text3 !== "" ? Number(text3) : NaN;
13453
- 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`;
13454
13485
  return `write ${integer ? "a whole number" : "a number"}, or leave the key out`;
13455
13486
  }
13456
13487
  function uidHint(value, why, elements) {
@@ -16466,7 +16497,7 @@ function measureProblems(name, row, model, handlers = {}) {
16466
16497
  const found = whereProblems(spec.where, EVENTS_SHAPE, at, who, {
16467
16498
  noColumn: (column) => `filters on "${column}", which is no column of the events; filter on at, action, handler, actor or note`
16468
16499
  });
16469
- 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))));
16470
16501
  } else problems.push(...whereProblems(spec.where, table, at, who, { platform: true }));
16471
16502
  }
16472
16503
  }
@@ -17722,6 +17753,7 @@ function validateContract(contract, catalog) {
17722
17753
  checkKind(app, problems);
17723
17754
  checkPreset(app, problems);
17724
17755
  if (kind === "dashboard") checkSources(doc, catalog, problems, skipped);
17756
+ checkSourceMetricIds(doc, catalog, problems, skipped);
17725
17757
  checkVocabulary(doc, kind, problems, renderer);
17726
17758
  checkPages(doc, kind, problems);
17727
17759
  if (kind === "dashboard") {
@@ -17744,22 +17776,22 @@ function validateContract(contract, catalog) {
17744
17776
  return { problems: dedupe(problems), skipped: dedupeSkipped(skipped), warnings: dedupe(warnings) };
17745
17777
  }
17746
17778
  function checkCustomDeclares(doc, problems) {
17747
- for (const problem5 of declaresProblems2(doc)) {
17779
+ for (const problem6 of declaresProblems2(doc)) {
17748
17780
  problems.push({
17749
- code: problem5.code,
17750
- pointer: problem5.pointer,
17751
- message: `${problem5.message}.`,
17781
+ code: problem6.code,
17782
+ pointer: problem6.pointer,
17783
+ message: `${problem6.message}.`,
17752
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).'
17753
17785
  });
17754
17786
  }
17755
17787
  }
17756
17788
  function checkTransforms(doc, problems) {
17757
- for (const problem5 of transformRowProblems2(obj(doc["data"]))) {
17758
- const row = problem5.code === "transform_row";
17789
+ for (const problem6 of transformRowProblems2(obj(doc["data"]))) {
17790
+ const row = problem6.code === "transform_row";
17759
17791
  problems.push({
17760
17792
  code: row ? "transform-row" : "transform-missing",
17761
- pointer: problem5.where,
17762
- message: `${problem5.message}.`,
17793
+ pointer: problem6.where,
17794
+ message: `${problem6.message}.`,
17763
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)."
17764
17796
  });
17765
17797
  }
@@ -18059,6 +18091,30 @@ function checkPreset(app, problems) {
18059
18091
  fix: `Name one of ${PRESETS2.join(", ")}, or drop the key and take the default.`
18060
18092
  });
18061
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
+ }
18062
18118
  function checkSources(doc, catalog, problems, skipped) {
18063
18119
  const widgets = obj(obj(doc["data"])["widgets"]);
18064
18120
  if (Object.keys(widgets).length === 0) return;
@@ -18300,9 +18356,9 @@ function calendarDateIn(filter) {
18300
18356
  }
18301
18357
  function epochOf(date) {
18302
18358
  const day = date.slice(0, 10);
18303
- const parsed = Date.parse(`${day}T00:00:00Z`);
18304
- if (Number.isNaN(parsed)) return void 0;
18305
- 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));
18306
18362
  }
18307
18363
  function addressOf(source, hasAlias, hasId) {
18308
18364
  const declared = source["address"];
@@ -19264,11 +19320,11 @@ function walk3(node, path3, visit) {
19264
19320
  function dedupe(problems) {
19265
19321
  const seen = /* @__PURE__ */ new Set();
19266
19322
  const out = [];
19267
- for (const problem5 of problems) {
19268
- 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}`;
19269
19325
  if (seen.has(key)) continue;
19270
19326
  seen.add(key);
19271
- out.push(problem5);
19327
+ out.push(problem6);
19272
19328
  }
19273
19329
  return out;
19274
19330
  }
@@ -20580,16 +20636,16 @@ async function readManifest(dir) {
20580
20636
  if (raw === void 0) {
20581
20637
  throw new Error(`no manifest.json in ${dir} \u2014 is this an app folder?`);
20582
20638
  }
20583
- let parsed;
20639
+ let parsed2;
20584
20640
  try {
20585
- parsed = JSON.parse(raw);
20641
+ parsed2 = JSON.parse(raw);
20586
20642
  } catch {
20587
20643
  throw new Error(`manifest.json in ${dir} is not valid JSON.`);
20588
20644
  }
20589
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
20645
+ if (typeof parsed2 !== "object" || parsed2 === null || Array.isArray(parsed2)) {
20590
20646
  throw new Error("manifest.json is not a JSON object.");
20591
20647
  }
20592
- const manifest = parsed;
20648
+ const manifest = parsed2;
20593
20649
  const id = manifest["id"];
20594
20650
  if (typeof id !== "string" || id === "") {
20595
20651
  throw new Error('manifest.json has no "id".');
@@ -20603,9 +20659,9 @@ async function readState(dir) {
20603
20659
  const raw = await readIfPresent(join5(dir, ".appspace.json"));
20604
20660
  if (raw === void 0) return {};
20605
20661
  try {
20606
- const parsed = JSON.parse(raw);
20607
- if (typeof parsed === "object" && parsed !== null) {
20608
- return parsed;
20662
+ const parsed2 = JSON.parse(raw);
20663
+ if (typeof parsed2 === "object" && parsed2 !== null) {
20664
+ return parsed2;
20609
20665
  }
20610
20666
  } catch {
20611
20667
  }
@@ -20722,8 +20778,8 @@ function summarise(contract) {
20722
20778
  }
20723
20779
  function describeFindings(contract) {
20724
20780
  let out = "";
20725
- for (const problem5 of contract.problems) {
20726
- 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}
20727
20783
  `;
20728
20784
  }
20729
20785
  for (const entry of contract.unresolved) {
@@ -21636,6 +21692,1082 @@ async function previewOne(document, file, opts, shared = {}) {
21636
21692
  import { readFile as readFile7 } from "fs/promises";
21637
21693
  import { join as join8 } from "path";
21638
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
21639
22771
  var KIT_ENV = "DR_APPSPACE_KIT";
21640
22772
  function kitForPush(opts, env = process.env) {
21641
22773
  const flag = (opts.kit ?? "").trim();
@@ -21804,12 +22936,39 @@ function describePostCommit(result) {
21804
22936
  }
21805
22937
  return lines.join("");
21806
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
+ }
21807
22963
  function registerPushCommand(parent) {
21808
22964
  withJsonOption(
21809
22965
  withServerOptions(
21810
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(
21811
22967
  "--kit <asset>",
21812
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)."
21813
22972
  )
21814
22973
  )
21815
22974
  ).action(async (dirArg, opts) => {
@@ -21851,6 +23010,9 @@ async function pushBody(dir, opts) {
21851
23010
  if (opts.force !== true && typeof state.revision === "number") {
21852
23011
  body["expected_revision"] = state.revision;
21853
23012
  }
23013
+ if (opts.metricBindings !== void 0 && Object.keys(opts.metricBindings).length > 0) {
23014
+ body["metric_bindings"] = opts.metricBindings;
23015
+ }
21854
23016
  return { appId, body, handlers };
21855
23017
  }
21856
23018
  function chartsWithoutEcharts(files) {
@@ -21872,6 +23034,9 @@ async function pushFolder(dir, opts) {
21872
23034
  const { appId, body, handlers } = await pushBody(dir, opts);
21873
23035
  const client = await connect(opts);
21874
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
+ }
21875
23040
  let result;
21876
23041
  try {
21877
23042
  result = await client.request(`/apps/${encodeURIComponent(appId)}/package`, {
@@ -21888,7 +23053,8 @@ async function pushFolder(dir, opts) {
21888
23053
  const outcome = {
21889
23054
  revision: result.revision,
21890
23055
  stage: result.stage,
21891
- 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 }
21892
23058
  };
21893
23059
  await recordRevision(dir, result.revision, result.stage, handlers, {
21894
23060
  id: appId,
@@ -21904,6 +23070,8 @@ async function pushFolder(dir, opts) {
21904
23070
  `
21905
23071
  );
21906
23072
  process.stdout.write(describePostCommit(result));
23073
+ for (const line2 of bindingWarnings(result.metric_bindings)) process.stdout.write(`warning: ${line2}
23074
+ `);
21907
23075
  process.stdout.write(describeContract(result.contract));
21908
23076
  process.stdout.write(describeIgnoredName(result.name_ignored));
21909
23077
  process.stdout.write(describeSeeding(result));
@@ -21983,9 +23151,9 @@ async function writeReport(dir, report) {
21983
23151
  }
21984
23152
  async function ownedFiles(dir) {
21985
23153
  try {
21986
- const parsed = JSON.parse(await readFile8(reportPath(dir), "utf8"));
21987
- if (!Array.isArray(parsed.files)) return /* @__PURE__ */ new Set();
21988
- 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));
21989
23157
  } catch {
21990
23158
  return /* @__PURE__ */ new Set();
21991
23159
  }
@@ -21995,8 +23163,8 @@ function digestOf(text3) {
21995
23163
  }
21996
23164
  async function recordedDigests(dir) {
21997
23165
  try {
21998
- const parsed = JSON.parse(await readFile8(reportPath(dir), "utf8"));
21999
- const held = parsed.digests;
23166
+ const parsed2 = JSON.parse(await readFile8(reportPath(dir), "utf8"));
23167
+ const held = parsed2.digests;
22000
23168
  if (typeof held !== "object" || held === null || Array.isArray(held)) return {};
22001
23169
  return Object.fromEntries(Object.entries(held).filter((entry) => typeof entry[1] === "string"));
22002
23170
  } catch {
@@ -22004,21 +23172,21 @@ async function recordedDigests(dir) {
22004
23172
  }
22005
23173
  }
22006
23174
  async function recordAsInput(dir, path3) {
22007
- let parsed;
23175
+ let parsed2;
22008
23176
  try {
22009
- parsed = JSON.parse(await readFile8(reportPath(dir), "utf8"));
23177
+ parsed2 = JSON.parse(await readFile8(reportPath(dir), "utf8"));
22010
23178
  } catch {
22011
23179
  return;
22012
23180
  }
22013
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return;
22014
- const list3 = (key) => Array.isArray(parsed[key]) ? parsed[key].map(String) : [];
22015
- parsed["files"] = list3("files").filter((one) => one !== path3);
22016
- parsed["inputs"] = [.../* @__PURE__ */ new Set([...list3("inputs"), path3])].sort();
22017
- 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"];
22018
23186
  if (typeof digests === "object" && digests !== null && !Array.isArray(digests)) {
22019
23187
  delete digests[path3];
22020
23188
  }
22021
- await writeFile4(reportPath(dir), JSON.stringify(parsed, null, 2) + "\n");
23189
+ await writeFile4(reportPath(dir), JSON.stringify(parsed2, null, 2) + "\n");
22022
23190
  }
22023
23191
 
22024
23192
  // src/commands/appspace/get.ts
@@ -22039,8 +23207,8 @@ function requireBundlePath(name) {
22039
23207
  async function unpushedInputs(dir, pkg) {
22040
23208
  let inputs = [];
22041
23209
  try {
22042
- const parsed = JSON.parse(await readFile9(reportPath(dir), "utf8"));
22043
- 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);
22044
23212
  } catch {
22045
23213
  return [];
22046
23214
  }
@@ -22305,7 +23473,7 @@ import { cp, mkdir as mkdir5, mkdtemp as mkdtemp2, rm as rm3, writeFile as write
22305
23473
  import { tmpdir as tmpdir3 } from "os";
22306
23474
  import { dirname as dirname4, join as join11, resolve as resolve5, sep as sep2 } from "path";
22307
23475
  var WHAT = "package";
22308
- async function checkPackage(dir, files, contract, opts, online) {
23476
+ async function checkPackage(dir, files, contract, opts, online, pendingMetricKeys = []) {
22309
23477
  if (!online) {
22310
23478
  return skip("no -s server to run the push's own checks against; the push is the first to run them");
22311
23479
  }
@@ -22317,13 +23485,13 @@ async function checkPackage(dir, files, contract, opts, online) {
22317
23485
  try {
22318
23486
  payload = await pushBody(scratch, { ...opts, contract: true });
22319
23487
  } catch (err) {
22320
- return skip(`the push body could not be built (${firstLine(err)})`);
23488
+ return skip(`the push body could not be built (${firstLine2(err)})`);
22321
23489
  }
22322
23490
  let client;
22323
23491
  try {
22324
23492
  client = await connect(opts);
22325
23493
  } catch (err) {
22326
- return skip(`could not reach the server (${firstLine(err)})`);
23494
+ return skip(`could not reach the server (${firstLine2(err)})`);
22327
23495
  }
22328
23496
  try {
22329
23497
  const path3 = `/apps/${encodeURIComponent(payload.appId)}/package/validate`;
@@ -22336,9 +23504,9 @@ async function checkPackage(dir, files, contract, opts, online) {
22336
23504
  body: JSON.stringify(payload.body)
22337
23505
  });
22338
23506
  } catch (err) {
22339
- return skip(`the server did not answer (${firstLine(err)})`);
23507
+ return skip(`the server did not answer (${firstLine2(err)})`);
22340
23508
  }
22341
- return interpret(res.status, parse(res.body), path3, contract);
23509
+ return setAsidePending(interpret(res.status, parse(res.body), path3, contract), pendingMetricKeys);
22342
23510
  } finally {
22343
23511
  client.cleanup();
22344
23512
  }
@@ -22346,11 +23514,28 @@ async function checkPackage(dir, files, contract, opts, online) {
22346
23514
  await rm3(scratch, { recursive: true, force: true });
22347
23515
  }
22348
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
+ }
22349
23534
  function interpret(status, body, path3, contract) {
22350
23535
  if (typeof body === "string" && /^<(!doctype html|html)/i.test(body.trimStart())) {
22351
23536
  return skip(`the request was intercepted before reaching App Space (HTTP ${String(status)}, an HTML page)`);
22352
23537
  }
22353
- if (status === 200 && isObj3(body)) {
23538
+ if (status === 200 && isObj4(body)) {
22354
23539
  const answer = body;
22355
23540
  if (answer.ok === true) return { problems: [], skipped: [] };
22356
23541
  if (answer.ok === false) {
@@ -22399,7 +23584,7 @@ function fromRefusal(status, detail, contract) {
22399
23584
  };
22400
23585
  });
22401
23586
  }
22402
- if (isObj3(detail) && detail["error"] === "stale_revision") {
23587
+ if (isObj4(detail) && detail["error"] === "stale_revision") {
22403
23588
  return [
22404
23589
  {
22405
23590
  code: "server:stale-revision",
@@ -22409,7 +23594,7 @@ function fromRefusal(status, detail, contract) {
22409
23594
  }
22410
23595
  ];
22411
23596
  }
22412
- if (isObj3(detail) && Array.isArray(detail["seed"])) {
23597
+ if (isObj4(detail) && Array.isArray(detail["seed"])) {
22413
23598
  return detail["seed"].map((line2) => ({
22414
23599
  code: "server:seed",
22415
23600
  pointer: "/",
@@ -22417,7 +23602,7 @@ function fromRefusal(status, detail, contract) {
22417
23602
  fix: "Fix the --seed file: every key a declared table, every row an object."
22418
23603
  }));
22419
23604
  }
22420
- if (isObj3(detail) && Array.isArray(detail["problems"])) {
23605
+ if (isObj4(detail) && Array.isArray(detail["problems"])) {
22421
23606
  const hint = typeof detail["hint"] === "string" ? detail["hint"] : null;
22422
23607
  return detail["problems"].map((line2) => fromLine(String(line2), refused, tables, hint));
22423
23608
  }
@@ -22468,11 +23653,11 @@ async function stage(dir, scratch, files) {
22468
23653
  }
22469
23654
  function tablesOf2(contract) {
22470
23655
  const out = /* @__PURE__ */ new Map();
22471
- const app = isObj3(contract) && isObj3(contract["app"]) ? contract["app"] : {};
22472
- const model = isObj3(app["data_model"]) ? app["data_model"] : {};
22473
- 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"] : {};
22474
23659
  for (const [name, table] of Object.entries(tables)) {
22475
- const columns = isObj3(table) && isObj3(table["columns"]) ? table["columns"] : {};
23660
+ const columns = isObj4(table) && isObj4(table["columns"]) ? table["columns"] : {};
22476
23661
  out.set(name, new Set(Object.keys(columns)));
22477
23662
  }
22478
23663
  return out;
@@ -22490,15 +23675,15 @@ function parse(bytes) {
22490
23675
  }
22491
23676
  }
22492
23677
  function detailOf(body) {
22493
- return isObj3(body) ? body["detail"] : void 0;
23678
+ return isObj4(body) ? body["detail"] : void 0;
22494
23679
  }
22495
23680
  function sentence(text3) {
22496
23681
  return text3.endsWith(".") ? text3 : `${text3}.`;
22497
23682
  }
22498
- function firstLine(err) {
23683
+ function firstLine2(err) {
22499
23684
  return (err instanceof Error ? err.message : String(err)).split("\n")[0] ?? "";
22500
23685
  }
22501
- function isObj3(value) {
23686
+ function isObj4(value) {
22502
23687
  return typeof value === "object" && value !== null && !Array.isArray(value);
22503
23688
  }
22504
23689
 
@@ -22540,8 +23725,8 @@ function stripPositional(contract, { keepTree = true, order: order2 } = {}) {
22540
23725
  else delete page.tree;
22541
23726
  }
22542
23727
  if (out.bindings !== void 0) out.bindings = renameDeep(out.bindings, rename2);
22543
- for (const problem5 of Array.isArray(out.problems) ? out.problems : []) {
22544
- 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);
22545
23730
  }
22546
23731
  return out;
22547
23732
  }
@@ -22946,7 +24131,7 @@ function specSource(data, card) {
22946
24131
  if (!isPlainObject11(row)) return;
22947
24132
  cite(row.source);
22948
24133
  if (typeof row.data === "string") readsOf(row.data);
22949
- 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);
22950
24135
  };
22951
24136
  visit(card);
22952
24137
  return { refs, sentence: parts.join("; ") };
@@ -22955,7 +24140,7 @@ function lookupOf(step) {
22955
24140
  if (!isPlainObject11(step)) return [];
22956
24141
  return Object.values(step).flatMap((value) => isPlainObject11(value) && typeof value.lookup === "string" ? [value.lookup] : []);
22957
24142
  }
22958
- function listOf(value) {
24143
+ function listOf2(value) {
22959
24144
  return Array.isArray(value) ? value : [];
22960
24145
  }
22961
24146
  function named6(value) {
@@ -23258,7 +24443,7 @@ function checkPage2(app, page, kit, runtime = null) {
23258
24443
  const missing = runtime === null ? [] : missingLibraries(page).map((one) => one.module);
23259
24444
  const library = runtime === null ? loadsModule(page.path, page.text, WIDGET_LIBRARY2) : !missing.includes("dr-app-runtime") && !missing.includes("dr-app-widgets");
23260
24445
  violations.push(...checkLibraryDraws(page, library, scripts, runtime === null ? null : missing));
23261
- 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 });
23262
24447
  if (widgets.length > 0 && !library && !/drInsights\.setData\s*\(/.test(scriptText)) {
23263
24448
  violations.push({
23264
24449
  rule: "R3.4",
@@ -24050,10 +25235,10 @@ var PAGE_PATH4 = /\.html?$/i;
24050
25235
  var AppLoadError2 = class extends Error {
24051
25236
  };
24052
25237
  function loadApp2(dir) {
24053
- const manifest = readJson2(dir, "manifest.json", true);
24054
- 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;
24055
25240
  const handlersSource = existsSync2(join13(dir, "handlers.json")) ? "handlers.json" : "manifest.json";
24056
- 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 ?? {};
24057
25242
  if (handlersIndex === null || typeof handlersIndex !== "object" || Array.isArray(handlersIndex)) {
24058
25243
  throw new AppLoadError2(`${handlersSource} must be an object mapping handler names to {http, file}`);
24059
25244
  }
@@ -24082,7 +25267,7 @@ function readHandlerSource2(dir, file) {
24082
25267
  if (!existsSync2(full) || !statSync2(full).isFile()) return "";
24083
25268
  return readFileSync4(full, "utf8");
24084
25269
  }
24085
- function readJson2(dir, path3, required) {
25270
+ function readJson3(dir, path3, required) {
24086
25271
  const full = join13(dir, path3);
24087
25272
  if (!existsSync2(full)) {
24088
25273
  if (required) throw new AppLoadError2(`${path3} is missing from the app version`);
@@ -25785,7 +26970,7 @@ function labelFor2(node, text3, tree) {
25785
26970
  }
25786
26971
  function textOf2(text3, node) {
25787
26972
  const next = text3.indexOf("<", node.end);
25788
- 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());
25789
26974
  }
25790
26975
  function slotsOf2(node, tree) {
25791
26976
  if (!(WIDGET_ATTR2 in node.attrs)) return {};
@@ -26252,7 +27437,7 @@ function specTransforms2(handlers, spec, problems) {
26252
27437
  out.push({ path: path3, text: text3 });
26253
27438
  }
26254
27439
  }
26255
- const unreadable = problems.some((problem5) => problem5.code === "transform_unreadable");
27440
+ const unreadable = problems.some((problem6) => problem6.code === "transform_unreadable");
26256
27441
  for (const key of Object.keys(spec.table).filter(isWidgetKey2).sort(natural5)) {
26257
27442
  const path3 = isPlainObject12(spec.table[key]) ? spec.table[key].transform : void 0;
26258
27443
  if (typeof path3 !== "string" || unreadable || carried !== null && carried.has(path3)) continue;
@@ -26548,7 +27733,7 @@ function natural5(a, b) {
26548
27733
  }
26549
27734
  var NAMED_ENTITIES2 = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
26550
27735
  var ENTITY2 = /&(#[0-9]{1,7}|#[xX][0-9a-fA-F]{1,6}|[a-zA-Z][a-zA-Z0-9]{1,31});/g;
26551
- function decodeEntities2(value) {
27736
+ function decodeEntities3(value) {
26552
27737
  const text3 = String(value);
26553
27738
  if (!text3.includes("&")) return text3;
26554
27739
  return text3.replace(ENTITY2, (whole, body) => {
@@ -26561,7 +27746,7 @@ function decodeEntities2(value) {
26561
27746
  function decodedTree2(html) {
26562
27747
  const tree = parseTree2(html);
26563
27748
  for (const node of tree.nodes) {
26564
- 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);
26565
27750
  }
26566
27751
  return tree;
26567
27752
  }
@@ -26598,9 +27783,9 @@ var MAX_HINT3 = 120;
26598
27783
  function validate3(instance, schema, options = {}) {
26599
27784
  const problems = [];
26600
27785
  check3(instance, schema, "", schema, problems, { quote: options.quote === true, inBranch: false });
26601
- return problems.map((problem5) => `${problem5.at}: ${problem5.why}`);
27786
+ return problems.map((problem6) => `${problem6.at}: ${problem6.why}`);
26602
27787
  }
26603
- function problem4(at, why, kind = "other") {
27788
+ function problem5(at, why, kind = "other") {
26604
27789
  return { at: at || "/", why, kind };
26605
27790
  }
26606
27791
  function describe4(node, ctx) {
@@ -26635,7 +27820,7 @@ function check3(instance, node, at, root, problems, ctx) {
26635
27820
  if (typeof node.$ref === "string") {
26636
27821
  const target = resolve10(node.$ref, root);
26637
27822
  if (target === null) {
26638
- 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"));
26639
27824
  return;
26640
27825
  }
26641
27826
  check3(instance, target, at, root, problems, ctx);
@@ -26643,29 +27828,29 @@ function check3(instance, node, at, root, problems, ctx) {
26643
27828
  if ("type" in node) {
26644
27829
  const names = Array.isArray(node.type) ? node.type : [node.type];
26645
27830
  if (!names.some((name) => isType3(instance, name))) {
26646
- 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"));
26647
27832
  return;
26648
27833
  }
26649
27834
  }
26650
- 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"));
26651
27836
  if (Array.isArray(node.enum) && !node.enum.some((one) => same3(instance, one))) {
26652
- 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"));
26653
27838
  }
26654
27839
  if (typeof node.minimum === "number" && isType3(instance, "number") && instance < node.minimum) {
26655
- problems.push(problem4(at, `below the minimum this contract sets${hint}`));
27840
+ problems.push(problem5(at, `below the minimum this contract sets${hint}`));
26656
27841
  }
26657
27842
  if (typeof node.pattern === "string" && typeof instance === "string" && !new RegExp(node.pattern, "u").test(instance)) {
26658
27843
  if (ctx.inBranch) {
26659
- 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}`));
26660
27845
  } else {
26661
- problems.push(problem4(at, "not in the form this contract requires"));
27846
+ problems.push(problem5(at, "not in the form this contract requires"));
26662
27847
  }
26663
27848
  }
26664
27849
  if (typeof node.minLength === "number" && typeof instance === "string" && [...instance].length < node.minLength) {
26665
- problems.push(problem4(at, `shorter than this contract allows${hint}`));
27850
+ problems.push(problem5(at, `shorter than this contract allows${hint}`));
26666
27851
  }
26667
27852
  if (typeof node.maxLength === "number" && typeof instance === "string" && [...instance].length > node.maxLength) {
26668
- problems.push(problem4(at, `longer than this contract allows${hint}`));
27853
+ problems.push(problem5(at, `longer than this contract allows${hint}`));
26669
27854
  }
26670
27855
  if (isObject5(instance)) checkObject3(instance, node, at, root, problems, ctx);
26671
27856
  if (Array.isArray(instance)) checkArray3(instance, node, at, root, problems, ctx, hint);
@@ -26701,16 +27886,16 @@ function checkBranches3(instance, branches, at, root, problems, ctx, wanted, sta
26701
27886
  if (kept.length > MAX_PER_PLACE3) {
26702
27887
  const more = kept.length - MAX_PER_PLACE3;
26703
27888
  kept = kept.slice(0, MAX_PER_PLACE3);
26704
- kept.push(problem4(at, `and ${more} more problems here`, "more"));
27889
+ kept.push(problem5(at, `and ${more} more problems here`, "more"));
26705
27890
  }
26706
27891
  for (const one of kept) problems.push(one);
26707
27892
  } else {
26708
- 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)})`));
26709
27894
  }
26710
27895
  } else if (matched === 0) {
26711
- 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"));
26712
27897
  } else if (wanted !== null && matched > wanted) {
26713
- 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"));
26714
27899
  }
26715
27900
  }
26716
27901
  function shapeMisses3(found, at) {
@@ -26752,7 +27937,7 @@ function checkObject3(instance, node, at, root, problems, ctx) {
26752
27937
  for (const name of Array.isArray(node.required) ? node.required : []) {
26753
27938
  if (Object.prototype.hasOwnProperty.call(instance, name)) continue;
26754
27939
  const wanted = Object.prototype.hasOwnProperty.call(properties, name) ? properties[name] : null;
26755
- 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"));
26756
27941
  }
26757
27942
  for (const [name, value] of Object.entries(instance)) {
26758
27943
  const where2 = `${at}/${escape3(name)}`;
@@ -26761,12 +27946,12 @@ function checkObject3(instance, node, at, root, problems, ctx) {
26761
27946
  continue;
26762
27947
  }
26763
27948
  const extra = node.additionalProperties ?? true;
26764
- 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"));
26765
27950
  else if (isObject5(extra)) check3(value, extra, node["x-tenant-keys"] === true && !ctx.quote ? `${at}/*` : where2, root, problems, ctx);
26766
27951
  }
26767
27952
  }
26768
27953
  function checkArray3(instance, node, at, root, problems, ctx, hint) {
26769
- 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}`));
26770
27955
  if (isObject5(node.items)) instance.forEach((value, index) => check3(value, node.items, `${at}/${index}`, root, problems, ctx));
26771
27956
  }
26772
27957
  function isType3(instance, name) {
@@ -27253,8 +28438,8 @@ function unproducedPayloadKeys2(contract, { handlerSource = () => void 0, module
27253
28438
  if (produced.includes(key)) continue;
27254
28439
  const there = produced.length === 0 ? "the controls there produce no payload keys" : `the controls there produce ${produced.join(", ")}`;
27255
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>).`;
27256
- const problem5 = `handler "${name}" reads payload "${key}", and nothing on ${either4(pages)} that calls it produces that key \u2014 ${there}.`;
27257
- 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}` });
27258
28443
  }
27259
28444
  }
27260
28445
  return out;
@@ -27288,9 +28473,9 @@ function undeclaredCustomCalls2(contract, { moduleSource = () => void 0 } = {})
27288
28473
  }
27289
28474
  function undeclaredCallMessage2(id, module, handler, calls, how) {
27290
28475
  const listed = calls.length === 0 ? "lists none" : `lists ${calls.join(", ")}`;
27291
- 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}.`;
27292
28477
  const fix = `Add "${handler}" to declares.calls \u2014 and what it sends to declares.produces \u2014 or stop calling it.`;
27293
- return { problem: problem5, fix, message: `${problem5} ${fix}` };
28478
+ return { problem: problem6, fix, message: `${problem6} ${fix}` };
27294
28479
  }
27295
28480
  function unreadUploadKeys2(contract, { handlerSource = () => void 0 } = {}) {
27296
28481
  const elements = isPlainObject13(contract?.elements) ? contract.elements : {};
@@ -27333,9 +28518,9 @@ function unreadUploadKeys2(contract, { handlerSource = () => void 0 } = {}) {
27333
28518
  }
27334
28519
  if (read) continue;
27335
28520
  const id = idOf3(element, uid);
27336
- 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.`;
27337
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.`;
27338
- 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}` });
27339
28524
  }
27340
28525
  }
27341
28526
  return out;
@@ -27351,9 +28536,9 @@ function stepsWithoutAction2(contract) {
27351
28536
  const id = idOf3(element, uid);
27352
28537
  const name = named8(step.id) ? step.id : String(index);
27353
28538
  const next = named8(steps[index + 1]?.id) ? steps[index + 1].id : "the next step";
27354
- 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.`;
27355
28540
  const fix = `Give it the handler that moves it to "${next}" (action, action_label), or make it the last step.`;
27356
- 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}` });
27357
28542
  });
27358
28543
  }
27359
28544
  return out;
@@ -27854,9 +29039,9 @@ function unboundControls(contract, files = {}) {
27854
29039
  const param = page.params.find((one) => one?.name === name) ?? {};
27855
29040
  const kind = named9(param.kind) ? param.kind : null;
27856
29041
  const opens3 = param.default === void 0 ? "no value at all" : `its default (${JSON.stringify(param.default)})`;
27857
- 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}.`;
27858
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.`;
27859
- 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}` });
27860
29045
  }
27861
29046
  }
27862
29047
  return out;
@@ -27910,9 +29095,9 @@ function unreadParams(contract, { moduleSource = () => void 0, handlerSource = (
27910
29095
  continue;
27911
29096
  }
27912
29097
  const there = named_.length === 0 ? "no component there reads a source" : `its sources are ${both2(named_.map((one) => `"${one}"`))}`;
27913
- 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.`;
27914
29099
  const fix = `Add "${name}" to the honors of the source it should move${named_.length === 1 ? ` ("${named_[0]}")` : ""}, or drop the param.`;
27915
- 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}` });
27916
29101
  }
27917
29102
  }
27918
29103
  return out;
@@ -29136,9 +30321,9 @@ function unreachablePages2(contract, files = {}) {
29136
30321
  else if (roles.length > 0) why = `it is for ${both3(quoted2(roles))}, and no page ${either5(quoted2(roles))} reaches links to it`;
29137
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`;
29138
30323
  else why = "the only pages that link to it are pages no role reaches";
29139
- 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.`;
29140
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.";
29141
- out.push({ page: page.path, problem: problem5, fix, message: `${problem5} ${fix}` });
30326
+ out.push({ page: page.path, problem: problem6, fix, message: `${problem6} ${fix}` });
29142
30327
  }
29143
30328
  return out;
29144
30329
  }
@@ -29284,9 +30469,9 @@ function unconfirmedStateChanges2(contract, files = {}) {
29284
30469
  named_.push(one.module ? `${one.what} (on ${one.page})` : `${one.what} "${one.id}" (on ${one.page})`);
29285
30470
  }
29286
30471
  const kind = handlers[handler]?.role === "decide" ? "decides a row" : "changes a record's state";
29287
- 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.`;
29288
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.';
29289
- 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}` });
29290
30475
  }
29291
30476
  return out;
29292
30477
  }
@@ -29364,18 +30549,18 @@ function unreadReasons2(contract, { handlerSource = () => void 0, moduleSource,
29364
30549
  for (const [key, pages] of [...sent.entries()].sort()) {
29365
30550
  if (read.has(key)) continue;
29366
30551
  const where2 = pages.size === 0 ? "the page" : either5([...pages].sort());
29367
- 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.`;
29368
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.`;
29369
- 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}` });
29370
30555
  }
29371
30556
  const lines = pyLines2(source);
29372
30557
  if (!changesState2(name, row, source) || !REJECTS2.test(lines.join("\n"))) continue;
29373
30558
  for (const key of REASON_KEYS2) {
29374
30559
  if (!reads.optional.includes(key) || reads.required.includes(key)) continue;
29375
30560
  if (requiresReason2(lines, key, reasonVars2(lines, key))) continue;
29376
- 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.`;
29377
30562
  const fix = `Refuse the reject path without one: if decision == "rejected" and not ${key}: return {"error": "Give a reason to reject."}.`;
29378
- 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}` });
29379
30564
  }
29380
30565
  }
29381
30566
  return out;
@@ -29412,9 +30597,9 @@ function unliftableRefusals2(contract, { handlerSource = () => void 0 } = {}) {
29412
30597
  }
29413
30598
  if (!refuses) continue;
29414
30599
  const shown2 = line2.trim().replace(/:$/, "");
29415
- 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.`;
29416
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.`;
29417
- 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}` });
29418
30603
  }
29419
30604
  }
29420
30605
  }
@@ -29430,9 +30615,9 @@ function rolesLandingNowhere2(contract) {
29430
30615
  const exists = pagesOf2(contract).some((page) => page.path === landing);
29431
30616
  if (exists && opens2(contract, model, role, landing)) continue;
29432
30617
  const stated2 = model.home.get(role);
29433
- 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.`;
29434
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.`;
29435
- out.push({ role, page: landing, problem: problem5, fix, message: `${problem5} ${fix}` });
30620
+ out.push({ role, page: landing, problem: problem6, fix, message: `${problem6} ${fix}` });
29436
30621
  }
29437
30622
  return out;
29438
30623
  }
@@ -29471,8 +30656,8 @@ function openToEveryRole2(contract) {
29471
30656
  const out = [];
29472
30657
  const say = (kind, names, what, fix) => {
29473
30658
  if (names.length === 0) return;
29474
- 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"}.`;
29475
- 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}` });
29476
30661
  };
29477
30662
  say(
29478
30663
  "page",
@@ -29859,7 +31044,7 @@ async function runCompile(dir, opts) {
29859
31044
  for (const fill of layoutFills(contract)) report.compose.push(fill);
29860
31045
  }
29861
31046
  const built = await compileFolder(dir, kind, contract, kit, loaded.overlay);
29862
- const refused = built.problems.filter((problem5) => REFUSALS.has(problem5.code));
31047
+ const refused = built.problems.filter((problem6) => REFUSALS.has(problem6.code));
29863
31048
  if (Object.keys(built.files).length === 0 || refused.length > 0) {
29864
31049
  const lines = (refused.length > 0 ? refused : built.problems).map(asProblem);
29865
31050
  report.validate.problems = lines;
@@ -29869,6 +31054,30 @@ async function runCompile(dir, opts) {
29869
31054
  else process.stdout.write(problemHeading(lines.length) + formatProblems(lines) + formatSkipped(skipped));
29870
31055
  return 1;
29871
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
+ }
29872
31081
  const paths = Object.keys(built.files).sort();
29873
31082
  const escaping = paths.filter((path3) => !insideFolder(dir, path3));
29874
31083
  if (escaping.length > 0) {
@@ -29915,12 +31124,12 @@ async function runCompile(dir, opts) {
29915
31124
  report.digests = Object.fromEntries(plan.owned.map((path3) => [path3, digestOf(built.files[path3] ?? "")]));
29916
31125
  report.inputs = plan.inputs;
29917
31126
  }
29918
- 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);
29919
31128
  } finally {
29920
31129
  if (check5) await rm4(target, { recursive: true, force: true });
29921
31130
  }
29922
31131
  }
29923
- 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) {
29924
31133
  const composed = kind === "dashboard" && !isRuntimeDashboard(contract) ? composeOf(contract) : built.contract ?? contract;
29925
31134
  const extracted = await extractContract(target);
29926
31135
  const diffs = roundTripDiffs(composed, extracted, built.problems, { order: built.order });
@@ -29957,21 +31166,29 @@ async function afterWrite(dir, target, kind, contract, built, report, kit, skipp
29957
31166
  return 4;
29958
31167
  }
29959
31168
  if (target !== dir) {
29960
- 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
+ );
29961
31177
  const notChecked3 = [...skipped, ...server.skipped];
29962
31178
  if ((opts.expect ?? []).length > 0) notChecked3.push({ what: "headline", why: "--check runs no load" });
29963
31179
  report.validate.problems = server.problems;
29964
31180
  report.validate.checks_skipped = notChecked3;
29965
31181
  if (server.problems.length > 0) report.failure = "push-would-be-refused";
29966
31182
  await writeReport(dir, report);
31183
+ const planned = metricsPlan === null ? "" : renderPlan(metricsPlan);
29967
31184
  if (opts.json === true) printJson(report);
29968
31185
  else if (server.problems.length > 0) {
29969
31186
  process.stdout.write(
29970
- problemHeading(server.problems.length) + formatProblems(server.problems) + formatSkipped(notChecked3)
31187
+ planned + problemHeading(server.problems.length) + formatProblems(server.problems) + formatSkipped(notChecked3)
29971
31188
  );
29972
31189
  } else {
29973
31190
  process.stdout.write(
29974
- 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)
29975
31192
  );
29976
31193
  }
29977
31194
  return server.problems.length > 0 ? 1 : 0;
@@ -29980,9 +31197,31 @@ async function afterWrite(dir, target, kind, contract, built, report, kit, skipp
29980
31197
  await liveness(dir, kind, report, extracted, opts);
29981
31198
  return finish(dir, report, skipped, opts);
29982
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
+ }
29983
31222
  let pushed = null;
29984
31223
  try {
29985
- pushed = await pushFolder(dir, { ...opts, force: opts.force });
31224
+ pushed = await pushFolder(dir, { ...opts, force: opts.force, ...metricBindings === void 0 ? {} : { metricBindings } });
29986
31225
  } catch (err) {
29987
31226
  report.push = { pushed: false, stage: null, revision: null, contract_source: null };
29988
31227
  report.failure = isStaleRevision(err) ? "stale-revision" : isLostRace(err) ? "concurrent-publish" : "push-rejected";
@@ -30006,7 +31245,11 @@ async function afterWrite(dir, target, kind, contract, built, report, kit, skipp
30006
31245
  contract_source: "cli",
30007
31246
  ...typeof pushed?.code_version === "number" ? { code_version: pushed.code_version } : {}
30008
31247
  };
31248
+ if (report.metrics !== void 0 && report.metrics !== null && pushed?.metric_bindings !== void 0) {
31249
+ report.metrics["push"] = pushed.metric_bindings;
31250
+ }
30009
31251
  await liveness(dir, kind, report, extracted, opts);
31252
+ await metricsLiveness(report, extracted, opts);
30010
31253
  return finish(dir, report, skipped, opts);
30011
31254
  }
30012
31255
  async function finish(dir, report, skipped, opts) {
@@ -30207,25 +31450,25 @@ async function writePlan(dir, kind, contract, files, paths, force) {
30207
31450
  }
30208
31451
  function ownedPaths(contract) {
30209
31452
  const out = customHandlerPaths(contract);
30210
- const data = obj2(obj2(contract)["data"]);
30211
- 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"]))]) {
30212
31455
  const transform = isPlain(row) ? row["transform"] : void 0;
30213
31456
  if (typeof transform === "string" && TRANSFORM_PATH2.test(transform)) out.add(transform);
30214
31457
  }
30215
- for (const element of Object.values(obj2(obj2(contract)["elements"]))) {
31458
+ for (const element of Object.values(obj3(obj3(contract)["elements"]))) {
30216
31459
  if (isPlain(element) && element["type"] === "custom") {
30217
- const module = obj2(element["props"])["module"];
31460
+ const module = obj3(element["props"])["module"];
30218
31461
  if (typeof module === "string" && MODULE_RE.test(module)) out.add(`ui/${module}`);
30219
31462
  }
30220
31463
  }
30221
- const pages = obj2(contract)["pages"];
31464
+ const pages = obj3(contract)["pages"];
30222
31465
  for (const page of Array.isArray(pages) ? pages : []) {
30223
31466
  if (isPlain(page) && page["owned"] === true && typeof page["path"] === "string") out.add(page["path"]);
30224
31467
  }
30225
31468
  return out;
30226
31469
  }
30227
31470
  function customHandlerPaths(contract) {
30228
- const handlers = obj2(obj2(obj2(obj2(contract)["data"])["app"])["handlers"]);
31471
+ const handlers = obj3(obj3(obj3(obj3(contract)["data"])["app"])["handlers"]);
30229
31472
  const out = /* @__PURE__ */ new Set();
30230
31473
  for (const [name, row] of Object.entries(handlers)) {
30231
31474
  if (isPlain(row) && row["role"] === "custom") out.add(`handlers/${name}.py`);
@@ -30268,7 +31511,7 @@ function roundTripDiffs(input, extracted, compilerProblems = [], { order: order2
30268
31511
  const read = order2 === void 0 ? {} : { order: "keys" };
30269
31512
  const left = stripPositional(normaliseApp(input), stated2);
30270
31513
  const right = stripPositional(normaliseApp(withoutUnstatedChrome(input, extracted)), read);
30271
- const declared = compilerProblems.some((problem5) => problem5.code === DECLARED_LOSS);
31514
+ const declared = compilerProblems.some((problem6) => problem6.code === DECLARED_LOSS);
30272
31515
  const diffs = [];
30273
31516
  for (const section of ["app", "pages", "elements", "data"]) {
30274
31517
  if (!(section in left)) continue;
@@ -30343,7 +31586,7 @@ function normaliseApp(contract) {
30343
31586
  }
30344
31587
  const model = record2["data_model"];
30345
31588
  if (isPlain(model)) {
30346
- for (const table of Object.values(obj2(model["tables"]))) {
31589
+ for (const table of Object.values(obj3(model["tables"]))) {
30347
31590
  if (!isPlain(table)) continue;
30348
31591
  const columns = table["columns"];
30349
31592
  if (!isPlain(columns)) continue;
@@ -30363,7 +31606,7 @@ function normaliseApp(contract) {
30363
31606
  }
30364
31607
  }
30365
31608
  }
30366
- const roles = obj2(out["backend"])["roles"];
31609
+ const roles = obj3(out["backend"])["roles"];
30367
31610
  if (Array.isArray(roles)) {
30368
31611
  for (const role of roles) {
30369
31612
  if (isPlain(role) && Array.isArray(role["sees"]) && role["sees"].length === 0) delete role["sees"];
@@ -30371,7 +31614,7 @@ function normaliseApp(contract) {
30371
31614
  }
30372
31615
  return out;
30373
31616
  }
30374
- function obj2(value) {
31617
+ function obj3(value) {
30375
31618
  return isPlain(value) ? value : {};
30376
31619
  }
30377
31620
  function withoutLibrary(section) {
@@ -30715,7 +31958,7 @@ async function recordPulled(dir, contractWritten) {
30715
31958
  } catch {
30716
31959
  return forget();
30717
31960
  }
30718
- 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))) {
30719
31962
  return forget();
30720
31963
  }
30721
31964
  const custom = ownedPaths(contract);
@@ -30821,21 +32064,21 @@ async function pushedState(dir) {
30821
32064
  const raw = await readIfPresent(join15(dir, ".appspace.json"));
30822
32065
  if (raw === void 0) return { stage: null, revision: null };
30823
32066
  try {
30824
- const parsed = JSON.parse(raw);
32067
+ const parsed2 = JSON.parse(raw);
30825
32068
  return {
30826
- stage: typeof parsed.stage === "string" ? parsed.stage : null,
30827
- 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
30828
32071
  };
30829
32072
  } catch {
30830
32073
  return { stage: null, revision: null };
30831
32074
  }
30832
32075
  }
30833
- function asProblem(problem5) {
32076
+ function asProblem(problem6) {
30834
32077
  return {
30835
- code: problem5.code,
30836
- pointer: problem5.pointer ?? problem5.where ?? "/",
30837
- message: problem5.message.endsWith(".") ? problem5.message : `${problem5.message}.`,
30838
- 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."
30839
32082
  };
30840
32083
  }
30841
32084
  function livenessLine(report) {
@@ -30873,6 +32116,7 @@ function describe5(report, skipped) {
30873
32116
  `;
30874
32117
  out += warningLines(report);
30875
32118
  out += livenessLine(report);
32119
+ out += metricsLines(report);
30876
32120
  if (report.push !== null && report.push.pushed) {
30877
32121
  out += `pushed \u2014 ${report.push.stage ?? "draft"}, revision ${String(report.push.revision ?? 0)}
30878
32122
  `;
@@ -30880,10 +32124,149 @@ function describe5(report, skipped) {
30880
32124
  out += formatSkipped(skipped);
30881
32125
  return out;
30882
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
+ }
30883
32266
  function registerCompileCommand(parent) {
30884
32267
  withJsonOption(
30885
32268
  withServerOptions(
30886
- 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(
30887
32270
  "after",
30888
32271
  [
30889
32272
  "",
@@ -30905,6 +32288,7 @@ function registerCompileCommand(parent) {
30905
32288
  'A declared Datarails metric (data.metrics, read by a source as from.financeos.metric "@<key>") is refused with:',
30906
32289
  ...Object.entries(METRIC_REFUSALS).map(([code, what]) => ` ${code.padEnd(22)}${what}`),
30907
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(),
30908
32292
  "",
30909
32293
  "A transform (a dashboard dataset series row's transforms/<name>.py, or an app source's) is refused with:",
30910
32294
  ...Object.entries(TRANSFORM_REFUSALS).map(([code, what]) => ` ${code.padEnd(22)}${what}`)
@@ -30936,7 +32320,7 @@ function registerShipCommand(parent) {
30936
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(
30937
32321
  "--onto-live",
30938
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."
30939
- )
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"))
30940
32324
  )
30941
32325
  )
30942
32326
  ).action(async (dirArg, opts) => {
@@ -31115,7 +32499,7 @@ async function runEject(target, opts) {
31115
32499
  }
31116
32500
  }
31117
32501
  async function ejectHandler(dir, contract, name) {
31118
- const handlers = obj3(obj3(obj3(contract["data"])["app"])["handlers"]);
32502
+ const handlers = obj4(obj4(obj4(contract["data"])["app"])["handlers"]);
31119
32503
  const row = handlers[name];
31120
32504
  if (!isPlain2(row)) {
31121
32505
  const known = Object.keys(handlers).sort();
@@ -31173,7 +32557,7 @@ function literalEnd2(text3, open2) {
31173
32557
  return -1;
31174
32558
  }
31175
32559
  async function ejectSource(dir, contract, name) {
31176
- const sources = obj3(obj3(contract["data"])["sources"]);
32560
+ const sources = obj4(obj4(contract["data"])["sources"]);
31177
32561
  if (!isPlain2(sources[name])) {
31178
32562
  const known = Object.keys(sources).sort();
31179
32563
  throw new Refused(
@@ -31183,7 +32567,7 @@ async function ejectSource(dir, contract, name) {
31183
32567
  const path3 = `handlers/${name}.py`;
31184
32568
  if (!insideFolder(dir, path3)) throw new Refused(`${JSON.stringify(path3)} is not a path inside ${dir}.`);
31185
32569
  const dependents = Object.keys(sources).filter((other) => {
31186
- const from = obj3(sources[other])["from"];
32570
+ const from = obj4(sources[other])["from"];
31187
32571
  return Array.isArray(from) && from.includes(name);
31188
32572
  }).sort();
31189
32573
  if (dependents.length > 0) {
@@ -31209,9 +32593,9 @@ async function ejectSource(dir, contract, name) {
31209
32593
  throw new Refused(`${path3} as compiled carries no SOURCES = {\u2026} table, so it is not a generated source handler.`);
31210
32594
  }
31211
32595
  await mayPlace(dir, path3, body, [compiled]);
31212
- const data = obj3(contract["data"]);
31213
- const app = obj3(data["app"]);
31214
- const handlers = obj3(app["handlers"]);
32596
+ const data = obj4(contract["data"]);
32597
+ const app = obj4(data["app"]);
32598
+ const handlers = obj4(app["handlers"]);
31215
32599
  handlers[name] = { role: "custom" };
31216
32600
  app["handlers"] = handlers;
31217
32601
  data["app"] = app;
@@ -31239,8 +32623,8 @@ async function ejectPage(dir, contract, path3) {
31239
32623
  throw new Refused(`${path3} as compiled has no <head> to carry ${OWNED_META}.`);
31240
32624
  }
31241
32625
  await mayPlace(dir, path3, marked2, [compiled]);
31242
- const elements = obj3(contract["elements"]);
31243
- 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));
31244
32628
  for (const uid of gone) delete elements[uid];
31245
32629
  for (const element of Object.values(elements)) {
31246
32630
  if (!isPlain2(element) || !Array.isArray(element["children"])) continue;
@@ -31251,11 +32635,11 @@ async function ejectPage(dir, contract, path3) {
31251
32635
  kept["owned"] = true;
31252
32636
  if (gate !== null && kept["roles"] === void 0) kept["role"] = gate;
31253
32637
  pages[index] = kept;
31254
- const app = obj3(obj3(contract["data"])["app"]);
32638
+ const app = obj4(obj4(contract["data"])["app"]);
31255
32639
  if (Array.isArray(app["screens"])) {
31256
32640
  app["screens"] = app["screens"].filter((screen) => !(isPlain2(screen) && screen["page"] === path3));
31257
32641
  }
31258
- const sources = Object.keys(obj3(obj3(contract["data"])["sources"]));
32642
+ const sources = Object.keys(obj4(obj4(contract["data"])["sources"]));
31259
32643
  const served = sources.filter((name) => files[`handlers/${name}.py`] !== void 0);
31260
32644
  const wrote = await commit(dir, contract, path3, marked2, (after) => {
31261
32645
  const lost = served.filter((name) => after[`handlers/${name}.py`] === void 0).sort();
@@ -31296,19 +32680,19 @@ ${OWNED_META}${html.slice(at2)}`;
31296
32680
  ${OWNED_META}${html.slice(at)}`;
31297
32681
  }
31298
32682
  async function ejectElement(dir, contract, uid) {
31299
- const elements = obj3(contract["elements"]);
32683
+ const elements = obj4(contract["elements"]);
31300
32684
  const element = elements[uid];
31301
32685
  if (!isPlain2(element)) {
31302
32686
  throw new Refused(`the contract has no element ${JSON.stringify(uid)}; an element's uid is its key in contract.elements.`);
31303
32687
  }
31304
32688
  const type = String(element["type"] ?? "");
31305
- 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")}.`);
31306
32690
  if (!EJECTABLE.has(type)) {
31307
32691
  throw new Refused(
31308
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>")}.`
31309
32693
  );
31310
32694
  }
31311
- const props = obj3(element["props"]);
32695
+ const props = obj4(element["props"]);
31312
32696
  const name = freeName(moduleName(props["id"], type, uid), ownedPaths(contract));
31313
32697
  const module = `components/${name}.js`;
31314
32698
  const path3 = `ui/${module}`;
@@ -31389,9 +32773,9 @@ ${lines}
31389
32773
  return built.files;
31390
32774
  }
31391
32775
  function refusedLines(built) {
31392
- const refused = built.problems.filter((problem5) => REFUSALS.has(problem5.code));
32776
+ const refused = built.problems.filter((problem6) => REFUSALS.has(problem6.code));
31393
32777
  if (Object.keys(built.files).length > 0 && refused.length === 0) return null;
31394
- 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");
31395
32779
  }
31396
32780
  async function mayPlace(dir, path3, text3, same5) {
31397
32781
  const existing = await readIfPresent(join16(dir, path3));
@@ -31436,7 +32820,7 @@ ${lines}
31436
32820
  function isPlain2(value) {
31437
32821
  return typeof value === "object" && value !== null && !Array.isArray(value);
31438
32822
  }
31439
- function obj3(value) {
32823
+ function obj4(value) {
31440
32824
  return isPlain2(value) ? value : {};
31441
32825
  }
31442
32826
  function registerEjectCommand(parent) {
@@ -31668,13 +33052,13 @@ function parseFileSpec(spec) {
31668
33052
  return { field, path: path3 };
31669
33053
  }
31670
33054
  function parseFileSpecs(specs) {
31671
- const parsed = specs.map(parseFileSpec);
33055
+ const parsed2 = specs.map(parseFileSpec);
31672
33056
  const seen = /* @__PURE__ */ new Set();
31673
- for (const one of parsed) {
33057
+ for (const one of parsed2) {
31674
33058
  if (seen.has(one.field)) throw new Error(`--file ${one.field} is given twice; give each field one file.`);
31675
33059
  seen.add(one.field);
31676
33060
  }
31677
- return parsed;
33061
+ return parsed2;
31678
33062
  }
31679
33063
  var MIME_TYPES = {
31680
33064
  ".pdf": "application/pdf",
@@ -32865,15 +34249,15 @@ function answerOf(res) {
32865
34249
  if (res.body.length === 0 || res.body.length > ROWS_BODY_LIMIT) return {};
32866
34250
  const type = Object.entries(res.headers).find(([key]) => key.toLowerCase() === "content-type")?.[1];
32867
34251
  if (type !== void 0 && !/json/i.test(type)) return {};
32868
- let parsed;
34252
+ let parsed2;
32869
34253
  try {
32870
- parsed = JSON.parse(res.body.toString("utf8"));
34254
+ parsed2 = JSON.parse(res.body.toString("utf8"));
32871
34255
  } catch {
32872
34256
  return {};
32873
34257
  }
32874
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
34258
+ if (typeof parsed2 !== "object" || parsed2 === null || Array.isArray(parsed2)) return {};
32875
34259
  const out = { object: true };
32876
- const { rows: rows2, error, needs } = parsed;
34260
+ const { rows: rows2, error, needs } = parsed2;
32877
34261
  if (needs && typeof needs === "object" && !Array.isArray(needs)) {
32878
34262
  const reason = needs.reason;
32879
34263
  if (reason === true || reason && typeof reason === "object" && !Array.isArray(reason)) {
@@ -32954,8 +34338,8 @@ function harvestNodes(document) {
32954
34338
  const filter = el.getAttribute(FILTER);
32955
34339
  if (filter !== null && filter !== "") props.filter = readFilter(filter);
32956
34340
  const args = el.getAttribute(ARGS);
32957
- const parsed = args !== null && args !== "" ? readArgs(args) : null;
32958
- if (parsed !== null) props.args = parsed;
34341
+ const parsed2 = args !== null && args !== "" ? readArgs(args) : null;
34342
+ if (parsed2 !== null) props.args = parsed2;
32959
34343
  const typed = el.getAttribute(PROPS);
32960
34344
  const typedProps = typed !== null && typed !== "" ? readProps(typed) : null;
32961
34345
  if (typedProps !== null) props.props = typedProps;
@@ -33251,8 +34635,8 @@ function createCanvas2dStub(canvas) {
33251
34635
  for (const name of CANVAS_2D_VOID_METHODS) target[name] = () => void 0;
33252
34636
  const spares = /* @__PURE__ */ new Map();
33253
34637
  return new Proxy(target, {
33254
- get(obj4, prop, receiver) {
33255
- 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);
33256
34640
  if (prop === "then") return void 0;
33257
34641
  let spare = spares.get(prop);
33258
34642
  if (spare === void 0) {
@@ -33536,13 +34920,13 @@ async function runUiSmoke(opts) {
33536
34920
  const graceUntil = Date.now() + graceMs;
33537
34921
  while (Date.now() < deadline) {
33538
34922
  if (drivePrompt?.()) lastActivity = Date.now();
33539
- const parsed = dom.window.document.readyState === "complete";
34923
+ const parsed2 = dom.window.document.readyState === "complete";
33540
34924
  const started = calls.length > since || inFlight > 0;
33541
34925
  if (!started && Date.now() < graceUntil) {
33542
34926
  await sleep(25);
33543
34927
  continue;
33544
34928
  }
33545
- if (parsed && inFlight === 0 && Date.now() - lastActivity >= quietMs) break;
34929
+ if (parsed2 && inFlight === 0 && Date.now() - lastActivity >= quietMs) break;
33546
34930
  await sleep(25);
33547
34931
  }
33548
34932
  }
@@ -34817,9 +36201,9 @@ function unboundControls2(contract, files = {}) {
34817
36201
  const param = page.params.find((one) => one?.name === name) ?? {};
34818
36202
  const kind = named12(param.kind) ? param.kind : null;
34819
36203
  const opens3 = param.default === void 0 ? "no value at all" : `its default (${JSON.stringify(param.default)})`;
34820
- 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}.`;
34821
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.`;
34822
- 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}` });
34823
36207
  }
34824
36208
  }
34825
36209
  return out;
@@ -34873,9 +36257,9 @@ function unreadParams2(contract, { moduleSource = () => void 0, handlerSource =
34873
36257
  continue;
34874
36258
  }
34875
36259
  const there = named_.length === 0 ? "no component there reads a source" : `its sources are ${both4(named_.map((one) => `"${one}"`))}`;
34876
- 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.`;
34877
36261
  const fix = `Add "${name}" to the honors of the source it should move${named_.length === 1 ? ` ("${named_[0]}")` : ""}, or drop the param.`;
34878
- 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}` });
34879
36263
  }
34880
36264
  }
34881
36265
  return out;
@@ -34917,9 +36301,9 @@ function pushedContract(bundle, pages) {
34917
36301
  const found = contractBlockText(html);
34918
36302
  if (found === null) continue;
34919
36303
  try {
34920
- const parsed = JSON.parse(found);
34921
- if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
34922
- return parsed;
36304
+ const parsed2 = JSON.parse(found);
36305
+ if (typeof parsed2 === "object" && parsed2 !== null && !Array.isArray(parsed2)) {
36306
+ return parsed2;
34923
36307
  }
34924
36308
  } catch {
34925
36309
  }
@@ -35852,11 +37236,11 @@ function customDeclaresFindings(contract, bundle, runs) {
35852
37236
  if (contract === null || !isAppContract3(contract)) return [];
35853
37237
  const out = [];
35854
37238
  const said2 = /* @__PURE__ */ new Set();
35855
- const undeclared = (uid, page, module, handler, problem5, fix) => {
37239
+ const undeclared = (uid, page, module, handler, problem6, fix) => {
35856
37240
  const key = `${uid}|${handler}`;
35857
37241
  if (said2.has(key)) return;
35858
37242
  said2.add(key);
35859
- 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 });
35860
37244
  };
35861
37245
  for (const found of DECLARES.undeclaredCustomCalls(contract, pushedFiles(bundle, {}))) {
35862
37246
  undeclared(found.uid, found.page, found.module, found.handler, found.problem, found.fix);
@@ -35869,8 +37253,8 @@ function customDeclaresFindings(contract, bundle, runs) {
35869
37253
  if (element?.type !== "custom" || declares === null || typeof module !== "string") continue;
35870
37254
  if (declares.calls.includes(entry.handler)) continue;
35871
37255
  const id = typeof element.props?.["id"] === "string" ? element.props["id"] : entry.uid;
35872
- const { problem: problem5, fix } = DECLARES.undeclaredCallMessage(id, module, entry.handler, declares.calls, "observed at load");
35873
- 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);
35874
37258
  }
35875
37259
  }
35876
37260
  for (const run of runs) {
@@ -36200,13 +37584,13 @@ async function loadFixtures(dir, explicit) {
36200
37584
  if (explicit !== void 0) throw new Error(`no fixtures file at ${path3}.`);
36201
37585
  return { fixtures: { cases: [], ui: { entry: "index.html" } } };
36202
37586
  }
36203
- let parsed;
37587
+ let parsed2;
36204
37588
  try {
36205
- parsed = JSON.parse(raw);
37589
+ parsed2 = JSON.parse(raw);
36206
37590
  } catch {
36207
37591
  throw new FixtureError(`${path3} is not valid JSON.`);
36208
37592
  }
36209
- return { fixtures: parseFixtures(parsed, path3), source: path3 };
37593
+ return { fixtures: parseFixtures(parsed2, path3), source: path3 };
36210
37594
  }
36211
37595
  async function runHandlerCases(cases, dir, appId, client, roleOf = () => void 0) {
36212
37596
  const code = /* @__PURE__ */ new Map();
@@ -36534,7 +37918,7 @@ ${attached}`;
36534
37918
  }
36535
37919
  out += ` FAIL ${head}
36536
37920
  ${attached}`;
36537
- for (const problem5 of outcome.problems) out += ` ${problem5}
37921
+ for (const problem6 of outcome.problems) out += ` ${problem6}
36538
37922
  `;
36539
37923
  }
36540
37924
  return out;
@@ -36599,7 +37983,7 @@ function rederiveComponentBindings(contract) {
36599
37983
  model
36600
37984
  });
36601
37985
  contract.problems = [
36602
- ...contract.problems.filter((problem5) => !COMPONENT_PROBLEM_CODES.has(problem5.code)),
37986
+ ...contract.problems.filter((problem6) => !COMPONENT_PROBLEM_CODES.has(problem6.code)),
36603
37987
  ...problems
36604
37988
  ].sort(byProblem4);
36605
37989
  }
@@ -36655,7 +38039,7 @@ function mergeDomComponents(contract, components, page) {
36655
38039
  contract.elements[uid] = element;
36656
38040
  }
36657
38041
  contract.problems = contract.problems.filter(
36658
- (problem5) => !(problem5.code === "duplicate_dom_id" && problem5.where.startsWith(`${page}@dom#`))
38042
+ (problem6) => !(problem6.code === "duplicate_dom_id" && problem6.where.startsWith(`${page}@dom#`))
36659
38043
  );
36660
38044
  for (const { id, uid } of duplicates) {
36661
38045
  contract.problems.push({
@@ -36768,15 +38152,15 @@ function untouchedHandlers(declared, cases, interactions, runs = []) {
36768
38152
  return declared.filter((name) => !touched.has(name) && !calls.some((call) => callsHandler(call.url, name))).sort();
36769
38153
  }
36770
38154
  async function readSeed(dir) {
36771
- let parsed;
38155
+ let parsed2;
36772
38156
  try {
36773
- parsed = JSON.parse(await readFile13(join18(dir, "seed.json"), "utf8"));
38157
+ parsed2 = JSON.parse(await readFile13(join18(dir, "seed.json"), "utf8"));
36774
38158
  } catch {
36775
38159
  return null;
36776
38160
  }
36777
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
38161
+ if (typeof parsed2 !== "object" || parsed2 === null || Array.isArray(parsed2)) return null;
36778
38162
  const seed = {};
36779
- for (const [table, rows2] of Object.entries(parsed)) {
38163
+ for (const [table, rows2] of Object.entries(parsed2)) {
36780
38164
  if (!Array.isArray(rows2)) continue;
36781
38165
  seed[table] = rows2.filter(
36782
38166
  (row) => typeof row === "object" && row !== null && !Array.isArray(row)
@@ -37270,11 +38654,16 @@ function describeTryRole(one) {
37270
38654
  function rolesAllowed(handler, view) {
37271
38655
  return view.roles.filter((role) => view.allowed[role]?.[handler] === true);
37272
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
+ }
37273
38662
  function registerTryCommand(parent) {
37274
38663
  withJsonOption(
37275
38664
  withServerOptions(
37276
38665
  parent.command("try <handler>").description(
37277
- "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.`
37278
38667
  ).option("--dir <path>", "The app folder (default: current directory).", ".").option(
37279
38668
  "--payload <json>",
37280
38669
  "JSON payload for the handler, or @<path> to read it from a JSON file (relative to the working directory).",
@@ -37354,6 +38743,9 @@ function registerTryCommand(parent) {
37354
38743
  if (res.ok) {
37355
38744
  process.stdout.write(`ok
37356
38745
  ${JSON.stringify(res.result, null, 2)}
38746
+ `);
38747
+ const note = unboundNote(res.result);
38748
+ if (note !== null) process.stdout.write(`note: ${note}
37357
38749
  `);
37358
38750
  return;
37359
38751
  }