@usefidel/contracts 0.10.0 → 0.12.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.
@@ -6,14 +6,29 @@
6
6
  * (webapp, landing).
7
7
  *
8
8
  * WHY THIS IS A PACKAGE AND NOT A LOCAL FILE PER SURFACE.
9
- * The webapp and the extension each identified the same human under a
10
- * different PostHog `distinct_id` the webapp by Supabase UUID-or-email, the
11
- * extension by email-or-Figma-handle. `extension/src/lib/posthog.ts` carried a
12
- * comment asserting the two agreed. They did not. Nothing failed; retention
13
- * cohorts were simply wrong, and stayed wrong, because the two "authoritative"
14
- * definitions lived in two repositories with no gate between them.
9
+ * The webapp and the extension each wrote their own identity rule, and the two
10
+ * rules were written to agree. In the common case they did: both normally
11
+ * identified a person by email.
15
12
  *
16
- * The package is the only mechanism that spans both repositories.
13
+ * The defect was in what happened around that agreement.
14
+ *
15
+ * 1. EMAIL IS MUTABLE. A `distinct_id` must be stable for the life of the
16
+ * person, and an email address is not — changing it forks one human into
17
+ * two identities with no event connecting them.
18
+ * 2. THE FALLBACKS DIVERGED. Email is not always available, and each surface
19
+ * fell back into a DIFFERENT namespace when it was missing — a Supabase
20
+ * UUID on one side, a Figma handle on the other. So the same human was
21
+ * identified consistently right up until the moment they were not, which
22
+ * is the hardest version of this bug to see.
23
+ * 3. NOTHING GATED THE DUPLICATE. The rule was written out twice, in two
24
+ * repositories, with no check that the two copies still said the same
25
+ * thing. A drift would have been silent — and an earlier comment in
26
+ * `extension/src/lib/posthog.ts` asserting the two agreed is exactly the
27
+ * kind of claim a gate exists to replace.
28
+ *
29
+ * Nothing failed loudly. Retention cohorts were simply wrong, and stayed wrong.
30
+ * The package is the only mechanism that spans both repositories, so it is the
31
+ * only place the rule can be stated once.
17
32
  *
18
33
  * WHAT THIS OWNS: event NAMES and property TYPES.
19
34
  * WHAT IT DOES NOT OWN: SDK initialization, identity resolution, storage,
@@ -42,7 +57,21 @@ import { type RunErrorCode } from './run-errors.js';
42
57
  * history survives; the meaning of their events does not.
43
58
  */
44
59
  export declare const ANALYTICS_SCHEMA_VERSION: 2;
45
- /** Which client emitted the event. Server-side surfaces do not emit at all. */
60
+ /**
61
+ * Which PRODUCT JOURNEY the event belongs to — not which process emitted it.
62
+ *
63
+ * This comment used to read "server-side surfaces do not emit at all", and that
64
+ * stopped being true when authoritative terminal events moved into the
65
+ * `web-validate` finalizer. A validation started from the web app is a webapp
66
+ * validation whether the browser or the backend reports its outcome, so the
67
+ * backend's `web_validation_completed` correctly carries `surface: 'webapp'`.
68
+ *
69
+ * Making the backend its own surface would split one funnel in half: the
70
+ * submission would sit under `webapp` and its completion under something else,
71
+ * and no tile could relate them without a union. Which process emitted an event
72
+ * is answered by the event NAME and by `release`, both of which already
73
+ * distinguish the two.
74
+ */
46
75
  export type AnalyticsSurface = 'landing' | 'webapp' | 'extension';
47
76
  /** Which deployment emitted the event. Drives the default dashboard filter. */
48
77
  export type AnalyticsEnvironment = 'production' | 'staging' | 'development';
@@ -112,7 +141,7 @@ export declare function errorPhaseFor(code: string | null | undefined): Validati
112
141
  /** True when `code` is a member of the current taxonomy. */
113
142
  export declare function isKnownErrorCode(code: string | null | undefined): code is RunErrorCode;
