@qontinui/ui-bridge-native 0.6.16 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1064,7 +1064,219 @@ function createDesignHandlers(registry, viewportProvider) {
1064
1064
  };
1065
1065
  }
1066
1066
 
1067
+ // src/observation/observation.ts
1068
+ var UNKNOWN_CODES = [
1069
+ "app_unreachable",
1070
+ "capability_unavailable_in_build",
1071
+ "producer_failed",
1072
+ "producer_not_run",
1073
+ "input_missing",
1074
+ "below_confidence_floor",
1075
+ "model_reply_unparseable",
1076
+ "needs_multi_frame_input",
1077
+ "stale_input"
1078
+ ];
1079
+ function isUnknownCode(value) {
1080
+ return typeof value === "string" && UNKNOWN_CODES.includes(value);
1081
+ }
1082
+ var ObservationError = class extends Error {
1083
+ constructor(message) {
1084
+ super(message);
1085
+ this.name = "ObservationError";
1086
+ }
1087
+ };
1088
+ function toRfc3339(t) {
1089
+ const d = t instanceof Date ? t : new Date(t);
1090
+ if (Number.isNaN(d.getTime())) {
1091
+ throw new ObservationError(`not a valid timestamp: ${String(t)}`);
1092
+ }
1093
+ return d.toISOString();
1094
+ }
1095
+ function normalizeConfidence(c) {
1096
+ if (c === null || c === void 0 || !Number.isFinite(c)) return null;
1097
+ return Math.min(1, Math.max(0, c));
1098
+ }
1099
+ function fullCoverage(n) {
1100
+ return { considered: n, measured: n, unmeasured: [] };
1101
+ }
1102
+ function assertCount(value, what) {
1103
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
1104
+ throw new ObservationError(`${what} must be a non-negative integer, got ${String(value)}`);
1105
+ }
1106
+ }
1107
+ function assertCoverage(coverage) {
1108
+ if (!coverage || typeof coverage !== "object") {
1109
+ throw new ObservationError("coverage must be an object");
1110
+ }
1111
+ assertCount(coverage.considered, "coverage.considered");
1112
+ assertCount(coverage.measured, "coverage.measured");
1113
+ if (!Array.isArray(coverage.unmeasured)) {
1114
+ throw new ObservationError("coverage.unmeasured must be an array");
1115
+ }
1116
+ coverage.unmeasured.forEach((u, i) => {
1117
+ if (!u || typeof u.dimension !== "string") {
1118
+ throw new ObservationError(`coverage.unmeasured[${i}].dimension must be a string`);
1119
+ }
1120
+ assertCount(u.count, `coverage.unmeasured[${i}].count`);
1121
+ if (!isUnknownCode(u.code)) {
1122
+ throw new ObservationError(
1123
+ `coverage.unmeasured[${i}].code is not an UnknownCode: ${String(u.code)}`
1124
+ );
1125
+ }
1126
+ });
1127
+ }
1128
+ function assertSource(source) {
1129
+ if (source === null) return;
1130
+ if (typeof source !== "object" || Array.isArray(source)) {
1131
+ throw new ObservationError("source must be a JSON object or null");
1132
+ }
1133
+ }
1134
+ var RFC3339 = /^(\d{4})-(\d{2})-(\d{2})[Tt ](\d{2}):(\d{2}):(\d{2})(\.\d+)?([Zz]|[+-](\d{2}):(\d{2}))$/;
1135
+ function isRfc3339(value) {
1136
+ if (typeof value !== "string") return false;
1137
+ const m = RFC3339.exec(value);
1138
+ if (!m) return false;
1139
+ const [y, mo, d, h, mi, s] = [m[1], m[2], m[3], m[4], m[5], m[6]].map(Number);
1140
+ if (mo < 1 || mo > 12 || d < 1 || h > 23 || mi > 59 || s > 60) return false;
1141
+ const leap = y % 4 === 0 && y % 100 !== 0 || y % 400 === 0;
1142
+ const daysInMonth = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][mo - 1];
1143
+ if (d > daysInMonth) return false;
1144
+ if (m[9] !== void 0 && (Number(m[9]) > 23 || Number(m[10]) > 59)) return false;
1145
+ return true;
1146
+ }
1147
+ function assertProvenance(prov) {
1148
+ if (!prov || typeof prov !== "object") throw new ObservationError("provenance is required");
1149
+ if (!prov.producer || typeof prov.producer.id !== "string" || typeof prov.producer.version !== "string") {
1150
+ throw new ObservationError("producer must carry a string id and version");
1151
+ }
1152
+ if (!isRfc3339(prov.evaluatedAt)) {
1153
+ throw new ObservationError(
1154
+ `evaluatedAt must be an RFC3339 date-time, got ${String(prov.evaluatedAt)}`
1155
+ );
1156
+ }
1157
+ if (prov.observedAt !== null && !isRfc3339(prov.observedAt)) {
1158
+ throw new ObservationError(
1159
+ `observedAt must be an RFC3339 date-time or null, got ${String(prov.observedAt)}`
1160
+ );
1161
+ }
1162
+ assertCoverage(prov.coverage);
1163
+ if (prov.confidence !== null && (typeof prov.confidence !== "number" || !Number.isFinite(prov.confidence) || prov.confidence < 0 || prov.confidence > 1)) {
1164
+ throw new ObservationError(
1165
+ `confidence must be in [0, 1] or null, got ${String(prov.confidence)}`
1166
+ );
1167
+ }
1168
+ if (prov.cache !== null) {
1169
+ if (typeof prov.cache !== "object" || typeof prov.cache.hit !== "boolean" || !Array.isArray(prov.cache.keyInputs) || !prov.cache.keyInputs.every((k) => typeof k === "string")) {
1170
+ throw new ObservationError(
1171
+ "cache must be { hit: boolean, storedAt, keyInputs: string[] } or null"
1172
+ );
1173
+ }
1174
+ if (prov.cache.storedAt !== null && !isRfc3339(prov.cache.storedAt)) {
1175
+ throw new ObservationError(
1176
+ `cache.storedAt must be an RFC3339 date-time or null, got ${String(prov.cache.storedAt)}`
1177
+ );
1178
+ }
1179
+ }
1180
+ assertSource(prov.source);
1181
+ }
1182
+ function provenance(init) {
1183
+ if (!init.producer || typeof init.producer.id !== "string" || typeof init.producer.version !== "string") {
1184
+ throw new ObservationError("producer must carry a string id and version");
1185
+ }
1186
+ const coverage = init.coverage ?? fullCoverage(0);
1187
+ assertCoverage(coverage);
1188
+ const source = init.source ?? null;
1189
+ assertSource(source);
1190
+ const built = {
1191
+ producer: { id: init.producer.id, version: init.producer.version },
1192
+ observedAt: init.observedAt === void 0 || init.observedAt === null ? null : toRfc3339(init.observedAt),
1193
+ evaluatedAt: toRfc3339(init.evaluatedAt ?? /* @__PURE__ */ new Date()),
1194
+ coverage: {
1195
+ considered: coverage.considered,
1196
+ measured: coverage.measured,
1197
+ unmeasured: coverage.unmeasured.map((u) => ({
1198
+ dimension: u.dimension,
1199
+ count: u.count,
1200
+ code: u.code
1201
+ }))
1202
+ },
1203
+ confidence: normalizeConfidence(init.confidence),
1204
+ cache: init.cache ? {
1205
+ hit: init.cache.hit,
1206
+ storedAt: init.cache.storedAt === null ? null : toRfc3339(init.cache.storedAt),
1207
+ keyInputs: [...init.cache.keyInputs]
1208
+ } : null,
1209
+ source
1210
+ };
1211
+ assertProvenance(built);
1212
+ return built;
1213
+ }
1214
+ function measured(value, prov) {
1215
+ if (value === void 0) {
1216
+ throw new ObservationError(
1217
+ 'measured requires a value; report "nothing there" as absent and "could not look" as unknown'
1218
+ );
1219
+ }
1220
+ assertProvenance(prov);
1221
+ return { status: "measured", value, provenance: prov };
1222
+ }
1223
+ function absent(prov) {
1224
+ assertProvenance(prov);
1225
+ const { considered, measured: measured2, unmeasured } = prov.coverage;
1226
+ if (unmeasured.length > 0) {
1227
+ const dims = unmeasured.map((u) => `${u.dimension} x${u.count} (${u.code})`).join(", ");
1228
+ throw new ObservationError(
1229
+ `cannot assert absent over unmeasured coverage: ${dims}. Report the observation as measured (degraded) or unknown instead.`
1230
+ );
1231
+ }
1232
+ if (considered === 0) {
1233
+ throw new ObservationError(
1234
+ "cannot assert absent over zero considered items: the producer looked at nothing. Report the observation as unknown instead."
1235
+ );
1236
+ }
1237
+ if (measured2 !== considered) {
1238
+ throw new ObservationError(
1239
+ `an \`absent\` observation must have measured every considered item (measured ${measured2} of ${considered}); a partial look is \`measured\` with its gaps in \`coverage.unmeasured\`, never \`absent\``
1240
+ );
1241
+ }
1242
+ return { status: "absent", provenance: prov };
1243
+ }
1244
+ function unknown(code, detail, prov) {
1245
+ if (!isUnknownCode(code)) {
1246
+ throw new ObservationError(`not an UnknownCode: ${String(code)}`);
1247
+ }
1248
+ if (typeof detail !== "string") {
1249
+ throw new ObservationError("unknown requires a string detail");
1250
+ }
1251
+ assertProvenance(prov);
1252
+ return { status: "unknown", unknown: { code, detail }, provenance: prov };
1253
+ }
1254
+ var Observation = {
1255
+ provenance,
1256
+ fullCoverage,
1257
+ measured,
1258
+ absent,
1259
+ unknown
1260
+ };
1261
+
1067
1262
  // src/server/page-health.ts
