@qontinui/ui-bridge-native 0.6.16 → 0.7.0

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.
@@ -2379,7 +2379,7 @@ var DefaultNativeActionExecutor = class {
2379
2379
  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) {
2380
2380
  return { alreadyVisible: true, scrolled: false };
2381
2381
  }
2382
- const measured = await new Promise(
2382
+ const measured2 = await new Promise(
2383
2383
  (resolve, reject) => {
2384
2384
  let settled = false;
2385
2385
  const timer = setTimeout(() => {
@@ -2416,13 +2416,13 @@ var DefaultNativeActionExecutor = class {
2416
2416
  }
2417
2417
  }
2418
2418
  );
2419
- if (ancestorRect && Number.isFinite(measured.x) && Number.isFinite(measured.width) && measured.x + measured.width > ancestorRect.right - ancestorRect.left + 1) {
2419
+ if (ancestorRect && Number.isFinite(measured2.x) && Number.isFinite(measured2.width) && measured2.x + measured2.width > ancestorRect.right - ancestorRect.left + 1) {
2420
2420
  throw new NativeActionError(
2421
2421
  `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`,
2422
2422
  "NOT_SUPPORTED"
2423
2423
  );
2424
2424
  }
2425
- container.scrollTo({ y: Math.max(0, measured.y - padding), animated: false });
2425
+ container.scrollTo({ y: Math.max(0, measured2.y - padding), animated: false });
2426
2426
  return { alreadyVisible: false, scrolled: true };
2427
2427
  }
2428
2428
  /**
@@ -3159,7 +3159,219 @@ function createDesignHandlers(registry, viewportProvider) {
3159
3159
  };
3160
3160
  }
3161
3161
 
3162
+ // src/observation/observation.ts
3163
+ var UNKNOWN_CODES = [
3164
+ "app_unreachable",
3165
+ "capability_unavailable_in_build",
3166
+ "producer_failed",
3167
+ "producer_not_run",
3168
+ "input_missing",
3169
+ "below_confidence_floor",
3170
+ "model_reply_unparseable",
3171
+ "needs_multi_frame_input",
3172
+ "stale_input"
3173
+ ];
3174
+ function isUnknownCode(value) {
3175
+ return typeof value === "string" && UNKNOWN_CODES.includes(value);
3176
+ }
3177
+ var ObservationError = class extends Error {
3178
+ constructor(message) {
3179
+ super(message);
3180
+ this.name = "ObservationError";
3181
+ }
3182
+ };
3183
+ function toRfc3339(t) {
3184
+ const d = t instanceof Date ? t : new Date(t);
3185
+ if (Number.isNaN(d.getTime())) {
3186
+ throw new ObservationError(`not a valid timestamp: ${String(t)}`);
3187
+ }
3188
+ return d.toISOString();
3189
+ }
3190
+ function normalizeConfidence(c) {
3191
+ if (c === null || c === void 0 || !Number.isFinite(c)) return null;
3192
+ return Math.min(1, Math.max(0, c));
3193
+ }
3194
+ function fullCoverage(n) {
3195
+ return { considered: n, measured: n, unmeasured: [] };
3196
+ }
3197
+ function assertCount(value, what) {
3198
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
3199
+ throw new ObservationError(`${what} must be a non-negative integer, got ${String(value)}`);
3200
+ }
3201
+ }
3202
+ function assertCoverage(coverage) {
3203
+ if (!coverage || typeof coverage !== "object") {
3204
+ throw new ObservationError("coverage must be an object");
3205
+ }
3206
+ assertCount(coverage.considered, "coverage.considered");
3207
+ assertCount(coverage.measured, "coverage.measured");
3208
+ if (!Array.isArray(coverage.unmeasured)) {
3209
+ throw new ObservationError("coverage.unmeasured must be an array");
3210
+ }
3211
+ coverage.unmeasured.forEach((u, i) => {
3212
+ if (!u || typeof u.dimension !== "string") {
3213
+ throw new ObservationError(`coverage.unmeasured[${i}].dimension must be a string`);
3214
+ }
3215
+ assertCount(u.count, `coverage.unmeasured[${i}].count`);
3216
+ if (!isUnknownCode(u.code)) {
3217
+ throw new ObservationError(
3218
+ `coverage.unmeasured[${i}].code is not an UnknownCode: ${String(u.code)}`
3219
+ );
3220
+ }
3221
+ });
3222
+ }
3223
+ function assertSource(source) {
3224
+ if (source === null) return;
3225
+ if (typeof source !== "object" || Array.isArray(source)) {
3226
+ throw new ObservationError("source must be a JSON object or null");
3227
+ }
3228
+ }
3229
+ var RFC3339 = /^(\d{4})-(\d{2})-(\d{2})[Tt ](\d{2}):(\d{2}):(\d{2})(\.\d+)?([Zz]|[+-](\d{2}):(\d{2}))$/;
3230
+ function isRfc3339(value) {
3231
+ if (typeof value !== "string") return false;
3232
+ const m = RFC3339.exec(value);
3233
+ if (!m) return false;
3234
+ const [y, mo, d, h, mi, s] = [m[1], m[2], m[3], m[4], m[5], m[6]].map(Number);
3235
+ if (mo < 1 || mo > 12 || d < 1 || h > 23 || mi > 59 || s > 60) return false;
3236
+ const leap = y % 4 === 0 && y % 100 !== 0 || y % 400 === 0;
3237
+ const daysInMonth = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][mo - 1];
3238
+ if (d > daysInMonth) return false;
3239
+ if (m[9] !== void 0 && (Number(m[9]) > 23 || Number(m[10]) > 59)) return false;
3240
+ return true;
3241
+ }
3242
+ function assertProvenance(prov) {
3243
+ if (!prov || typeof prov !== "object") throw new ObservationError("provenance is required");
3244
+ if (!prov.producer || typeof prov.producer.id !== "string" || typeof prov.producer.version !== "string") {
3245
+ throw new ObservationError("producer must carry a string id and version");
3246
+ }
3247
+ if (!isRfc3339(prov.evaluatedAt)) {
3248
+ throw new ObservationError(
3249
+ `evaluatedAt must be an RFC3339 date-time, got ${String(prov.evaluatedAt)}`
3250
+ );
3251
+ }
3252
+ if (prov.observedAt !== null && !isRfc3339(prov.observedAt)) {
3253
+ throw new ObservationError(
3254
+ `observedAt must be an RFC3339 date-time or null, got ${String(prov.observedAt)}`
3255
+ );
3256
+ }
3257
+ assertCoverage(prov.coverage);
3258
+ if (prov.confidence !== null && (typeof prov.confidence !== "number" || !Number.isFinite(prov.confidence) || prov.confidence < 0 || prov.confidence > 1)) {
3259
+ throw new ObservationError(
3260
+ `confidence must be in [0, 1] or null, got ${String(prov.confidence)}`
3261
+ );
3262
+ }
3263
+ if (prov.cache !== null) {
3264
+ if (typeof prov.cache !== "object" || typeof prov.cache.hit !== "boolean" || !Array.isArray(prov.cache.keyInputs) || !prov.cache.keyInputs.every((k) => typeof k === "string")) {
3265
+ throw new ObservationError(
3266
+ "cache must be { hit: boolean, storedAt, keyInputs: string[] } or null"
3267
+ );
3268
+ }
3269
+ if (prov.cache.storedAt !== null && !isRfc3339(prov.cache.storedAt)) {
3270
+ throw new ObservationError(
3271
+ `cache.storedAt must be an RFC3339 date-time or null, got ${String(prov.cache.storedAt)}`
3272
+ );
3273
+ }
3274
+ }
3275
+ assertSource(prov.source);
3276
+ }
3277
+ function provenance(init) {
3278
+ if (!init.producer || typeof init.producer.id !== "string" || typeof init.producer.version !== "string") {
3279
+ throw new ObservationError("producer must carry a string id and version");
3280
+ }
3281
+ const coverage = init.coverage ?? fullCoverage(0);
3282
+ assertCoverage(coverage);
3283
+ const source = init.source ?? null;
3284
+ assertSource(source);
3285
+ const built = {
3286
+ producer: { id: init.producer.id, version: init.producer.version },
3287
+ observedAt: init.observedAt === void 0 || init.observedAt === null ? null : toRfc3339(init.observedAt),
3288
+ evaluatedAt: toRfc3339(init.evaluatedAt ?? /* @__PURE__ */ new Date()),
3289
+ coverage: {
3290
+ considered: coverage.considered,
3291
+ measured: coverage.measured,
3292
+ unmeasured: coverage.unmeasured.map((u) => ({
3293
+ dimension: u.dimension,
3294
+ count: u.count,
3295
+ code: u.code
3296
+ }))
3297
+ },
3298
+ confidence: normalizeConfidence(init.confidence),
3299
+ cache: init.cache ? {
3300
+ hit: init.cache.hit,
3301
+ storedAt: init.cache.storedAt === null ? null : toRfc3339(init.cache.storedAt),
3302
+ keyInputs: [...init.cache.keyInputs]
3303
+ } : null,
3304
+ source
3305
+ };
3306
+ assertProvenance(built);
3307
+ return built;
3308
+ }
3309
+ function measured(value, prov) {
3310
+ if (value === void 0) {
3311
+ throw new ObservationError(
3312
+ 'measured requires a value; report "nothing there" as absent and "could not look" as unknown'
3313
+ );
3314
+ }
3315
+ assertProvenance(prov);
3316
+ return { status: "measured", value, provenance: prov };
3317
+ }
3318
+ function absent(prov) {
3319
+ assertProvenance(prov);
3320
+ const { considered, measured: measured2, unmeasured } = prov.coverage;
3321
+ if (unmeasured.length > 0) {
3322
+ const dims = unmeasured.map((u) => `${u.dimension} x${u.count} (${u.code})`).join(", ");
3323
+ throw new ObservationError(
3324
+ `cannot assert absent over unmeasured coverage: ${dims}. Report the observation as measured (degraded) or unknown instead.`
3325
+ );
3326
+ }
3327
+ if (considered === 0) {
3328
+ throw new ObservationError(
3329
+ "cannot assert absent over zero considered items: the producer looked at nothing. Report the observation as unknown instead."
3330
+ );
3331
+ }
3332
+ if (measured2 !== considered) {
3333
+ throw new ObservationError(
3334
+ `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\``
3335
+ );
3336
+ }
3337
+ return { status: "absent", provenance: prov };
3338
+ }
3339
+ function unknown(code, detail, prov) {
3340
+ if (!isUnknownCode(code)) {
3341
+ throw new ObservationError(`not an UnknownCode: ${String(code)}`);
3342
+ }
3343
+ if (typeof detail !== "string") {
3344
+ throw new ObservationError("unknown requires a string detail");
3345
+ }
3346
+ assertProvenance(prov);
3347
+ return { status: "unknown", unknown: { code, detail }, provenance: prov };
3348
+ }
3349
+ var Observation = {
3350
+ provenance,
3351
+ fullCoverage,
3352
+ measured,
3353
+ absent,
3354
+ unknown
3355
+ };
3356
+
3162
3357
  // src/server/page-health.ts