114
143
  /** Events the GA dashboard is built on. Typed properties, specified triggers. */
115
- export declare const GA_EVENT_NAMES: readonly ["signup_started", "signup_completed", "onboarding_started", "onboarding_completed", "figma_connected", "environment_connected", "web_validation_submitted", "web_validation_completed", "web_validation_failed", "web_validation_cancelled", "report_viewed", "report_share_created", "report_link_copied", "report_share_failed", "report_copy_failed", "diff_mark_resolved", "upgrade_clicked", "checkout_started", "feedback_submitted", "extension_opened", "validation_start", "validation_accepted", "validation_complete", "validation_error", "validation_cancel", "review_saved", "figma_auth_success", "figma_auth_error", "extension_share_copied", "upgrade_modal_viewed", "upgrade_cta_clicked"];
144
+ export declare const GA_EVENT_NAMES: readonly ["signup_started", "signup_completed", "onboarding_started", "onboarding_completed", "figma_connected", "environment_connected", "web_validation_submitted", "web_validation_completed", "web_validation_failed", "validation_cancel_requested", "validation_result_observed", "report_viewed", "report_share_created", "report_link_copied", "report_share_failed", "report_copy_failed", "diff_mark_resolved", "upgrade_clicked", "checkout_started", "feedback_submitted", "extension_opened", "validation_start", "validation_accepted", "validation_complete", "validation_error", "validation_cancel", "review_saved", "figma_auth_success", "figma_auth_error", "extension_share_copied", "upgrade_modal_viewed", "upgrade_cta_clicked"];
116
145
  /**
117
146
  * Events that exist but are NOT GA metrics.
118
147
  *
@@ -125,18 +154,21 @@ export declare const GA_EVENT_NAMES: readonly ["signup_started", "signup_complet
125
154
  */
126
155
  export declare const SECONDARY_EVENT_NAMES: readonly ["checkout_completed", "console_errors", "diff_expanded", "drawer_filter_changed", "review_save_error", "review_verdict", "review_verdict_all", "screenshot_exported", "ui_crash", "validation_resume", "validation_retry", "view_changed", "auth_refresh", "consensus_complete", "consensus_error", "extension_validation_failed", "idempotency_dedup_client", "pipeline_complete", "pipeline_phase", "pipeline_resume", "rate_limit_hit", "upgrade_wall_hit"];
127
156
  /** Every event name any Fidel client may emit. */
128
- export declare const ANALYTICS_EVENT_NAMES: readonly ["signup_started", "signup_completed", "onboarding_started", "onboarding_completed", "figma_connected", "environment_connected", "web_validation_submitted", "web_validation_completed", "web_validation_failed", "web_validation_cancelled", "report_viewed", "report_share_created", "report_link_copied", "report_share_failed", "report_copy_failed", "diff_mark_resolved", "upgrade_clicked", "checkout_started", "feedback_submitted", "extension_opened", "validation_start", "validation_accepted", "validation_complete", "validation_error", "validation_cancel", "review_saved", "figma_auth_success", "figma_auth_error", "extension_share_copied", "upgrade_modal_viewed", "upgrade_cta_clicked", "checkout_completed", "console_errors", "diff_expanded", "drawer_filter_changed", "review_save_error", "review_verdict", "review_verdict_all", "screenshot_exported", "ui_crash", "validation_resume", "validation_retry", "view_changed", "auth_refresh", "consensus_complete", "consensus_error", "extension_validation_failed", "idempotency_dedup_client", "pipeline_complete", "pipeline_phase", "pipeline_resume", "rate_limit_hit", "upgrade_wall_hit"];
157
+ export declare const ANALYTICS_EVENT_NAMES: readonly ["signup_started", "signup_completed", "onboarding_started", "onboarding_completed", "figma_connected", "environment_connected", "web_validation_submitted", "web_validation_completed", "web_validation_failed", "validation_cancel_requested", "validation_result_observed", "report_viewed", "report_share_created", "report_link_copied", "report_share_failed", "report_copy_failed", "diff_mark_resolved", "upgrade_clicked", "checkout_started", "feedback_submitted", "extension_opened", "validation_start", "validation_accepted", "validation_complete", "validation_error", "validation_cancel", "review_saved", "figma_auth_success", "figma_auth_error", "extension_share_copied", "upgrade_modal_viewed", "upgrade_cta_clicked", "checkout_completed", "console_errors", "diff_expanded", "drawer_filter_changed", "review_save_error", "review_verdict", "review_verdict_all", "screenshot_exported", "ui_crash", "validation_resume", "validation_retry", "view_changed", "auth_refresh", "consensus_complete", "consensus_error", "extension_validation_failed", "idempotency_dedup_client", "pipeline_complete", "pipeline_phase", "pipeline_resume", "rate_limit_hit", "upgrade_wall_hit"];
129
158
  export type GaEventName = (typeof GA_EVENT_NAMES)[number];