1263
+ var NATIVE_PAGE_HEALTH_PRODUCER_ID = "sdk-native/page-health";
1264
+ function defaultProducer() {
1265
+ return {
1266
+ id: NATIVE_PAGE_HEALTH_PRODUCER_ID,
1267
+ version: "0.7.1"
1268
+ };
1269
+ }
1270
+ function pageHealthUnknown(code, detail, options = {}) {
1271
+ return Observation.unknown(
1272
+ code,
1273
+ detail,
1274
+ Observation.provenance({
1275
+ producer: options.producer ?? defaultProducer(),
1276
+ evaluatedAt: options.evaluatedAt
1277
+ })
1278
+ );
1279
+ }
1068
1280
  var GRID_SIZE = 20;
1069
1281
  var NAV_TYPES = /* @__PURE__ */ new Set(["button", "heading", "badge", "status-message"]);
1070
1282
  var SKIP_TEXT_TYPES = /* @__PURE__ */ new Set(["button", "link", "tab", "menuitem"]);
@@ -1094,14 +1306,7 @@ var LOADING_PHRASES = [
1094
1306
  "initializing",
1095
1307
  "fetching"
1096
1308
  ];
1097
- var EMPTY_PHRASES = [
1098
- "no data",
1099
- "no results",
1100
- "nothing here",
1101
- "empty",
1102
- "no items",
1103
- "get started"
1104
- ];
1309
+ var EMPTY_PHRASES = ["no data", "no results", "nothing here", "empty", "no items", "get started"];
1105
1310
  function severityRank(s) {
1106
1311
  if (s === "CRITICAL") return 3;
1107
1312
  if (s === "WARNING") return 2;
@@ -1133,13 +1338,90 @@ function normalizeRect(state, viewport) {
1133
1338
  height: layout.height / viewport.height
1134
1339
  };
1135
1340
  }
