@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.
package/dist/index.mjs CHANGED
@@ -2462,7 +2462,7 @@ var DefaultNativeActionExecutor = class {
2462
2462
  if (rect && clip && !isEmptyRect(clip) && rect.right > rect.left && rect.bottom > rect.top && rect.top >= clip.top && rect.left >= clip.left && rect.bottom <= clip.bottom && rect.right <= clip.right) {
2463
2463
  return { alreadyVisible: true, scrolled: false };
2464
2464
  }
2465
- const measured = await new Promise(
2465
+ const measured2 = await new Promise(
2466
2466
  (resolve, reject) => {
2467
2467
  let settled = false;
2468
2468
  const timer = setTimeout(() => {
@@ -2499,13 +2499,13 @@ var DefaultNativeActionExecutor = class {
2499
2499
  }
2500
2500
  }
2501
2501
  );
2502
- if (ancestorRect && Number.isFinite(measured.x) && Number.isFinite(measured.width) && measured.x + measured.width > ancestorRect.right - ancestorRect.left + 1) {
2502
+ if (ancestorRect && Number.isFinite(measured2.x) && Number.isFinite(measured2.width) && measured2.x + measured2.width > ancestorRect.right - ancestorRect.left + 1) {
2503
2503
  throw new NativeActionError(
2504
2504
  `scrollIntoView is not supported on element "${element.id}": it is laid out beyond the horizontal extent of its scroll ancestor "${ancestorId}" (sideways overflow, a horizontal container, or a nested horizontal container \u2014 declare that inner container as its scrollAncestorId); only vertical scrolling is supported`,
2505
2505
  "NOT_SUPPORTED"
2506
2506
  );
2507
2507
  }
2508
- container.scrollTo({ y: Math.max(0, measured.y - padding), animated: false });
2508
+ container.scrollTo({ y: Math.max(0, measured2.y - padding), animated: false });
2509
2509
  return { alreadyVisible: false, scrolled: true };
2510
2510
  }
2511
2511
  /**
@@ -3242,7 +3242,219 @@ function createDesignHandlers(registry, viewportProvider) {
3242
3242
  };
3243
3243
  }
3244
3244
 
3245
+ // src/observation/observation.ts
3246
+ var UNKNOWN_CODES = [
3247
+ "app_unreachable",
3248
+ "capability_unavailable_in_build",
3249
+ "producer_failed",
3250
+ "producer_not_run",
3251
+ "input_missing",
3252
+ "below_confidence_floor",
3253
+ "model_reply_unparseable",
3254
+ "needs_multi_frame_input",
3255
+ "stale_input"
3256
+ ];
3257
+ function isUnknownCode(value) {
3258
+ return typeof value === "string" && UNKNOWN_CODES.includes(value);
3259
+ }
3260
+ var ObservationError = class extends Error {
3261
+ constructor(message) {
3262
+ super(message);
3263
+ this.name = "ObservationError";
3264
+ }
3265
+ };
3266
+ function toRfc3339(t) {
3267
+ const d = t instanceof Date ? t : new Date(t);
3268
+ if (Number.isNaN(d.getTime())) {
3269
+ throw new ObservationError(`not a valid timestamp: ${String(t)}`);
3270
+ }
3271
+ return d.toISOString();
3272
+ }
3273
+ function normalizeConfidence(c) {
3274
+ if (c === null || c === void 0 || !Number.isFinite(c)) return null;
3275
+ return Math.min(1, Math.max(0, c));
3276
+ }
3277
+ function fullCoverage(n) {
3278
+ return { considered: n, measured: n, unmeasured: [] };
3279
+ }
3280
+ function assertCount(value, what) {
3281
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
3282
+ throw new ObservationError(`${what} must be a non-negative integer, got ${String(value)}`);
3283
+ }
3284
+ }
3285
+ function assertCoverage(coverage) {
3286
+ if (!coverage || typeof coverage !== "object") {
3287
+ throw new ObservationError("coverage must be an object");
3288
+ }
3289
+ assertCount(coverage.considered, "coverage.considered");
3290
+ assertCount(coverage.measured, "coverage.measured");
3291
+ if (!Array.isArray(coverage.unmeasured)) {
3292
+ throw new ObservationError("coverage.unmeasured must be an array");
3293
+ }
3294
+ coverage.unmeasured.forEach((u, i) => {
3295
+ if (!u || typeof u.dimension !== "string") {
3296
+ throw new ObservationError(`coverage.unmeasured[${i}].dimension must be a string`);
3297
+ }
3298
+ assertCount(u.count, `coverage.unmeasured[${i}].count`);
3299
+ if (!isUnknownCode(u.code)) {
3300
+ throw new ObservationError(
3301
+ `coverage.unmeasured[${i}].code is not an UnknownCode: ${String(u.code)}`
3302
+ );
3303
+ }
3304
+ });
3305
+ }
3306
+ function assertSource(source) {
3307
+ if (source === null) return;
3308
+ if (typeof source !== "object" || Array.isArray(source)) {
3309
+ throw new ObservationError("source must be a JSON object or null");
3310
+ }
3311
+ }
3312
+ var RFC3339 = /^(\d{4})-(\d{2})-(\d{2})[Tt ](\d{2}):(\d{2}):(\d{2})(\.\d+)?([Zz]|[+-](\d{2}):(\d{2}))$/;
3313
+ function isRfc3339(value) {
3314
+ if (typeof value !== "string") return false;
3315
+ const m = RFC3339.exec(value);
3316
+ if (!m) return false;
3317
+ const [y, mo, d, h, mi, s] = [m[1], m[2], m[3], m[4], m[5], m[6]].map(Number);
3318
+ if (mo < 1 || mo > 12 || d < 1 || h > 23 || mi > 59 || s > 60) return false;
3319
+ const leap = y % 4 === 0 && y % 100 !== 0 || y % 400 === 0;
3320
+ const daysInMonth = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][mo - 1];
3321
+ if (d > daysInMonth) return false;
3322
+ if (m[9] !== void 0 && (Number(m[9]) > 23 || Number(m[10]) > 59)) return false;
3323
+ return true;
3324
+ }
3325
+ function assertProvenance(prov) {
3326
+ if (!prov || typeof prov !== "object") throw new ObservationError("provenance is required");
3327
+ if (!prov.producer || typeof prov.producer.id !== "string" || typeof prov.producer.version !== "string") {
3328
+ throw new ObservationError("producer must carry a string id and version");
3329
+ }
3330
+ if (!isRfc3339(prov.evaluatedAt)) {
3331
+ throw new ObservationError(
3332
+ `evaluatedAt must be an RFC3339 date-time, got ${String(prov.evaluatedAt)}`
3333
+ );
3334
+ }
3335
+ if (prov.observedAt !== null && !isRfc3339(prov.observedAt)) {
3336
+ throw new ObservationError(
3337
+ `observedAt must be an RFC3339 date-time or null, got ${String(prov.observedAt)}`
3338
+ );
3339
+ }
3340
+ assertCoverage(prov.coverage);
3341
+ if (prov.confidence !== null && (typeof prov.confidence !== "number" || !Number.isFinite(prov.confidence) || prov.confidence < 0 || prov.confidence > 1)) {
3342
+ throw new ObservationError(
3343
+ `confidence must be in [0, 1] or null, got ${String(prov.confidence)}`
3344
+ );
3345
+ }
3346
+ if (prov.cache !== null) {
3347
+ if (typeof prov.cache !== "object" || typeof prov.cache.hit !== "boolean" || !Array.isArray(prov.cache.keyInputs) || !prov.cache.keyInputs.every((k) => typeof k === "string")) {
3348
+ throw new ObservationError(
3349
+ "cache must be { hit: boolean, storedAt, keyInputs: string[] } or null"
3350
+ );
3351
+ }
3352
+ if (prov.cache.storedAt !== null && !isRfc3339(prov.cache.storedAt)) {
3353
+ throw new ObservationError(
3354
+ `cache.storedAt must be an RFC3339 date-time or null, got ${String(prov.cache.storedAt)}`
3355
+ );
3356
+ }
3357
+ }
3358
+ assertSource(prov.source);
3359
+ }
3360
+ function provenance(init) {
3361
+ if (!init.producer || typeof init.producer.id !== "string" || typeof init.producer.version !== "string") {
3362
+ throw new ObservationError("producer must carry a string id and version");
3363
+ }
3364
+ const coverage = init.coverage ?? fullCoverage(0);
3365
+ assertCoverage(coverage);
3366
+ const source = init.source ?? null;
3367
+ assertSource(source);
3368
+ const built = {
3369
+ producer: { id: init.producer.id, version: init.producer.version },
3370
+ observedAt: init.observedAt === void 0 || init.observedAt === null ? null : toRfc3339(init.observedAt),
3371
+ evaluatedAt: toRfc3339(init.evaluatedAt ?? /* @__PURE__ */ new Date()),
3372
+ coverage: {
3373
+ considered: coverage.considered,
3374
+ measured: coverage.measured,
3375
+ unmeasured: coverage.unmeasured.map((u) => ({
3376
+ dimension: u.dimension,
3377
+ count: u.count,
3378
+ code: u.code
3379
+ }))
3380
+ },
3381
+ confidence: normalizeConfidence(init.confidence),
3382
+ cache: init.cache ? {
3383
+ hit: init.cache.hit,
3384
+ storedAt: init.cache.storedAt === null ? null : toRfc3339(init.cache.storedAt),
3385
+ keyInputs: [...init.cache.keyInputs]
3386
+ } : null,
3387
+ source
3388
+ };
3389
+ assertProvenance(built);
3390
+ return built;
3391
+ }
3392
+ function measured(value, prov) {
3393
+ if (value === void 0) {
3394
+ throw new ObservationError(
3395
+ 'measured requires a value; report "nothing there" as absent and "could not look" as unknown'
3396
+ );
3397
+ }
3398
+ assertProvenance(prov);
3399
+ return { status: "measured", value, provenance: prov };
3400
+ }
3401
+ function absent(prov) {
3402
+ assertProvenance(prov);
3403
+ const { considered, measured: measured2, unmeasured } = prov.coverage;
3404
+ if (unmeasured.length > 0) {
3405
+ const dims = unmeasured.map((u) => `${u.dimension} x${u.count} (${u.code})`).join(", ");
3406
+ throw new ObservationError(
3407
+ `cannot assert absent over unmeasured coverage: ${dims}. Report the observation as measured (degraded) or unknown instead.`
3408
+ );
3409
+ }
3410
+ if (considered === 0) {
3411
+ throw new ObservationError(
3412
+ "cannot assert absent over zero considered items: the producer looked at nothing. Report the observation as unknown instead."
3413
+ );
3414
+ }
3415
+ if (measured2 !== considered) {
3416
+ throw new ObservationError(
3417
+ `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\``
3418
+ );
3419
+ }
3420
+ return { status: "absent", provenance: prov };
3421
+ }
3422
+ function unknown(code, detail, prov) {
3423
+ if (!isUnknownCode(code)) {
3424
+ throw new ObservationError(`not an UnknownCode: ${String(code)}`);
3425
+ }
3426
+ if (typeof detail !== "string") {
3427
+ throw new ObservationError("unknown requires a string detail");
3428
+ }
3429
+ assertProvenance(prov);
3430
+ return { status: "unknown", unknown: { code, detail }, provenance: prov };
3431
+ }
3432
+ var Observation = {
3433
+ provenance,
3434
+ fullCoverage,
3435
+ measured,
3436
+ absent,
3437
+ unknown
3438
+ };
3439
+
3245
3440
  // src/server/page-health.ts
3441
+ var NATIVE_PAGE_HEALTH_PRODUCER_ID = "sdk-native/page-health";
3442
+ function defaultProducer() {
3443
+ return {
3444
+ id: NATIVE_PAGE_HEALTH_PRODUCER_ID,
3445
+ version: "0.7.1"
3446
+ };
3447
+ }
3448
+ function pageHealthUnknown(code, detail, options = {}) {
3449
+ return Observation.unknown(
3450
+ code,
3451
+ detail,
3452
+ Observation.provenance({
3453
+ producer: options.producer ?? defaultProducer(),
3454
+ evaluatedAt: options.evaluatedAt
3455
+ })
3456
+ );
3457
+ }
3246
3458
  var GRID_SIZE = 20;
3247
3459
  var NAV_TYPES = /* @__PURE__ */ new Set(["button", "heading", "badge", "status-message"]);
3248
3460
  var SKIP_TEXT_TYPES = /* @__PURE__ */ new Set(["button", "link", "tab", "menuitem"]);
@@ -3272,14 +3484,7 @@ var LOADING_PHRASES = [
3272
3484
  "initializing",
3273
3485
  "fetching"
3274
3486
  ];
3275
- var EMPTY_PHRASES = [
3276
- "no data",
3277
- "no results",
3278
- "nothing here",
3279
- "empty",
3280
- "no items",
3281
- "get started"
3282
- ];
3487
+ var EMPTY_PHRASES = ["no data", "no results", "nothing here", "empty", "no items", "get started"];
3283
3488
  function severityRank(s) {
3284
3489
  if (s === "CRITICAL") return 3;
3285
3490
  if (s === "WARNING") return 2;
@@ -3311,13 +3516,90 @@ function normalizeRect(state, viewport) {
3311
3516
  height: layout.height / viewport.height
3312
3517
  };
3313
3518
  }
3314
- function diagnosePageHealth(elements, viewport) {
3519
+ function diagnosePageHealth(input, options = {}) {
3520
+ const producer = options.producer ?? defaultProducer();
3521
+ try {
3522
+ return observe(input, producer, options.evaluatedAt);
3523
+ } catch (err) {
3524
+ return Observation.unknown(
3525
+ "producer_failed",
3526
+ `page-health producer threw: ${err instanceof Error ? err.message : String(err)}`,
3527
+ Observation.provenance({ producer })
3528
+ );
3529
+ }
3530
+ }
3531
+ function observe(input, producer, evaluatedAt) {
3532
+ const base = {
3533
+ producer,
3534
+ observedAt: input.observedAt,
3535
+ evaluatedAt
3536
+ };
3537
+ if (!Array.isArray(input.elements)) {
3538
+ return Observation.unknown(
3539
+ "input_missing",
3540
+ "the registry handed over no `elements` array, so there was nothing to analyze; this says nothing about the screen itself",
3541
+ Observation.provenance({ ...base, observedAt: null })
3542
+ );
3543
+ }
3544
+ const elements = input.elements;
3545
+ const viewport = input.viewport;
3546
+ if (!viewport || !(viewport.width > 0) || !(viewport.height > 0)) {
3547
+ return Observation.unknown(
3548
+ "input_missing",
3549
+ 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`",
3550
+ Observation.provenance(base)
3551
+ );
3552
+ }
3553
+ if (elements.length === 0 && (input.registeredComponents ?? 0) === 0) {
3554
+ return Observation.unknown(
3555
+ "producer_not_run",
3556
+ "the registry reports zero elements and zero components \u2014 nothing has registered yet, so there was nothing to look at",
3557
+ Observation.provenance(base)
3558
+ );
3559
+ }
3560
+ return analyze(elements, viewport, base);
3561
+ }
3562
+ function nothingOnScreenDetail(elements, visibleCount, visibilityUnstated) {
3563
+ if (visibilityUnstated === 0 && visibleCount > 0) {
3564
+ return `all ${visibleCount} visible element(s) have no measured layout yet, so spatial coverage, layout regions and visual anomalies cannot be measured`;
3565
+ }
3566
+ if (visibilityUnstated > 0) {
3567
+ const geometry = visibleCount > 0 ? ` and none of the ${visibleCount} visible one(s) has a measured layout` : "";
3568
+ 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`;
3569
+ }
3570
+ const unmounted = elements.filter((el) => el.state?.mounted === false).length;
3571
+ 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`;
3572
+ }
3573
+ function analyze(elements, viewport, base) {
3315
3574
  const findings = [];
3316
- const projected = elements.map((el) => ({
3317
- el,
3318
- rect: el.state.visible ? normalizeRect(el.state, viewport) : null
3319
- }));
3320
- const visible = projected.filter((p) => p.rect !== null);
3575
+ const onScreen = elements.filter((el) => el.state?.visible === true);
3576
+ const visibilityUnstated = elements.filter(
3577
+ (el) => typeof el.state?.visible !== "boolean"
3578
+ ).length;
3579
+ const visible = onScreen.flatMap((el) => {
3580
+ const rect = normalizeRect(el.state, viewport);
3581
+ return rect ? [{ el, rect }] : [];
3582
+ });
3583
+ const withoutGeometry = onScreen.length - visible.length;
3584
+ const unmeasured = [];
3585
+ if (withoutGeometry > 0) {
3586
+ unmeasured.push({ dimension: "geometry", count: withoutGeometry, code: "input_missing" });
3587
+ }
3588
+ if (visibilityUnstated > 0) {
3589
+ unmeasured.push({ dimension: "visibility", count: visibilityUnstated, code: "input_missing" });
3590
+ }
3591
+ const coverage = {
3592
+ considered: onScreen.length + visibilityUnstated,
3593
+ measured: visible.length,
3594
+ unmeasured
3595
+ };
3596
+ if (elements.length > 0 && visible.length === 0) {
3597
+ return Observation.unknown(
3598
+ "input_missing",
3599
+ nothingOnScreenDetail(elements, onScreen.length, visibilityUnstated),
3600
+ Observation.provenance({ ...base, coverage })
3601
+ );
3602
+ }
3321
3603
  const grid = Array.from(
3322
3604
  { length: GRID_SIZE },
3323
3605
  () => Array.from({ length: GRID_SIZE }, () => false)
@@ -3404,7 +3686,7 @@ function diagnosePageHealth(elements, viewport) {
3404
3686
  for (const el of elements) {
3405
3687
  const elType = el.type ?? "";
3406
3688
  if (SKIP_TEXT_TYPES.has(elType)) continue;
3407
- const text = (el.state.textContent ?? "").toString();
3689
+ const text = (el.state?.textContent ?? "").toString();
3408
3690
  if (!text) continue;
3409
3691
  const textLower = text.toLowerCase();
3410
3692
  for (const p of ERROR_PHRASES) {
@@ -3445,7 +3727,7 @@ function diagnosePageHealth(elements, viewport) {
3445
3727
  for (const el of elements) {
3446
3728
  if (!INTERACTIVE_TYPES2.has(el.type)) continue;
3447
3729
  interactiveTotal++;
3448
- if (el.state.enabled === false) interactiveDisabled++;
3730
+ if (el.state?.enabled === false) interactiveDisabled++;
3449
3731
  }
3450
3732
  const interactiveSeverity = interactiveTotal > 0 && interactiveDisabled / interactiveTotal > 0.5 ? "WARNING" : "OK";
3451
3733
  findings.push({
@@ -3469,16 +3751,18 @@ function diagnosePageHealth(elements, viewport) {
3469
3751
  detail: `zero_size=${zeroSize}, outside_viewport=${outsideViewport} (horizontal-overflow only; below/above-fold scrollable content is not an anomaly)`,
3470
3752
  data: { zero_size: zeroSize, outside_viewport: outsideViewport }
3471
3753
  });
3472
- const heatmap = grid.map(
3473
- (row) => row.map((cell) => cell ? "#" : ".").join("")
3754
+ const heatmap = grid.map((row) => row.map((cell) => cell ? "#" : ".").join(""));
3755
+ return Observation.measured(
3756
+ {
3757
+ summary: worstSeverity(findings),
3758
+ findings,
3759
+ heatmap,
3760
+ element_count: elements.length,
3761
+ visible_count: visible.length
3762
+ },
3763
+ // A deduction over the registry, not an estimate.
3764
+ Observation.provenance({ ...base, coverage })
3474
3765
  );
3475
- return {
3476
- summary: worstSeverity(findings),
3477
- findings,
3478
- heatmap,
3479
- element_count: elements.length,
3480
- visible_count: visible.length
3481
- };
3482
3766
  }
3483
3767
 
3484
3768
  // src/server/handlers.ts
@@ -3926,13 +4210,30 @@ function createServerHandlers(registry, executor, config) {
3926
4210
  },
3927
4211
  getPageHealth: async (ctx) => {
3928
4212
  const bodyViewport = ctx.body?.viewport;
3929
- const viewport = bodyViewport ?? config?.viewportProvider?.() ?? { width: 0, height: 0 };
3930
- const elements = registry.getAllElements().map((e) => ({
3931
- type: e.type,
3932
- state: e.getState()
3933
- }));
3934
- const report = diagnosePageHealth(elements, viewport);
3935
- return success2(report);
4213
+ let viewport;
4214
+ try {
4215
+ viewport = bodyViewport ?? config?.viewportProvider?.() ?? null;
4216
+ } catch {
4217
+ viewport = null;
4218
+ }
4219
+ const observedAt = Date.now();
4220
+ let elements;
4221
+ let registeredComponents;
4222
+ try {
4223
+ elements = registry.getAllElements().map((e) => ({
4224
+ type: e.type,
4225
+ state: e.getState()
4226
+ }));
4227
+ registeredComponents = registry.getAllComponents().length;
4228
+ } catch (err) {
4229
+ return success2(
4230
+ pageHealthUnknown(
4231
+ "producer_failed",
4232
+ `could not read the registry: ${err instanceof Error ? err.message : String(err)}`
4233
+ )
4234
+ );
4235
+ }
4236
+ return success2(diagnosePageHealth({ elements, registeredComponents, viewport, observedAt }));
3936
4237
  },
3937
4238
  // Workflows
3938
4239
  getWorkflows: async () => {
@@ -4436,7 +4737,7 @@ function createServerHandlers(registry, executor, config) {
4436
4737
  };
4437
4738
  if (config?.appInfo) {
4438
4739
  response.uiBridge = {
4439
- version: "0.6.16",
4740
+ version: "0.7.1",
4440
4741
  ...config.appInfo,
4441
4742
  capabilities: ["elements", "components", "actions", "design"],
4442
4743
  elementCount: stats.elements,