130
159
  export type SecondaryEventName = (typeof SECONDARY_EVENT_NAMES)[number];
131
160
  export type AnalyticsEventName = GaEventName | SecondaryEventName;
132
161
  /**
133
- * Stamped on every schema-2 event by the wrapper, not by call sites.
162
+ * Stamped on every schema-2 event by the emitter, not by call sites — the
163
+ * browser wrapper for client events, `_shared/analytics.ts` for the backend
164
+ * terminal events.
134
165
  *
135
- * `team_id` and `is_internal` are optional because the extension cannot get
136
- * them without a network request it is not allowed to make: `team_id` lives in
137
- * `profiles`, not in the Supabase JWT. Team-level analysis uses
166
+ * `fidel_team_id` and `is_internal` are optional because the extension cannot
167
+ * get them without a network request it is not allowed to make: the team lives
168
+ * in `profiles`, not in the Supabase JWT. Team-level analysis uses
138
169
  * `validation_runs.team_id` in the database instead. The webapp has both and
139
- * always sends them.
170
+ * always sends them, and the backend reads both from the resolved team
171
+ * context it already loaded to run the subscription gate.
140
172
  *
141
173
  * There is no `user_id` field. The Supabase UUID is the `distinct_id`;
142
174
  * duplicating it as a property invites two sources of truth for identity.
@@ -147,7 +179,23 @@ export interface AnalyticsContext {
147
179
  surface: AnalyticsSurface;
148
180
  /** Git SHA (webapp/landing) or `manifest.json` version (extension). */
149
181
  release: string;
150
- team_id?: string;
182
+ /**
183
+ * The Fidel team, PREFIXED — and the prefix is not cosmetic.
184
+ *
185
+ * `team_id` is a reserved top-level column on PostHog's own events table (it
186
+ * holds the PostHog project id). A property of that name is still stored in
187
+ * the payload, but `properties.team_id` resolves to the reserved column and
188
+ * returns NULL, so every dashboard filter and breakdown built on it silently
189
+ * matches nothing. Measured on staging 2026-09-08:
190
+ *
191
+ * JSONExtractString(properties,'team_id') -> b34bb03c-… (the real value)
192
+ * properties.team_id -> NULL
193
+ * team_id -> 330851 (PostHog's project)
194
+ *
195
+ * That is the worst failure shape available: instrumented, ingested, and
196
+ * invisible. `is_internal` does not collide and keeps its plain name.
197
+ */
198
+ fidel_team_id?: string;
151
199
  is_internal?: boolean;
152
200
  }
153
201
  /**
@@ -179,7 +227,11 @@ export interface ValidationEventProperties {
179
227
  * upgrade wall still counted as started and no event could carry a `run_id`.
180
228
  * This one cannot be emitted without one.
181
229
  *
182
- * NOT a terminal event: exactly one of completed / failed / cancelled follows.
230
+ * NOT a terminal event. Emitted by the BROWSER, because acceptance is the
231
+ * browser's own observation: it made the request and read the response.
232
+ *
233
+ * Its terminal counterpart is emitted by the SERVER — see
234
+ * `WebValidationCompletedProperties` for why ownership splits here.
183
235
  */