1136
- function diagnosePageHealth(elements, viewport) {
1341
+ function diagnosePageHealth(input, options = {}) {
1342
+ const producer = options.producer ?? defaultProducer();
1343
+ try {
1344
+ return observe(input, producer, options.evaluatedAt);
1345
+ } catch (err) {
1346
+ return Observation.unknown(
1347
+ "producer_failed",
1348
+ `page-health producer threw: ${err instanceof Error ? err.message : String(err)}`,
1349
+ Observation.provenance({ producer })
1350
+ );
1351
+ }
1352
+ }
1353
+ function observe(input, producer, evaluatedAt) {
1354
+ const base = {
1355
+ producer,
1356
+ observedAt: input.observedAt,
1357
+ evaluatedAt
1358
+ };
1359
+ if (!Array.isArray(input.elements)) {
1360
+ return Observation.unknown(
1361
+ "input_missing",
1362
+ "the registry handed over no `elements` array, so there was nothing to analyze; this says nothing about the screen itself",
1363
+ Observation.provenance({ ...base, observedAt: null })
1364
+ );
1365
+ }
1366
+ const elements = input.elements;
1367
+ const viewport = input.viewport;
1368
+ if (!viewport || !(viewport.width > 0) || !(viewport.height > 0)) {
1369
+ return Observation.unknown(
1370
+ "input_missing",
1371
+ viewport ? `the device viewport is degenerate (${viewport.width}x${viewport.height}), so no element can be placed on screen \u2014 pass \`body.viewport\` or wire \`viewportProvider\`` : "the device viewport is unknown, so no element can be placed on screen \u2014 pass `body.viewport` or wire `viewportProvider`",
1372
+ Observation.provenance(base)
1373
+ );
1374
+ }
1375
+ if (elements.length === 0 && (input.registeredComponents ?? 0) === 0) {
1376
+ return Observation.unknown(
1377
+ "producer_not_run",
1378
+ "the registry reports zero elements and zero components \u2014 nothing has registered yet, so there was nothing to look at",
1379
+ Observation.provenance(base)
1380
+ );
1381
+ }
1382
+ return analyze(elements, viewport, base);
1383
+ }
1384
+ function nothingOnScreenDetail(elements, visibleCount, visibilityUnstated) {
1385
+ if (visibilityUnstated === 0 && visibleCount > 0) {
1386
+ return `all ${visibleCount} visible element(s) have no measured layout yet, so spatial coverage, layout regions and visual anomalies cannot be measured`;
1387
+ }
1388
+ if (visibilityUnstated > 0) {
1389
+ const geometry = visibleCount > 0 ? ` and none of the ${visibleCount} visible one(s) has a measured layout` : "";
1390
+ return `${visibilityUnstated} of ${elements.length} element(s) carry no boolean \`state.visible\`${geometry}, so no element can be placed on screen; spatial coverage, layout regions and visual anomalies cannot be measured`;
1391
+ }
1392
+ const unmounted = elements.filter((el) => el.state?.mounted === false).length;
1393
+ return `all ${elements.length} registered element(s) report visible: false (unmounted=${unmounted}, off-screen route or otherwise=${elements.length - unmounted}), so whatever is on screen is not a registered element and there is nothing on screen to measure; this says nothing about whether the screen is blank`;
1394
+ }
1395
+ function analyze(elements, viewport, base) {
1137
1396
  const findings = [];
1138
- const projected = elements.map((el) => ({
1139
- el,
1140
- rect: el.state.visible ? normalizeRect(el.state, viewport) : null
1141
- }));
1142
- const visible = projected.filter((p) => p.rect !== null);
1397
+ const onScreen = elements.filter((el) => el.state?.visible === true);
1398
+ const visibilityUnstated = elements.filter(
1399
+ (el) => typeof el.state?.visible !== "boolean"
1400
+ ).length;
1401
+ const visible = onScreen.flatMap((el) => {
1402
+ const rect = normalizeRect(el.state, viewport);
1403
+ return rect ? [{ el, rect }] : [];
1404
+ });
1405
+ const withoutGeometry = onScreen.length - visible.length;
1406
+ const unmeasured = [];
1407
+ if (withoutGeometry > 0) {
1408
+ unmeasured.push({ dimension: "geometry", count: withoutGeometry, code: "input_missing" });
1409
+ }
1410
+ if (visibilityUnstated > 0) {
1411
+ unmeasured.push({ dimension: "visibility", count: visibilityUnstated, code: "input_missing" });
1412
+ }
1413
+ const coverage = {
1414
+ considered: onScreen.length + visibilityUnstated,
1415
+ measured: visible.length,
1416
+ unmeasured
1417
+ };
1418
+ if (elements.length > 0 && visible.length === 0) {
1419
+ return Observation.unknown(
1420
+ "input_missing",
1421
+ nothingOnScreenDetail(elements, onScreen.length, visibilityUnstated),
1422
+ Observation.provenance({ ...base, coverage })
1423
+ );
1424
+ }
1143
1425
  const grid = Array.from(
1144
1426
  { length: GRID_SIZE },
1145
1427
  () => Array.from({ length: GRID_SIZE }, () => false)
@@ -1226,7 +1508,7 @@ function diagnosePageHealth(elements, viewport) {
1226
1508
  for (const el of elements) {
1227
1509
  const elType = el.type ?? "";
1228
1510
  if (SKIP_TEXT_TYPES.has(elType)) continue;
1229
- const text = (el.state.textContent ?? "").toString();
1511
+ const text = (el.state?.textContent ?? "").toString();
1230
1512
  if (!text) continue;
1231
1513
  const textLower = text.toLowerCase();
1232
1514
  for (const p of ERROR_PHRASES) {
@@ -1267,7 +1549,7 @@ function diagnosePageHealth(elements, viewport) {
1267
1549
  for (const el of elements) {
1268
1550
  if (!INTERACTIVE_TYPES2.has(el.type)) continue;
1269
1551
  interactiveTotal++;
1270
- if (el.state.enabled === false) interactiveDisabled++;
1552
+ if (el.state?.enabled === false) interactiveDisabled++;
1271
1553
  }
1272
1554
  const interactiveSeverity = interactiveTotal > 0 && interactiveDisabled / interactiveTotal > 0.5 ? "WARNING" : "OK";
1273
1555
  findings.push({
@@ -1291,16 +1573,18 @@ function diagnosePageHealth(elements, viewport) {
1291
1573
  detail: `zero_size=${zeroSize}, outside_viewport=${outsideViewport} (horizontal-overflow only; below/above-fold scrollable content is not an anomaly)`,
1292
1574
  data: { zero_size: zeroSize, outside_viewport: outsideViewport }
1293
1575
  });
1294
- const heatmap = grid.map(
1295
- (row) => row.map((cell) => cell ? "#" : ".").join("")
1576
+ const heatmap = grid.map((row) => row.map((cell) => cell ? "#" : ".").join(""));
1577
+ return Observation.measured(
1578
+ {
1579
+ summary: worstSeverity(findings),
1580
+ findings,
1581
+ heatmap,
1582
+ element_count: elements.length,
1583
+ visible_count: visible.length
1584
+ },
1585
+ // A deduction over the registry, not an estimate.
1586
+ Observation.provenance({ ...base, coverage })
1296
1587
  );
1297
- return {
1298
- summary: worstSeverity(findings),
1299
- findings,
1300
- heatmap,
1301
- element_count: elements.length,
1302
- visible_count: visible.length
1303
- };
1304
1588
  }
1305
1589
 
1306
1590
  // src/server/handlers.ts
@@ -1748,13 +2032,30 @@ function createServerHandlers(registry, executor, config) {
1748
2032
  },
1749
2033
  getPageHealth: async (ctx) => {
1750
2034
  const bodyViewport = ctx.body?.viewport;
1751
- const viewport = bodyViewport ?? config?.viewportProvider?.() ?? { width: 0, height: 0 };
1752
- const elements = registry.getAllElements().map((e) => ({
1753
- type: e.type,
1754
- state: e.getState()
1755
- }));
1756
- const report = diagnosePageHealth(elements, viewport);
1757
- return success2(report);
2035
+ let viewport;
2036
+ try {
2037
+ viewport = bodyViewport ?? config?.viewportProvider?.() ?? null;
2038
+ } catch {
2039
+ viewport = null;
2040
+ }
2041
+ const observedAt = Date.now();
2042
+ let elements;
2043
+ let registeredComponents;
2044
+ try {
2045
+ elements = registry.getAllElements().map((e) => ({
2046
+ type: e.type,
2047
+ state: e.getState()
2048
+ }));
2049
+ registeredComponents = registry.getAllComponents().length;
2050
+ } catch (err) {
2051
+ return success2(
2052
+ pageHealthUnknown(
2053
+ "producer_failed",
2054
+ `could not read the registry: ${err instanceof Error ? err.message : String(err)}`
2055
+ )
2056
+ );
2057
+ }
2058
+ return success2(diagnosePageHealth({ elements, registeredComponents, viewport, observedAt }));
1758
2059
  },
1759
2060
  // Workflows
1760
2061
  getWorkflows: async () => {
@@ -2258,7 +2559,7 @@ function createServerHandlers(registry, executor, config) {
2258
2559
  };
2259
2560
  if (config?.appInfo) {
2260
2561
  response.uiBridge = {
2261
- version: "0.6.16",
2562
+ version: "0.7.1",
2262
2563
  ...config.appInfo,
2263
2564
  capabilities: ["elements", "components", "actions", "design"],
2264
2565
  elementCount: stats.elements,
@@ -4548,6 +4849,6 @@ async function bindWithRetry(attempt, options = {}) {
4548
4849
  }
4549
4850
  }
4550
4851
 
4551
- export { ConsoleErrorBuffer, NativeUIBridgeServer, NetworkRequestBuffer, OPCODE_BINARY, OPCODE_CLOSE, OPCODE_CONTINUATION, OPCODE_PING, OPCODE_PONG, OPCODE_TEXT, UI_BRIDGE_NATIVE_ROUTES, WebSocketConnection, WebSocketEventBridge, bindWithRetry, buildUpgradeResponse, computeAcceptKey, createDesignHandlers, createNativeServer, createServerHandlers, decodeFrames, decodeTextPayload, encodeCloseFrame, encodePingFrame, encodePongFrame, encodeTextFrame, httpStatusForResponse, isAddrInUseError, isBatchRequest, isSubscribe, isUnsubscribe };
4852
+ export { ConsoleErrorBuffer, NATIVE_PAGE_HEALTH_PRODUCER_ID, NativeUIBridgeServer, NetworkRequestBuffer, OPCODE_BINARY, OPCODE_CLOSE, OPCODE_CONTINUATION, OPCODE_PING, OPCODE_PONG, OPCODE_TEXT, Observation, ObservationError, UI_BRIDGE_NATIVE_ROUTES, UNKNOWN_CODES, WebSocketConnection, WebSocketEventBridge, bindWithRetry, buildUpgradeResponse, computeAcceptKey, createDesignHandlers, createNativeServer, createServerHandlers, decodeFrames, decodeTextPayload, diagnosePageHealth, encodeCloseFrame, encodePingFrame, encodePongFrame, encodeTextFrame, httpStatusForResponse, isAddrInUseError, isBatchRequest, isSubscribe, isUnknownCode, isUnsubscribe, pageHealthUnknown };
4552
4853
  //# sourceMappingURL=index.mjs.map
4553
4854
  //# sourceMappingURL=index.mjs.map