@widgetic/creator 0.3.71 → 0.3.73

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 (33) hide show
  1. package/dist/CreatorApp.svelte +882 -78
  2. package/dist/components/BrandKitPanel.svelte +556 -0
  3. package/dist/components/BrandKitPanel.svelte.d.ts +32 -0
  4. package/dist/components/DesignQuickEdit.svelte +218 -0
  5. package/dist/components/DesignQuickEdit.svelte.d.ts +33 -0
  6. package/dist/components/FileUrlSourceControl.svelte +69 -0
  7. package/dist/components/FileUrlSourceControl.svelte.d.ts +15 -0
  8. package/dist/components/VersionDropdownRow.svelte +86 -1
  9. package/dist/components/VersionDropdownRow.svelte.d.ts +10 -0
  10. package/dist/components/WidgetDetails.svelte +19 -2
  11. package/dist/components/WidgetShapeLivePreviews.svelte +8 -3
  12. package/dist/retiredSurface.test.d.ts +1 -0
  13. package/dist/retiredSurface.test.js +113 -0
  14. package/dist/stores/websocketStore.d.ts +3 -0
  15. package/dist/stores/websocketStore.js +4 -0
  16. package/dist/utils/canvasShapeSnapClient.d.ts +27 -0
  17. package/dist/utils/canvasShapeSnapClient.js +67 -0
  18. package/dist/utils/designIntentJevClient.d.ts +32 -0
  19. package/dist/utils/designIntentJevClient.js +51 -0
  20. package/dist/utils/designIntentResolver.d.ts +107 -0
  21. package/dist/utils/designIntentResolver.js +509 -0
  22. package/dist/utils/designIntentResolver.test.d.ts +1 -0
  23. package/dist/utils/designIntentResolver.test.js +246 -0
  24. package/dist/utils/designIntentTelemetry.d.ts +176 -0
  25. package/dist/utils/designIntentTelemetry.js +474 -0
  26. package/dist/utils/designIntentTelemetry.test.d.ts +1 -0
  27. package/dist/utils/designIntentTelemetry.test.js +385 -0
  28. package/dist/utils/designQuickEditValues.d.ts +51 -0
  29. package/dist/utils/designQuickEditValues.js +77 -0
  30. package/dist/utils/designQuickEditValues.test.d.ts +1 -0
  31. package/dist/utils/designQuickEditValues.test.js +100 -0
  32. package/dist/utils/pinScrollToBottom.js +9 -2
  33. package/package.json +2 -2