184
236
  export interface ValidationSubmittedProperties extends ValidationEventProperties {
185
237
  /**
@@ -215,6 +267,30 @@ export interface ReportShareFailureProperties {
215
267
  */
216
268
  error_code: string;
217
269
  }
270
+ /**
271
+ * An accepted run reached a terminal status. EMITTED BY THE BACKEND FINALIZER,
272
+ * immediately after the `validation_runs` status update succeeds.
273
+ *
274
+ * A browser cannot own this event. Validation is asynchronous: the edge
275
+ * function answers 202 and finishes the run in a background task, so the
276
+ * transition happens in a process the tab has no part in. A browser-emitted
277
+ * completion is really "the user was still watching when it landed", and a
278
+ * completion rate built on it has that as its hidden denominator. Measured on
279
+ * staging: three accepted runs, three `web_validation_submitted`, zero
280
+ * completions — including one on the fixed client, because the tab moved on
281
+ * before the run finished.
282
+ *
283
+ * The emit is strictly ordered after the status write and is skipped when that
284
+ * write fails, so this event cannot claim a completion the database does not
285
+ * have. Analytics failure never changes the validation result: a failed capture
286
+ * is logged and dropped.
287
+ *
288
+ * The server uses the SAME Supabase user UUID as `distinct_id` that the browser
289
+ * uses. Anything else forks the person.
290
+ *
291
+ * DEDUPLICATE BY DISTINCT `run_id`. Never count raw event rows: a retry inside
292
+ * the finalizer, or a replayed background task, can emit twice for one run.
293
+ */
218
294
  export interface WebValidationCompletedProperties extends ValidationEventProperties {
219
295
  duration_ms: number;
220
296
  /**
@@ -237,7 +313,17 @@ export interface WebValidationCompletedProperties extends ValidationEventPropert
237
313
  result_completeness?: ResultCompleteness;
238
314
  }
239
315
  /**
240
- * A run that failed or was cancelled.
316
+ * A validation failed.
317
+ *
318
+ * OWNERSHIP SPLITS ON `run_id`, and the nullability is what carries it:
319
+ * - `run_id === null` — rejected BEFORE the backend accepted it (upgrade
320
+ * wall, rate limit, invalid URL). Only the browser saw this; there is no
321
+ * run and no server process to emit from. Browser-emitted.
322
+ * - `run_id !== null` — an accepted run reached `status = 'error'`.
323
+ * Backend-emitted, immediately after that status write succeeds, for the
324
+ * same reason `web_validation_completed` is.
325
+ *
326
+ * DEDUPLICATE BY DISTINCT `run_id` for the accepted half.
241
327
  *
242
328
  * `run_id` is NULLABLE here and required on the success events, and the
243
329
  * asymmetry is the point: a run rejected BEFORE the backend accepted it — the
@@ -254,12 +340,51 @@ export interface ValidationFailedProperties {
254
340
  error_phase: ValidationErrorPhase;
255
341
  retryable: boolean;
256
342
  }
257
- export interface ValidationCancelledProperties {
343
+ /**
344
+ * The user asked to stop, and the browser aborted its in-flight request.
345
+ *
346
+ * This is a REQUEST, not an outcome, and the name says so. `validation_runs`
347
+ * has no `cancelled` status — its check constraint allows only `complete`,
348
+ * `error`, `partial` and `processing` — so no accepted run can ever persist as
349
+ * cancelled. Aborting the client fetch does not stop the backend: the edge
350
+ * function answers 202 and finishes the run in a background task, so a run the
351
+ * user "cancelled" usually goes on to complete and emits
352
+ * `web_validation_completed` from the server a moment later.
353
+ *
354
+ * That is not a contradiction to reconcile away. The two events describe
355
+ * different things: this one is the user's intent, the server's is the run's
356
+ * fate. Counting this as a terminal outcome would under-count completions and
357
+ * invent a state the database cannot hold — which is exactly what the
358
+ * predecessor name `web_validation_cancelled` implied.
359
+ *
360
+ * `run_id` is null whenever the abort landed before the server replied.
361
+ */
362
+ export interface ValidationCancelRequestedProperties {
258
363
  run_id: string | null;
259
364
  validation_mode: ValidationMode;
260
365
  duration_ms: number;
261
366
  error_phase: ValidationErrorPhase;
262
367
  }
368
+ /**
369
+ * The user was still present when the result rendered.
370
+ *
371
+ * Behavioural, never operational. Completion is owned by the backend
372
+ * finalizer; this records only whether anyone was watching. Keeping the two
373
+ * apart is what makes both measurable:
374
+ *
375
+ * accepted-run completion rate = server terminal events / submitted
376
+ * result observation rate = this / completed runs
377
+ *
378
+ * Fusing them — which is what a browser-emitted `completed` did — yields a
379
+ * completion rate whose real denominator is "users who stayed on the page",
380
+ * silently floored by however long the pipeline takes.
381
+ */
382
+ export interface ValidationResultObservedProperties extends ValidationEventProperties {
383
+ /** Terminal status the client actually rendered. */
384
+ run_status: 'complete' | 'error' | 'partial';
385
+ /** ms from submit to the client rendering a terminal state. */
386
+ observed_after_ms: number;
387
+ }
263
388
  export interface ReportViewedProperties {
264
389
  run_id: string;
265
390
  validation_mode: ValidationMode;
@@ -322,7 +447,8 @@ export interface AnalyticsEventProperties {
322
447
  web_validation_submitted: ValidationSubmittedProperties;
323
448
  web_validation_completed: WebValidationCompletedProperties;
324
449
  web_validation_failed: ValidationFailedProperties;
325
- web_validation_cancelled: ValidationCancelledProperties;
450
+ validation_cancel_requested: ValidationCancelRequestedProperties;
451
+ validation_result_observed: ValidationResultObservedProperties;
326
452
  report_viewed: ReportViewedProperties;
327
453
  report_share_created: ReportShareProperties;
328
454
  report_link_copied: ReportShareProperties;
@@ -352,7 +478,7 @@ export interface AnalyticsEventProperties {
352
478
  validation_accepted: ValidationSubmittedProperties;
353
479
  validation_complete: WebValidationCompletedProperties;
354
480
  validation_error: ValidationFailedProperties;
355
- validation_cancel: ValidationCancelledProperties;
481
+ validation_cancel: ValidationCancelRequestedProperties;
356
482
  review_saved: {
357
483
  run_id?: string;
358
484
  review_action: string;
package/dist/analytics.js CHANGED
@@ -6,14 +6,29 @@
6
6
  * (webapp, landing).
7
7
  *
8
8
  * WHY THIS IS A PACKAGE AND NOT A LOCAL FILE PER SURFACE.
9
- * The webapp and the extension each identified the same human under a
10
- * different PostHog `distinct_id` the webapp by Supabase UUID-or-email, the
11
- * extension by email-or-Figma-handle. `extension/src/lib/posthog.ts` carried a
12
- * comment asserting the two agreed. They did not. Nothing failed; retention
13
- * cohorts were simply wrong, and stayed wrong, because the two "authoritative"
14
- * definitions lived in two repositories with no gate between them.
9
+ * The webapp and the extension each wrote their own identity rule, and the two
10
+ * rules were written to agree. In the common case they did: both normally
11
+ * identified a person by email.
15
12
  *
16
- * The package is the only mechanism that spans both repositories.
13
+ * The defect was in what happened around that agreement.
14
+ *
15
+ * 1. EMAIL IS MUTABLE. A `distinct_id` must be stable for the life of the
16
+ * person, and an email address is not — changing it forks one human into
17
+ * two identities with no event connecting them.
18
+ * 2. THE FALLBACKS DIVERGED. Email is not always available, and each surface
19
+ * fell back into a DIFFERENT namespace when it was missing — a Supabase
20
+ * UUID on one side, a Figma handle on the other. So the same human was
21
+ * identified consistently right up until the moment they were not, which
22
+ * is the hardest version of this bug to see.
23
+ * 3. NOTHING GATED THE DUPLICATE. The rule was written out twice, in two
24
+ * repositories, with no check that the two copies still said the same
25
+ * thing. A drift would have been silent — and an earlier comment in
26
+ * `extension/src/lib/posthog.ts` asserting the two agreed is exactly the
27
+ * kind of claim a gate exists to replace.
28
+ *
29
+ * Nothing failed loudly. Retention cohorts were simply wrong, and stayed wrong.
30
+ * The package is the only mechanism that spans both repositories, so it is the
31
+ * only place the rule can be stated once.
17
32
  *
18
33
  * WHAT THIS OWNS: event NAMES and property TYPES.
19
34
  * WHAT IT DOES NOT OWN: SDK initialization, identity resolution, storage,
@@ -119,7 +134,8 @@ export const GA_EVENT_NAMES = [
119
134
  'web_validation_submitted',
120
135
  'web_validation_completed',
121
136
  'web_validation_failed',
122
- 'web_validation_cancelled',
137
+ 'validation_cancel_requested',
138
+ 'validation_result_observed',
123
139
  'report_viewed',
124
140
  'report_share_created',
125
141
  'report_link_copied',
package/dist/index.d.ts CHANGED
@@ -2,4 +2,4 @@ export { RUN_ERROR_CODES, ERROR_CODE_META, mapSnapshotErrorToCode, mapFigmaError
2
2
  export type { RunErrorCode, RunErrorMeta, ErrorMapping, RunDisplay, } from './run-errors.js';
3
3
  export type { IntakeFramework, UnsupportedStack, UnsupportedReason, UnresolvedReferenceSection, UnresolvedReferenceReason, UnresolvedReference, ParsedThemeColor, ParsedThemeTypography, ParsedThemeSize, ShadcnConfig, ParsedTheme, IntakeDetection, TokenSourceKind, TokenSource, PackageRole, PackageInfo, } from './theme-intake.js';
4
4
  export { ANALYTICS_SCHEMA_VERSION, ANALYTICS_EVENT_NAMES, GA_EVENT_NAMES, SECONDARY_EVENT_NAMES, ERROR_CODE_PHASE, FORBIDDEN_PROPERTY_KEYS, errorPhaseFor, isKnownErrorCode, sanitizeProperties, } from './analytics.js';
5
- export type { AnalyticsSurface, AnalyticsEnvironment, AnalyticsContext, AnalyticsPersonProperties, AnalyticsEventName, GaEventName, SecondaryEventName, AnalyticsEventProperties, AnalyticsPayload, ValidationMode, ValidationErrorPhase, ResultCompleteness, ValidationEventProperties, ValidationSubmittedProperties, ReportShareProperties, ReportShareFailureProperties, WebValidationCompletedProperties, ValidationFailedProperties, ValidationCancelledProperties, ReportViewedProperties, } from './analytics.js';
5
+ export type { AnalyticsSurface, AnalyticsEnvironment, AnalyticsContext, AnalyticsPersonProperties, AnalyticsEventName, GaEventName, SecondaryEventName, AnalyticsEventProperties, AnalyticsPayload, ValidationMode, ValidationErrorPhase, ResultCompleteness, ValidationEventProperties, ValidationSubmittedProperties, ReportShareProperties, ReportShareFailureProperties, WebValidationCompletedProperties, ValidationFailedProperties, ValidationCancelRequestedProperties, ValidationResultObservedProperties, ReportViewedProperties, } from './analytics.js';
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "The source of truth still lives in the monorepo at packages/contracts/."
8
8
  ],
9
9
  "name": "@usefidel/contracts",
10
- "version": "0.10.0",
10
+ "version": "0.12.0",
11
11
  "description": "Shared, code-free contracts between Fidel surfaces. Run-error taxonomy, theme-intake wire types, and the canonical fidel.config.json builders.",
12
12
  "license": "UNLICENSED",
13
13
  "private": false,