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