@@ -0,0 +1,474 @@
1
+ /**
2
+ * designIntentTelemetry — shadow-mode measurement for the canvas chat.
3
+ *
4
+ * Every message a user sends in the canvas chat is bucketed by *how it could be
5
+ * resolved*, independently of what the app actually does:
6
+ *
7
+ * literal_arithmetic — explicit value / arithmetic in code (the deterministic
8
+ * resolver already handles it; no model needed)
9
+ * bounded_set_semantic — a design ask whose property/value needs judgment
10
+ * (the JEV-shaped residual: Choice over the closed set)
11
+ * free_text — needs a text value (an LLM)
12
+ * new_property — asks for a design property that does not exist yet
13
+ * (generation; an LLM)
14
+ * not_design — not a design tweak at all → codegen as today
15
+ *
16
+ * This module is measurement only: it never applies a change and never alters a
17
+ * send. It records one event per message, persists rolling counts so fractions
18
+ * survive reloads, and reports them. Enabling it is safe in production.
19
+ *
20
+ * The Jev fallback is measured twice over: how big its candidate pool is (the
21
+ * bounded-set-semantic bucket) and how much of that pool it resolves. Every dial
22
+ * is attributed to the *ask shape* that produced it, so a fall-through count can
23
+ * be read against the question it implicates — a weak value bucket and a weak
24
+ * property roster produce entirely different reasons — instead of as one global
25
+ * total.
26
+ */
27
+ import { collectDesignProperties, resolveDesignIntent, scoreDesignProperties } from './designIntentResolver';
28
+ export const DESIGN_INTENT_BUCKETS = [
29
+ 'literal_arithmetic',
30
+ 'bounded_set_semantic',
31
+ 'free_text',
32
+ 'new_property',
33
+ 'not_design'
34
+ ];
35
+ export const JE_ASK_SHAPES = ['property_named', 'value_only', 'unknown'];
36
+ const TEXT_CONTROLLERS = new Set(['textinput', 'textarea']);
37
+ /**
38
+ * Words that signal a design ask even when no schema property is named — value
39
+ * words (colors, sizes) plus the semantic adjectives users reach for when they
40
+ * want a look rather than a value ("make it pop", "more breathing room").
41
+ */
42
+ const DESIGN_VALUE_RE = new RegExp('\\b(' +
43
+ [
44
+ 'rounder', 'rounded', 'radius', 'corner', 'corners', 'darker', 'lighter',
45
+ 'brighter', 'bigger', 'smaller', 'wider', 'narrower', 'taller', 'shorter',
46
+ 'padding', 'spacing', 'margin', 'gap', 'color', 'colour', 'tint',
47
+ 'font', 'typeface', 'typography', 'border', 'stroke', 'outline',
48
+ 'shadow', 'glow', 'opacity', 'transparent', 'align', 'alignment',
49
+ 'layout', 'columns', 'background', 'foreground', 'hover', 'accent',
50
+ 'black', 'white', 'red', 'green', 'blue', 'yellow', 'orange', 'purple',
51
+ 'pink', 'grey', 'gray', 'navy', 'teal', 'cyan', 'magenta', 'gold', 'beige',
52
+ // semantic / aesthetic asks
53
+ 'pop', 'premium', 'modern', 'sleek', 'clean', 'professional', 'playful',
54
+ 'elegant', 'bold', 'subtle', 'minimal', 'minimalistic', 'contrast',
55
+ 'roomier', 'airy', 'tighter', 'softer', 'warmer', 'cooler', 'refined',
56
+ 'polished', 'breathing', 'personality', 'aesthetic'
57
+ ].join('|') +
58
+ ')\\b', 'i');
59
+ const ADD_PROPERTY_RE = /\b(add|create|introduce|expose|support)\b/;
60
+ const PROPERTY_WORD_RE = /\b(property|properties|setting|settings|control|controls)\b/;
61
+ const MAKE_EDITABLE_RE = /\bmake\s+(?:it|the\s+\w+)\s+(?:editable|customizable|configurable)\b/;
62
+ function looksLikeNewProperty(message) {
63
+ const text = message.toLowerCase();
64
+ if (MAKE_EDITABLE_RE.test(text))
65
+ return true;
66
+ return PROPERTY_WORD_RE.test(text) && ADD_PROPERTY_RE.test(text);
67
+ }
68
+ function hasDesignValueSignal(message) {
69
+ if (/#([0-9a-f]{3}|[0-9a-f]{6})\b/i.test(message))
70
+ return true;
71
+ if (/rgba?\(/i.test(message))
72
+ return true;
73
+ if (/\d+(?:\.\d+)?\s*(px|%|rem|em)\b/i.test(message))
74
+ return true;
75
+ return DESIGN_VALUE_RE.test(message);
76
+ }
77
+ /**
78
+ * Bucket a single chat message. Pure and deterministic — the same heuristics the
79
+ * resolver uses for property naming, plus a few value/verb signals.
80
+ */
81
+ export function classifyDesignIntent(message, schema) {
82
+ const raw = String(message ?? '').trim();
83
+ if (!raw)
84
+ return { bucket: 'not_design', propertyIds: [], detail: 'empty message' };
85
+ // 1. Explicitly asks to add a design property/setting → generation.
86
+ // Checked before resolution: "add a shadow property" is about creating the
87
+ // property, not about flipping the existing one.
88
+ if (looksLikeNewProperty(raw)) {
89
+ return { bucket: 'new_property', propertyIds: [], detail: 'asks for a new design property' };
90
+ }
91
+ // 2. The deterministic resolver can satisfy it outright.
92
+ if (resolveDesignIntent(raw, schema) !== null) {
93
+ return { bucket: 'literal_arithmetic', propertyIds: [], detail: 'resolved deterministically' };
94
+ }
95
+ // 3. A design property is named but the value needs judgment.
96
+ const named = scoreDesignProperties(raw, schema);
97
+ if (named.length > 0) {
98
+ const textNamed = named.find((entry) => TEXT_CONTROLLERS.has(entry.property.controllerType));
99
+ if (textNamed) {
100
+ return {
101
+ bucket: 'free_text',
102
+ propertyIds: [textNamed.property.id],
103
+ detail: 'names a text property without a quoted value'
104
+ };
105
+ }
106
+ return {
107
+ bucket: 'bounded_set_semantic',
108
+ propertyIds: named.map((entry) => entry.property.id),
109
+ detail: 'design property named, value needs judgment',
110
+ shape: 'property_named'
111
+ };
112
+ }
113
+ // 4. A design value is stated but which property it targets needs judgment.
114
+ if (hasDesignValueSignal(raw)) {
115
+ return {
116
+ bucket: 'bounded_set_semantic',
117
+ propertyIds: [],
118
+ detail: 'design value stated, property unresolved',
119
+ shape: 'value_only'
120
+ };
121
+ }
122
+ return { bucket: 'not_design', propertyIds: [], detail: 'no design signal' };
123
+ }
124
+ /**
125
+ * The one bucket Jev is asked about. It is the residual where a design property
126
+ * or value needs judgment but the value space is closed — `free_text` and
127
+ * `new_property` can only fall through (Jev cannot generate), and `not_design`
128
+ * is not a design ask at all, so neither is ever dialed.
129
+ */
130
+ export const JEV_CANDIDATE_BUCKET = 'bounded_set_semantic';
131
+ export function isJevCandidateBucket(bucket) {
132
+ return bucket === JEV_CANDIDATE_BUCKET;
133
+ }
134
+ const STORAGE_KEY = 'widgetCreator_designIntentTelemetry_v2';
135
+ const REPORT_EVERY = 25;
136
+ function emptyBuckets() {
137
+ return {
138
+ literal_arithmetic: 0,
139
+ bounded_set_semantic: 0,
140
+ free_text: 0,
141
+ new_property: 0,
142
+ not_design: 0
143
+ };
144
+ }
145
+ function emptyShapeBreakdown() {
146
+ return { attempts: 0, acted: 0, fellThrough: 0, byReason: {} };
147
+ }
148
+ function emptyJev() {
149
+ return {
150
+ attempts: 0,
151
+ acted: 0,
152
+ fellThrough: 0,
153
+ byReason: {},
154
+ byShape: {
155
+ property_named: emptyShapeBreakdown(),
156
+ value_only: emptyShapeBreakdown(),
157
+ unknown: emptyShapeBreakdown()
158
+ },
159
+ offBucketDials: 0
160
+ };
161
+ }
162
+ function readCount(value) {
163
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : 0;
164
+ }
165
+ function readReasons(value) {
166
+ if (!value || typeof value !== 'object')
167
+ return {};
168
+ const out = {};
169
+ for (const [reason, count] of Object.entries(value)) {
170
+ out[reason] = readCount(count);
171
+ }
172
+ return out;
173
+ }
174
+ function readShapeBreakdown(value) {
175
+ const raw = (value ?? {});
176
+ return {
177
+ attempts: readCount(raw.attempts),
178
+ acted: readCount(raw.acted),
179
+ fellThrough: readCount(raw.fellThrough),
180
+ byReason: readReasons(raw.byReason)
181
+ };
182
+ }
183
+ /**
184
+ * Parse one persisted payload. Pure, so the upgrade path — every shape recorded
185
+ * before `byShape` existed — is testable without a browser: counts that are
186
+ * absent, negative or not numbers become zero, and the flat reason map it does
187
+ * have is kept.
188
+ */
189
+ export function parsePersistedTelemetry(raw) {
190
+ const base = {
191
+ total: 0,
192
+ noSchema: 0,
193
+ buckets: emptyBuckets(),
194
+ jev: emptyJev()
195
+ };
196
+ if (!raw)
197
+ return base;
198
+ let parsed;
199
+ try {
200
+ parsed = JSON.parse(raw);
201
+ }
202
+ catch {
203
+ return base;
204
+ }
205
+ if (!parsed || typeof parsed !== 'object')
206
+ return base;
207
+ const buckets = { ...base.buckets };
208
+ for (const bucket of DESIGN_INTENT_BUCKETS) {
209
+ buckets[bucket] = readCount((parsed.buckets ?? {})[bucket]);
210
+ }
211
+ return {
212
+ total: readCount(parsed.total),
213
+ noSchema: readCount(parsed.noSchema),
214
+ buckets,
215
+ jev: {
216
+ attempts: readCount(parsed.jev?.attempts),
217
+ acted: readCount(parsed.jev?.acted),
218
+ fellThrough: readCount(parsed.jev?.fellThrough),
219
+ byReason: readReasons(parsed.jev?.byReason),
220
+ byShape: {
221
+ property_named: readShapeBreakdown(parsed.jev?.byShape?.property_named),
222
+ value_only: readShapeBreakdown(parsed.jev?.byShape?.value_only),
223
+ unknown: readShapeBreakdown(parsed.jev?.byShape?.unknown)
224
+ },
225
+ offBucketDials: readCount(parsed.jev?.offBucketDials)
226
+ }
227
+ };
228
+ }
229
+ function loadPersisted() {
230
+ try {
231
+ if (typeof localStorage === 'undefined')
232
+ return parsePersistedTelemetry(null);
233
+ return parsePersistedTelemetry(localStorage.getItem(STORAGE_KEY));
234
+ }
235
+ catch {
236
+ return parsePersistedTelemetry(null);
237
+ }
238
+ }
239
+ let state = loadPersisted();
240
+ const recent = [];
241
+ const RECENT_CAP = 100;
242
+ function persist() {
243
+ try {
244
+ if (typeof localStorage === 'undefined')
245
+ return;
246
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
247
+ }
248
+ catch {
249
+ /* a full/blocked store must never break a chat send */
250
+ }
251
+ }
252
+ /**
253
+ * Record one canvas-chat send and bucket it. Shadow mode: this does not change
254
+ * any behavior — the caller generates exactly as it did before. Returns the
255
+ * classification so a caller could log or assert on it.
256
+ */
257
+ export function recordDesignIntentTelemetry(message, schema) {
258
+ const hasSchema = collectDesignProperties(schema).length > 0;
259
+ const classification = hasSchema
260
+ ? classifyDesignIntent(message, schema)
261
+ : { bucket: 'not_design', propertyIds: [], detail: 'no design schema loaded' };
262
+ state.total += 1;
263
+ if (!hasSchema)
264
+ state.noSchema += 1;
265
+ else
266
+ state.buckets[classification.bucket] += 1;
267
+ const event = {
268
+ ...classification,
269
+ messageLength: String(message ?? '').length,
270
+ at: Date.now()
271
+ };
272
+ recent.push(event);
273
+ if (recent.length > RECENT_CAP)
274
+ recent.shift();
275
+ persist();
276
+ const counted = state.total - state.noSchema;
277
+ console.log(`[DesignIntentTelemetry] bucket=${classification.bucket}` +
278
+ ` shape=${classification.shape ?? 'n/a'}` +
279
+ ` detail="${classification.detail}"` +
280
+ ` schema=${hasSchema ? 'yes' : 'no'}` +
281
+ ` counted=${counted}`);
282
+ if (counted > 0 && counted % REPORT_EVERY === 0) {
283
+ console.log(formatDesignIntentTelemetryReport(getDesignIntentTelemetrySnapshot()));
284
+ }
285
+ return classification;
286
+ }
287
+ /**
288
+ * Record the outcome of one Jev fallback dial. This closes the measurement
289
+ * loop: the bucketing tells us how big the candidate pool is, and this tells us
290
+ * how much of it Jev actually resolves — the number that earns the flip from
291
+ * `shadow` to `enforce`.
292
+ */
293
+ export function recordDesignIntentJevOutcome(outcome) {
294
+ state.jev.attempts += 1;
295
+ if (outcome.acted)
296
+ state.jev.acted += 1;
297
+ else
298
+ state.jev.fellThrough += 1;
299
+ const reason = outcome.reason || 'unknown';
300
+ state.jev.byReason[reason] = (state.jev.byReason[reason] ?? 0) + 1;
301
+ // Attribute the dial to the ask shape that produced it: this is what turns a
302
+ // reason count into a decision about the option strings or the confidence
303
+ // floor, since each shape puts a different question under load.
304
+ const shape = outcome.classification?.shape ?? 'unknown';
305
+ const slice = state.jev.byShape[shape] ?? emptyShapeBreakdown();
306
+ slice.attempts += 1;
307
+ if (outcome.acted)
308
+ slice.acted += 1;
309
+ else
310
+ slice.fellThrough += 1;
311
+ slice.byReason[reason] = (slice.byReason[reason] ?? 0) + 1;
312
+ state.jev.byShape[shape] = slice;
313
+ // Only the bounded-set-semantic bucket is ever dialed by design, so a dial on
314
+ // any other bucket is counted as the anomaly it is instead of being mixed in.
315
+ const bucket = outcome.classification?.bucket;
316
+ if (bucket && bucket !== JEV_CANDIDATE_BUCKET)
317
+ state.jev.offBucketDials += 1;
318
+ persist();
319
+ console.log(`[DesignIntentTelemetry] jev source=${outcome.source} acted=${outcome.acted} reason=${reason}` +
320
+ ` shape=${shape}` +
321
+ ` resolved=${state.jev.acted}/${state.jev.attempts}`);
322
+ }
323
+ export function getDesignIntentTelemetrySnapshot() {
324
+ const counted = state.total - state.noSchema;
325
+ const fractions = emptyBuckets();
326
+ for (const bucket of DESIGN_INTENT_BUCKETS) {
327
+ fractions[bucket] = counted > 0 ? state.buckets[bucket] / counted : 0;
328
+ }
329
+ const modelShare = counted > 0
330
+ ? (state.buckets.bounded_set_semantic + state.buckets.free_text + state.buckets.new_property) / counted
331
+ : 0;
332
+ return {
333
+ total: state.total,
334
+ counted,
335
+ noSchema: state.noSchema,
336
+ buckets: { ...state.buckets },
337
+ fractions,
338
+ deterministicShare: fractions.literal_arithmetic,
339
+ modelShare,
340
+ jev: {
341
+ attempts: state.jev.attempts,
342
+ acted: state.jev.acted,
343
+ fellThrough: state.jev.fellThrough,
344
+ resolvedShare: share(state.jev.acted, state.jev.attempts),
345
+ byReason: { ...state.jev.byReason },
346
+ byShape: {
347
+ property_named: withShare(state.jev.byShape.property_named),
348
+ value_only: withShare(state.jev.byShape.value_only),
349
+ unknown: withShare(state.jev.byShape.unknown)
350
+ },
351
+ offBucketDials: state.jev.offBucketDials
352
+ }
353
+ };
354
+ }
355
+ function share(part, whole) {
356
+ return whole > 0 ? part / whole : 0;
357
+ }
358
+ function withShare(breakdown) {
359
+ return {
360
+ attempts: breakdown.attempts,
361
+ acted: breakdown.acted,
362
+ fellThrough: breakdown.fellThrough,
363
+ resolvedShare: share(breakdown.acted, breakdown.attempts),
364
+ byReason: { ...breakdown.byReason }
365
+ };
366
+ }
367
+ function pct(n) {
368
+ return `${(n * 100).toFixed(1)}%`;
369
+ }
370
+ /**
371
+ * What each known reason implicates, so a count can be read as a decision rather
372
+ * than a number. The gateway owns this vocabulary; this is the report's mirror
373
+ * of it, and a reason the mirror does not know renders as unrecognised — visible,
374
+ * never silently given a guessed knob.
375
+ */
376
+ const JEV_REASON_LEVER = {
377
+ resolved: 'acted (the win)',
378
+ shadow: 'the rollout mode; enforce would act on this',
379
+ low_confidence: 'the confidence floor on the *property* question',
380
+ low_confidence_value: 'the confidence floor on the *value* question',
381
+ ambiguous_choice: 'the property option strings (two properties read alike)',
382
+ none_of_these: 'the property option strings (nothing matched the ask)',
383
+ unusable_property_answer: 'the question shape, not the option strings',
384
+ no_bounded_value: 'the roster: the chosen property has no closed value space',
385
+ unknown_property: 'internal: the plan and the schema disagreed',
386
+ no_value_stated: 'the value option strings (no bucket matched the ask)',
387
+ unusable_value_answer: 'the question shape, not the option strings',
388
+ no_color_baseline: 'not tunable: the current colour is unreadable',
389
+ no_numeric_baseline: 'not tunable: the current value is unreadable',
390
+ no_change: 'not tunable: the derived value already matched',
391
+ mode_off: 'the rollout mode, not the strings',
392
+ no_design_properties: 'the roster: the schema exposed nothing resolvable',
393
+ error: 'the gateway, not the questions',
394
+ timeout: 'the transport, not the questions',
395
+ circuit_open: 'the transport, not the questions',
396
+ no_response: 'the client could not reach the gateway'
397
+ };
398
+ /** Which question carried the ask, printed beside each shape's counters. */
399
+ const JEV_SHAPE_CONTEXT = {
400
+ property_named: 'the value question was under load',
401
+ value_only: 'the property question was under load',
402
+ unknown: 'unattributed'
403
+ };
404
+ function reasonLever(reason) {
405
+ const lever = JEV_REASON_LEVER[reason];
406
+ return lever ? ` — ${lever}` : ' — unrecognised reason';
407
+ }
408
+ /** `reason=count` pairs, most frequent first. */
409
+ function formatReasonCounts(byReason) {
410
+ return Object.entries(byReason)
411
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
412
+ .map(([reason, count]) => `${reason}=${count}`)
413
+ .join(', ');
414
+ }
415
+ export function formatDesignIntentTelemetryReport(snapshot) {
416
+ const lines = [
417
+ `[DesignIntentTelemetry] report — ${snapshot.counted} classified of ${snapshot.total} sends` +
418
+ ` (${snapshot.noSchema} without a design schema)`,
419
+ ` literal/arithmetic (deterministic): ${snapshot.buckets.literal_arithmetic} (${pct(snapshot.fractions.literal_arithmetic)})`,
420
+ ` bounded-set semantic (JEV-shaped): ${snapshot.buckets.bounded_set_semantic} (${pct(snapshot.fractions.bounded_set_semantic)})`,
421
+ ` free text (LLM): ${snapshot.buckets.free_text} (${pct(snapshot.fractions.free_text)})`,
422
+ ` new property (LLM): ${snapshot.buckets.new_property} (${pct(snapshot.fractions.new_property)})`,
423
+ ` not a design ask (codegen): ${snapshot.buckets.not_design} (${pct(snapshot.fractions.not_design)})`,
424
+ ` → could skip the LLM (deterministic share): ${pct(snapshot.deterministicShare)}`,
425
+ ` → design asks needing a model: ${pct(snapshot.modelShare)}`
426
+ ];
427
+ const { jev } = snapshot;
428
+ lines.push(` Jev fallback on the bounded-set-semantic bucket: ${jev.acted}/${jev.attempts} resolved` +
429
+ ` (${pct(jev.resolvedShare)}), ${jev.fellThrough} fell through`);
430
+ if (jev.attempts > 0) {
431
+ // The overall count comes first, then the same counts split by the ask
432
+ // shape that produced them — because "low_confidence=4" means a different
433
+ // thing when the property was named than when it was not.
434
+ lines.push(` reasons overall: ${formatReasonCounts(jev.byReason)}`);
435
+ lines.push(' by ask shape (which question carried the ask):');
436
+ for (const shape of JE_ASK_SHAPES) {
437
+ const slice = jev.byShape[shape];
438
+ if (slice.attempts === 0)
439
+ continue;
440
+ lines.push(` ${shape} — ${JEV_SHAPE_CONTEXT[shape]}: ${slice.acted}/${slice.attempts} resolved` +
441
+ ` (${pct(slice.resolvedShare)}), ${slice.fellThrough} fell through`);
442
+ for (const [reason, count] of Object.entries(slice.byReason).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))) {
443
+ lines.push(` ${reason}=${count}${reasonLever(reason)}`);
444
+ }
445
+ }
446
+ if (jev.offBucketDials > 0) {
447
+ lines.push(` ⚠ ${jev.offBucketDials} dials were attributed to another bucket —` +
448
+ ` only ${JEV_CANDIDATE_BUCKET} is ever dialed by design.`);
449
+ }
450
+ }
451
+ return lines.join('\n');
452
+ }
453
+ /** Recent events (last 100, newest last) — for a quick look in the console. */
454
+ export function getRecentDesignIntentEvents() {
455
+ return [...recent];
456
+ }
457
+ export function resetDesignIntentTelemetry() {
458
+ state = { total: 0, noSchema: 0, buckets: emptyBuckets(), jev: emptyJev() };
459
+ recent.length = 0;
460
+ persist();
461
+ }
462
+ // Dev/ops handle: inspect or reset from the console without a rebuild.
463
+ if (typeof window !== 'undefined') {
464
+ window.__widgeticDesignIntentTelemetry = {
465
+ snapshot: getDesignIntentTelemetrySnapshot,
466
+ report: () => {
467
+ const snapshot = getDesignIntentTelemetrySnapshot();
468
+ console.log(formatDesignIntentTelemetryReport(snapshot));
469
+ return snapshot;
470
+ },
471
+ recent: getRecentDesignIntentEvents,
472
+ reset: resetDesignIntentTelemetry
473
+ };
474
+ }
@@ -0,0 +1 @@
1
+ export {};