3358
+ var NATIVE_PAGE_HEALTH_PRODUCER_ID = "sdk-native/page-health";
3359
+ function defaultProducer() {
3360
+ return {
3361
+ id: NATIVE_PAGE_HEALTH_PRODUCER_ID,
3362
+ version: "0.7.0"
3363
+ };
3364
+ }
3365
+ function pageHealthUnknown(code, detail, options = {}) {
3366
+ return Observation.unknown(
3367
+ code,
3368
+ detail,
3369
+ Observation.provenance({
3370
+ producer: options.producer ?? defaultProducer(),
3371
+ evaluatedAt: options.evaluatedAt
3372
+ })
3373
+ );
3374
+ }
3163
3375
  var GRID_SIZE = 20;
3164
3376
  var NAV_TYPES = /* @__PURE__ */ new Set(["button", "heading", "badge", "status-message"]);
3165
3377
  var SKIP_TEXT_TYPES = /* @__PURE__ */ new Set(["button", "link", "tab", "menuitem"]);
@@ -3228,13 +3440,66 @@ function normalizeRect(state, viewport) {
3228
3440
  height: layout.height / viewport.height
3229
3441
  };
3230
3442
  }
3231
- function diagnosePageHealth(elements, viewport) {
3443
+ function diagnosePageHealth(input, options = {}) {
3444
+ const producer = options.producer ?? defaultProducer();
3445
+ try {
3446
+ return observe(input, producer, options.evaluatedAt);
3447
+ } catch (err) {
3448
+ return Observation.unknown(
3449
+ "producer_failed",
3450
+ `page-health producer threw: ${err instanceof Error ? err.message : String(err)}`,
3451
+ Observation.provenance({ producer })
3452
+ );
3453
+ }
3454
+ }
3455
+ function observe(input, producer, evaluatedAt) {
3456
+ const base = {
3457
+ producer,
3458
+ observedAt: input.observedAt,
3459
+ evaluatedAt
3460
+ };
3461
+ if (!Array.isArray(input.elements)) {
3462
+ return Observation.unknown(
3463
+ "input_missing",
3464
+ "the registry handed over no `elements` array, so there was nothing to analyze; this says nothing about the screen itself",
3465
+ Observation.provenance({ ...base, observedAt: null })
3466
+ );
3467
+ }
3468
+ const elements = input.elements;
3469
+ const viewport = input.viewport;
3470
+ if (!viewport || !(viewport.width > 0) || !(viewport.height > 0)) {
3471
+ return Observation.unknown(
3472
+ "input_missing",
3473
+ 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`",
3474
+ Observation.provenance(base)
3475
+ );
3476
+ }
3477
+ if (elements.length === 0 && (input.registeredComponents ?? 0) === 0) {
3478
+ return Observation.unknown(
3479
+ "producer_not_run",
3480
+ "the registry reports zero elements and zero components \u2014 nothing has registered yet, so there was nothing to look at",
3481
+ Observation.provenance(base)
3482
+ );
3483
+ }
3484
+ return analyze(elements, viewport, base);
3485
+ }
3486
+ function analyze(elements, viewport, base) {
3232
3487
  const findings = [];
3233
- const projected = elements.map((el) => ({
3234
- el,
3235
- rect: el.state.visible ? normalizeRect(el.state, viewport) : null
3236
- }));
3237
- const visible = projected.filter((p) => p.rect !== null);
3488
+ const considered = elements.filter((el) => el.state.visible);
3489
+ const visible = considered.flatMap((el) => {
3490
+ const rect = normalizeRect(el.state, viewport);
3491
+ return rect ? [{ el, rect }] : [];
3492
+ });
3493
+ const withoutGeometry = considered.length - visible.length;
3494
+ const unmeasured = withoutGeometry > 0 ? [{ dimension: "geometry", count: withoutGeometry, code: "input_missing" }] : [];
3495
+ const coverage = { considered: considered.length, measured: visible.length, unmeasured };
3496
+ if (considered.length > 0 && visible.length === 0) {
3497
+ return Observation.unknown(
3498
+ "input_missing",
3499
+ `all ${considered.length} visible element(s) have no measured layout yet, so spatial coverage, layout regions and visual anomalies cannot be measured`,
3500
+ Observation.provenance({ ...base, coverage })
3501
+ );
3502
+ }
3238
3503
  const grid = Array.from(
3239
3504
  { length: GRID_SIZE },
3240
3505
  () => Array.from({ length: GRID_SIZE }, () => false)
@@ -3389,13 +3654,17 @@ function diagnosePageHealth(elements, viewport) {
3389
3654
  const heatmap = grid.map(
3390
3655
  (row) => row.map((cell) => cell ? "#" : ".").join("")
3391
3656
  );
3392
- return {
3393
- summary: worstSeverity(findings),
3394
- findings,
3395
- heatmap,
3396
- element_count: elements.length,
3397
- visible_count: visible.length
3398
- };
3657
+ return Observation.measured(
3658
+ {
3659
+ summary: worstSeverity(findings),
3660
+ findings,
3661
+ heatmap,
3662
+ element_count: elements.length,
3663
+ visible_count: visible.length
3664
+ },
3665
+ // A deduction over the registry, not an estimate.
3666
+ Observation.provenance({ ...base, coverage })
3667
+ );
3399
3668
  }
3400
3669
 
3401
3670
  // src/server/handlers.ts
@@ -3843,13 +4112,30 @@ function createServerHandlers(registry, executor, config) {
3843
4112
  },
3844
4113
  getPageHealth: async (ctx) => {
3845
4114
  const bodyViewport = ctx.body?.viewport;
3846
- const viewport = bodyViewport ?? config?.viewportProvider?.() ?? { width: 0, height: 0 };
3847
- const elements = registry.getAllElements().map((e) => ({
3848
- type: e.type,
3849
- state: e.getState()
3850
- }));
3851
- const report = diagnosePageHealth(elements, viewport);
3852
- return success2(report);
4115
+ let viewport;
4116
+ try {
4117
+ viewport = bodyViewport ?? config?.viewportProvider?.() ?? null;
4118
+ } catch {
4119
+ viewport = null;
4120
+ }
4121
+ const observedAt = Date.now();
4122
+ let elements;
4123
+ let registeredComponents;
4124
+ try {
4125
+ elements = registry.getAllElements().map((e) => ({
4126
+ type: e.type,
4127
+ state: e.getState()
4128
+ }));
4129
+ registeredComponents = registry.getAllComponents().length;
4130
+ } catch (err) {
4131
+ return success2(
4132
+ pageHealthUnknown(
4133
+ "producer_failed",
4134
+ `could not read the registry: ${err instanceof Error ? err.message : String(err)}`
4135
+ )
4136
+ );
4137
+ }
4138
+ return success2(diagnosePageHealth({ elements, registeredComponents, viewport, observedAt }));
3853
4139
  },
3854
4140
  // Workflows
3855
4141
  getWorkflows: async () => {
@@ -4353,7 +4639,7 @@ function createServerHandlers(registry, executor, config) {
4353
4639
  };
4354
4640
  if (config?.appInfo) {
4355
4641
  response.uiBridge = {
4356
- version: "0.6.16",
4642
+ version: "0.7.0",
4357
4643
  ...config.appInfo,
4358
4644
  capabilities: ["elements", "components", "actions", "design"],
4359
4645
  elementCount: stats.elements,