@devfellowship/components 3.0.0 → 3.0.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.
Files changed (2) hide show
  1. package/dist/cli.js +416 -18
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -47,26 +47,424 @@ import Ajv2020 from "ajv/dist/2020.js";
47
47
  import addFormats from "ajv-formats";
48
48
  import chalk2 from "chalk";
49
49
 
50
+ // src/cli/ux-paths/lib/v1.schema.json
51
+ var v1_schema_default = {
52
+ $schema: "https://json-schema.org/draft/2020-12/schema",
53
+ $id: "https://raw.githubusercontent.com/devfellowship/dfl-ux-paths/main/schema/v1.json",
54
+ title: "DFL UX Paths v1",
55
+ description: "Versioned per-app user-flow JSON schema for migration-gap analysis and dead-code detection. Accepts schema_version 1.0.0, 1.1.0, 1.2.0 and 1.3.0 (all additive \u2014 v1.1 adds optional navigation/prereq/test_metadata fields; v1.2 adds optional source_ref (screen\u2192repo file) and screenshots[] (per platform\xD7orientation), and documents `route` as the canonical shared route nomenclature for 1:1 cross-app mapping; v1.3 adds optional source (build|spec), phases[], and phase/description on screen and flow, so the format can also describe a product that has not been built yet. Backward compatible \u2014 v1.0/v1.1/v1.2 files still validate).",
56
+ $comment: "schema v1.2 \u2014 additive over v1.1. New optional screen fields: source_ref { component_file, file_path_chain? } links a screen to the repo file(s) that render it (Figma-Code-Connect style); screenshots[] { platform, orientation, viewport?, url, captured_at? } captures rendered images per platform\xD7orientation for vision-first comparison. `screen.id` is the STICKY 1:1 JOIN KEY across apps \u2014 comparators pair screens by shared id. `route` is the canonical shared route nomenclature (e.g. studio/project/:id/editor) letting two apps map 1:1 by the same name even when their real URLs differ. Draft 2020-12. The file `$id` is intentionally unchanged across additive bumps so consumers pinned to the canonical URL keep working. || schema v1.3 \u2014 additive over v1.2, and the first bump aimed at files authored BEFORE the code exists. `source: \"spec\"` says so explicitly: routes are proposed nomenclature, app_version carries the zero-sha `init` emits, and the absence of source_ref/screenshots/base_url is correct rather than incomplete. `phases[]` declares the app's delivery-phase vocabulary once; `screen.phase` / `flow.phase` reference a phase id. That pair is what lets a viewer render committed scope differently from work the requirement explicitly parks (`phases[].optional: true`) instead of showing one undifferentiated blob \u2014 the distinction a client reads as 'what am I actually getting'. `screen.description` / `flow.description` carry the prose a spec needs and a generated-from-code file usually does not.",
57
+ type: "object",
58
+ required: ["schema_version", "app_id", "app_version", "screens", "flows"],
59
+ additionalProperties: false,
60
+ properties: {
61
+ schema_version: {
62
+ type: "string",
63
+ enum: ["1.0.0", "1.1.0", "1.2.0", "1.3.0"],
64
+ description: "Version of the dfl-ux-paths schema this file conforms to. v1.1.0 adds optional navigation_path, prerequisites, test_metadata, and richer flow step objects. v1.2.0 adds optional screen.source_ref (screen\u2192repo file link) and screen.screenshots[] (per platform\xD7orientation), and documents `route` as the canonical shared route nomenclature for 1:1 cross-app mapping. v1.3.0 adds optional spec-first authoring fields: top-level `source` (build|spec) and `phases[]`, plus optional `phase`/`description` on screens and flows."
65
+ },
66
+ app_id: {
67
+ type: "string",
68
+ minLength: 1,
69
+ description: "Stable identifier for the app (e.g. 'dfl-learn-mobile')."
70
+ },
71
+ app_version: {
72
+ type: "string",
73
+ pattern: "^\\d{4}-\\d{2}-\\d{2}-[0-9a-f]{4,40}$",
74
+ description: 'App build identifier in the form YYYY-MM-DD-<git-sha-short>. A file authored before any build exists uses the zero-sha form `YYYY-MM-DD-0000000` that `dfl-ux-paths init` already emits, and should also set `source: "spec"`.'
75
+ },
76
+ source: {
77
+ type: "string",
78
+ enum: ["build", "spec"],
79
+ description: "v1.3 \u2014 provenance of this file. 'build' (the assumed default when omitted) means it was derived from a shipped build, so `route` values are real and source_ref/screenshots are meaningful. 'spec' means it was authored from a requirement document before the code exists: routes are PROPOSED nomenclature, app_version carries a zero-sha, and the absence of source_ref/screenshots/test_metadata.base_url is expected rather than a gap. Optional, additive."
80
+ },
81
+ phases: {
82
+ type: "array",
83
+ items: { $ref: "#/$defs/phase" },
84
+ description: "v1.3 \u2014 the app's delivery-phase vocabulary, declared once and referenced by `screen.phase` / `flow.phase`. Phase ids are free-form on purpose (every product names its phases differently). Mark a phase `optional: true` when the requirement explicitly parks it outside committed scope. Optional, additive."
85
+ },
86
+ generated_at: {
87
+ type: "string",
88
+ format: "date-time",
89
+ description: "ISO-8601 timestamp when this snapshot was generated."
90
+ },
91
+ tech_stack: {
92
+ type: "array",
93
+ items: { type: "string" },
94
+ description: "List of tech-stack tags (e.g. 'react-native', 'expo', 'powersync')."
95
+ },
96
+ test_metadata: {
97
+ $ref: "#/$defs/testMetadata",
98
+ description: "v1.1 \u2014 app-level metadata used by e2e-user-persona / Playwright skills to bootstrap a session (auth strategy, default viewport, base URL)."
99
+ },
100
+ screens: {
101
+ type: "array",
102
+ items: { $ref: "#/$defs/screen" }
103
+ },
104
+ flows: {
105
+ type: "array",
106
+ items: { $ref: "#/$defs/flow" }
107
+ },
108
+ dead_code: {
109
+ type: "array",
110
+ items: { $ref: "#/$defs/deadCodeEntry" }
111
+ }
112
+ },
113
+ $defs: {
114
+ phase: {
115
+ type: "object",
116
+ required: ["id"],
117
+ additionalProperties: false,
118
+ description: "v1.3 \u2014 one entry in the app's delivery-phase vocabulary.",
119
+ properties: {
120
+ id: {
121
+ type: "string",
122
+ minLength: 1,
123
+ description: "Stable phase id referenced by `screen.phase` / `flow.phase` (e.g. 'alpha', 'beta', 'mvp', 'optional')."
124
+ },
125
+ label: {
126
+ type: "string",
127
+ description: "Human-readable phase name."
128
+ },
129
+ description: {
130
+ type: "string",
131
+ description: "What this phase covers \u2014 ideally quoting the source requirement rather than paraphrasing it."
132
+ },
133
+ optional: {
134
+ type: "boolean",
135
+ description: "True when the requirement explicitly parks this phase outside committed scope (e.g. 'eventual landing, not in MVP'). Viewers and generated Mermaid MUST render it as visually distinct from in-scope work, so a reader never mistakes parked work for a commitment."
136
+ }
137
+ }
138
+ },
139
+ screen: {
140
+ type: "object",
141
+ required: ["id", "name"],
142
+ additionalProperties: false,
143
+ properties: {
144
+ id: {
145
+ type: "string",
146
+ minLength: 1,
147
+ description: "Stable screen identifier (snake_case recommended). REQUIRED. This is the STICKY 1:1 JOIN KEY across apps: cross-app comparators (web \u2194 mobile, old build \u2194 new build) pair screens by shared `id`. Treat it as a contract \u2014 keep ids stable across versions and identical across apps you intend to map 1:1, or diffs/parity comparisons break."
148
+ },
149
+ name: {
150
+ type: "string",
151
+ minLength: 1,
152
+ description: "Human-readable screen name."
153
+ },
154
+ route: {
155
+ type: "string",
156
+ description: "v1.2 \u2014 canonical SHARED route nomenclature for 1:1 cross-app mapping (e.g. 'studio/project/:id/editor'). Lets two apps (web \u2194 mobile) map by the same route name even when their real URLs/deep-links differ. Also doubles as the screen's route/path/deep-link when the app uses a single URL scheme. Use ':param' for dynamic segments. Optional, additive."
157
+ },
158
+ description: {
159
+ type: "string",
160
+ description: "v1.3 \u2014 free-form prose describing this screen. For a spec-first file the description IS the requirement, so keep it faithful to the source document. Optional, additive."
161
+ },
162
+ phase: {
163
+ type: "string",
164
+ minLength: 1,
165
+ description: "v1.3 \u2014 id of the phase (from top-level `phases[]`) this screen belongs to. Optional, additive."
166
+ },
167
+ source_ref: {
168
+ $ref: "#/$defs/sourceRef",
169
+ description: "v1.2 \u2014 link from this screen to the repo file(s) that render it (route/page/modal component). Figma-Code-Connect style. Optional, additive \u2014 populated by the route\u2192file resolver (bin/resolve-routes.mjs) or by hand."
170
+ },
171
+ screenshots: {
172
+ type: "array",
173
+ items: { $ref: "#/$defs/screenshot" },
174
+ description: "v1.2 \u2014 rendered screenshots of this screen, one entry per platform\xD7orientation(\xD7viewport). URLs are committed (e.g. S3 via supabase-upload) so humans AND agents view the same image for vision-first comparison. Optional, additive."
175
+ },
176
+ components: {
177
+ type: "array",
178
+ items: { type: "string" },
179
+ description: "Component identifiers rendered on this screen."
180
+ },
181
+ api_calls: {
182
+ type: "array",
183
+ items: { type: "string" },
184
+ description: "API endpoints or RPC calls invoked from this screen."
185
+ },
186
+ actions: {
187
+ type: "array",
188
+ items: { $ref: "#/$defs/action" }
189
+ },
190
+ navigation_path: {
191
+ type: "array",
192
+ items: { $ref: "#/$defs/navigationStep" },
193
+ description: "v1.1 \u2014 ordered list of UI actions to navigate TO this screen from the app entrypoint. Used by e2e-user-persona to auto-generate persona scripts instead of LLM-driven discovery. When multiple navigationStep entries are present, the runner tries them in order and the first matching selector wins (useful for resilience across renames)."
194
+ },
195
+ prerequisites: {
196
+ $ref: "#/$defs/prerequisites",
197
+ description: "v1.1 \u2014 preconditions that must hold for this screen to be reachable / functional."
198
+ }
199
+ }
200
+ },
201
+ sourceRef: {
202
+ type: "object",
203
+ required: ["component_file"],
204
+ additionalProperties: false,
205
+ description: "v1.2 \u2014 points a screen at the repo file(s) that render it.",
206
+ properties: {
207
+ component_file: {
208
+ type: "string",
209
+ minLength: 1,
210
+ description: "Repo-relative path to the route/page/modal component that renders this screen (e.g. 'app/studio/[id]/editor.tsx' for Expo Router, 'app/studio/[id]/editor/page.tsx' for Next app-router). This is what the route\u2192file resolver emits."
211
+ },
212
+ file_path_chain: {
213
+ type: "array",
214
+ items: { type: "string" },
215
+ minItems: 1,
216
+ description: "Optional \u2014 ordered repo-relative paths from the app entry (e.g. 'App.tsx' / 'app/_layout.tsx') down to the component, giving the 'tree' from app root to this screen's renderer."
217
+ }
218
+ }
219
+ },
220
+ screenshot: {
221
+ type: "object",
222
+ required: ["platform", "orientation", "url"],
223
+ additionalProperties: false,
224
+ description: "v1.2 \u2014 one rendered screenshot of a screen, keyed by platform\xD7orientation(\xD7viewport).",
225
+ properties: {
226
+ platform: {
227
+ type: "string",
228
+ enum: ["web", "mobile"],
229
+ description: "Rendering platform the screenshot was captured on."
230
+ },
231
+ orientation: {
232
+ type: "string",
233
+ enum: ["portrait", "landscape"],
234
+ description: "Device/viewport orientation."
235
+ },
236
+ viewport: {
237
+ type: "string",
238
+ description: "Optional viewport dimensions string, e.g. '375x812' or '1440x900'."
239
+ },
240
+ url: {
241
+ type: "string",
242
+ format: "uri",
243
+ description: "Public URL of the committed screenshot (e.g. S3 via supabase-upload)."
244
+ },
245
+ captured_at: {
246
+ type: "string",
247
+ format: "date-time",
248
+ description: "Optional ISO-8601 timestamp when the screenshot was captured."
249
+ }
250
+ }
251
+ },
252
+ action: {
253
+ type: "object",
254
+ required: ["id", "label"],
255
+ additionalProperties: false,
256
+ properties: {
257
+ id: {
258
+ type: "string",
259
+ minLength: 1
260
+ },
261
+ label: {
262
+ type: "string",
263
+ minLength: 1
264
+ },
265
+ next_screen: {
266
+ type: "string",
267
+ description: "Screen id the user lands on after this action."
268
+ },
269
+ side_effect: {
270
+ type: "string",
271
+ description: "Free-form description of side effect (mutation, analytics, etc.)."
272
+ }
273
+ }
274
+ },
275
+ flow: {
276
+ type: "object",
277
+ required: ["name", "start", "steps"],
278
+ additionalProperties: false,
279
+ properties: {
280
+ name: {
281
+ type: "string",
282
+ minLength: 1
283
+ },
284
+ description: {
285
+ type: "string",
286
+ description: "v1.3 \u2014 free-form prose describing what this flow accomplishes and why it matters. Optional, additive."
287
+ },
288
+ phase: {
289
+ type: "string",
290
+ minLength: 1,
291
+ description: "v1.3 \u2014 id of the phase (from top-level `phases[]`) this flow belongs to. Optional, additive."
292
+ },
293
+ start: {
294
+ type: "string",
295
+ minLength: 1,
296
+ description: "Screen id where the flow starts."
297
+ },
298
+ steps: {
299
+ type: "array",
300
+ items: {
301
+ oneOf: [
302
+ {
303
+ type: "string",
304
+ description: "v1.0 \u2014 screen id traversed in this flow."
305
+ },
306
+ {
307
+ $ref: "#/$defs/flowStepObject"
308
+ }
309
+ ]
310
+ },
311
+ description: "Ordered list of flow steps. v1.0 accepts plain screen ids (strings); v1.1 additionally accepts step objects with action/selector/target_screen for executable scripts. Mixed arrays are allowed during migration."
312
+ },
313
+ actions: {
314
+ type: "array",
315
+ items: { type: "string" },
316
+ description: "Ordered list of action ids exercised during this flow."
317
+ },
318
+ tested_by: {
319
+ type: "array",
320
+ items: { type: "string" },
321
+ description: "References to tests (file paths, test names) that cover this flow."
322
+ }
323
+ }
324
+ },
325
+ flowStepObject: {
326
+ type: "object",
327
+ required: ["screen"],
328
+ additionalProperties: false,
329
+ properties: {
330
+ screen: {
331
+ type: "string",
332
+ minLength: 1,
333
+ description: "Screen id where this step is executed."
334
+ },
335
+ action: {
336
+ type: "string",
337
+ description: "Action verb (tap, type, scroll, swipe, navigate, wait, assert, etc.)."
338
+ },
339
+ selector: {
340
+ type: "string",
341
+ description: "Playwright-compatible selector (e.g. 'text=Next', 'role=button[name=Save]', 'css=[data-testid=foo]')."
342
+ },
343
+ value: {
344
+ type: "string",
345
+ description: "Optional value to type / select / assert."
346
+ },
347
+ target_screen: {
348
+ type: "string",
349
+ description: "Screen id the user lands on after this step."
350
+ },
351
+ wait_for_target: {
352
+ type: "string",
353
+ description: "Selector / condition to wait for to confirm the target screen rendered."
354
+ },
355
+ note: {
356
+ type: "string",
357
+ description: "Free-form annotation for humans / debugging."
358
+ }
359
+ }
360
+ },
361
+ navigationStep: {
362
+ type: "object",
363
+ required: ["selector", "action"],
364
+ additionalProperties: false,
365
+ properties: {
366
+ selector: {
367
+ type: "string",
368
+ minLength: 1,
369
+ description: "Playwright-compatible selector (e.g. 'text=Studio', 'role=tab[name=Studio]', 'css=[data-testid=studio-tab]')."
370
+ },
371
+ action: {
372
+ type: "string",
373
+ enum: ["tap", "click", "type", "scroll", "swipe", "navigate", "wait", "assert"],
374
+ description: "Interaction primitive to execute."
375
+ },
376
+ value: {
377
+ type: "string",
378
+ description: "Optional value for type/select/assert."
379
+ },
380
+ wait_for: {
381
+ type: "string",
382
+ description: "Selector / condition to wait for after performing the action to confirm success."
383
+ },
384
+ note: {
385
+ type: "string",
386
+ description: "Free-form annotation."
387
+ }
388
+ }
389
+ },
390
+ prerequisites: {
391
+ type: "object",
392
+ additionalProperties: false,
393
+ properties: {
394
+ auth_required: {
395
+ type: "boolean",
396
+ description: "True if a logged-in user session is required to reach this screen."
397
+ },
398
+ auth_role: {
399
+ type: "string",
400
+ description: "Optional \u2014 specific role required (e.g. 'admin', 'instructor'). Omit for any authenticated user."
401
+ },
402
+ data_required: {
403
+ type: "array",
404
+ items: { type: "string" },
405
+ description: "Free-form descriptions of DB/state preconditions (e.g. 'lesson_studio.projects has >= 0 rows')."
406
+ },
407
+ preconditions_in_app: {
408
+ type: "array",
409
+ items: { type: "string" },
410
+ description: "Free-form descriptions of in-app preconditions (feature flag enabled, onboarding complete, etc.)."
411
+ },
412
+ feature_flags: {
413
+ type: "array",
414
+ items: { type: "string" },
415
+ description: "Feature flag identifiers that must be enabled."
416
+ }
417
+ }
418
+ },
419
+ testMetadata: {
420
+ type: "object",
421
+ additionalProperties: false,
422
+ properties: {
423
+ auth_strategy: {
424
+ type: "string",
425
+ description: "Brief description of how to authenticate (e.g. 'supabase-anon + CLAUDE_SMOKE creds', 'magic-link', 'oauth google')."
426
+ },
427
+ default_viewport: {
428
+ type: "string",
429
+ enum: ["mobile", "tablet", "desktop", "responsive"],
430
+ description: "Hint for test runners \u2014 preferred viewport for this app."
431
+ },
432
+ base_url: {
433
+ type: "string",
434
+ format: "uri",
435
+ description: "Production / smoke-target base URL (e.g. 'https://mobile.devfellowship.com/')."
436
+ },
437
+ test_user_secret_path: {
438
+ type: "string",
439
+ description: "Infisical path to credentials suitable for smoke tests (e.g. '/shared/CLAUDE_SMOKE_USERNAME')."
440
+ },
441
+ notes: {
442
+ type: "string",
443
+ description: "Free-form notes for test authors / persona-script generators."
444
+ }
445
+ }
446
+ },
447
+ deadCodeEntry: {
448
+ type: "object",
449
+ required: ["component", "reason"],
450
+ additionalProperties: false,
451
+ properties: {
452
+ component: {
453
+ type: "string",
454
+ minLength: 1
455
+ },
456
+ reason: {
457
+ type: "string",
458
+ minLength: 1
459
+ }
460
+ }
461
+ }
462
+ }
463
+ };
464
+
50
465
  // src/cli/ux-paths/lib/load-schema.ts
51
- var SCHEMA_URL = "https://raw.githubusercontent.com/devfellowship/dfl-ux-paths/main/schema/v1.json";
52
- var cached = null;
53
466
  async function loadSchemaV1() {
54
- if (cached) return cached;
55
- let res;
56
- try {
57
- res = await fetch(SCHEMA_URL);
58
- } catch (err) {
59
- throw new Error(
60
- `Unable to fetch the UX Paths schema from ${SCHEMA_URL}: ${err.message}`
61
- );
62
- }
63
- if (!res.ok) {
64
- throw new Error(
65
- `Unable to fetch the UX Paths schema from ${SCHEMA_URL}: HTTP ${res.status} ${res.statusText}`
66
- );
67
- }
68
- cached = await res.json();
69
- return cached;
467
+ return v1_schema_default;
70
468
  }
71
469
 
72
470
  // src/cli/ux-paths/commands/validate.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devfellowship/components",
3
- "version": "3.0.0",
3
+ "version": "3.0.1",
4
4
  "description": "DFL Design System — UI components, hooks, utils and providers",
5
5
  "type": "module",
6
6
  "sideEffects": [