@loj-lang/cli 0.5.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.
Files changed (36) hide show
  1. package/README.md +87 -0
  2. package/agent-assets/loj-authoring/SKILL.md +179 -0
  3. package/agent-assets/loj-authoring/agents/openai.yaml +3 -0
  4. package/agent-assets/loj-authoring/metadata.json +6 -0
  5. package/agent-assets/loj-authoring/references/backend-family.md +340 -0
  6. package/agent-assets/loj-authoring/references/backend-targets.md +171 -0
  7. package/agent-assets/loj-authoring/references/frontend-family.md +794 -0
  8. package/agent-assets/loj-authoring/references/frontend-runtime-trace.md +204 -0
  9. package/agent-assets/loj-authoring/references/policy-rules-proof.md +178 -0
  10. package/agent-assets/loj-authoring/references/project-and-transport.md +454 -0
  11. package/agent-assets/loj-authoring/references/workflow-flow-proof.md +263 -0
  12. package/dist/database-native-sql.d.ts +4 -0
  13. package/dist/database-native-sql.d.ts.map +1 -0
  14. package/dist/database-native-sql.js +266 -0
  15. package/dist/database-native-sql.js.map +1 -0
  16. package/dist/env.d.ts +31 -0
  17. package/dist/env.d.ts.map +1 -0
  18. package/dist/env.js +229 -0
  19. package/dist/env.js.map +1 -0
  20. package/dist/fastapi-dev-runner.d.ts +3 -0
  21. package/dist/fastapi-dev-runner.d.ts.map +1 -0
  22. package/dist/fastapi-dev-runner.js +263 -0
  23. package/dist/fastapi-dev-runner.js.map +1 -0
  24. package/dist/flow-proof.d.ts +3 -0
  25. package/dist/flow-proof.d.ts.map +1 -0
  26. package/dist/flow-proof.js +2 -0
  27. package/dist/flow-proof.js.map +1 -0
  28. package/dist/index.d.ts +36 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.js +5353 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/rules-proof.d.ts +3 -0
  33. package/dist/rules-proof.d.ts.map +1 -0
  34. package/dist/rules-proof.js +2 -0
  35. package/dist/rules-proof.js.map +1 -0
  36. package/package.json +49 -0
@@ -0,0 +1,794 @@
1
+ # Frontend-Family Authoring (`.web.loj`, legacy `.rdsl`)
2
+
3
+ Use this reference when authoring or reviewing frontend-family source files.
4
+
5
+ ## Defaults
6
+
7
+ - Prefer `.web.loj` for new files. Use `.rdsl` only for legacy edits.
8
+ - Current implemented compiler triple is:
9
+
10
+ ```yaml
11
+ compiler:
12
+ target: react
13
+ ```
14
+
15
+ - Files are a strict YAML subset:
16
+ - no anchors
17
+ - no aliases
18
+ - no merge keys
19
+ - no custom tags
20
+
21
+ ## File Shape
22
+
23
+ Root files may contain:
24
+
25
+ - `app:`
26
+ - optional `compiler:`
27
+ - optional `imports:`
28
+ - `model <Name>:`
29
+ - `resource <name>:`
30
+ - `readModel <name>:`
31
+ - `page <name>:`
32
+
33
+ Module files may contain:
34
+
35
+ - optional `imports:`
36
+ - `model <Name>:`
37
+ - `resource <name>:`
38
+ - `readModel <name>:`
39
+ - `page <name>:`
40
+
41
+ Module files may not contain `app:` or `compiler:`.
42
+
43
+ ## Imports
44
+
45
+ - Use single-file for small demos.
46
+ - Split only when the app gets large:
47
+ - `4+` models
48
+ - `3+` resources
49
+ - complex custom pages
50
+ - `imports:` entries must be relative `.web.loj` / `.rdsl` paths or directories ending in `/`.
51
+ - Nested imports are allowed.
52
+ - Import cycles are invalid.
53
+ - Directory imports expand direct child family files only, sorted lexicographically.
54
+
55
+ ## `app:`
56
+
57
+ Example:
58
+
59
+ ```yaml
60
+ app:
61
+ name: "Flight Booking"
62
+ theme: light
63
+ auth: jwt
64
+ style: '@style("./styles/theme")'
65
+ seo:
66
+ siteName: "Loj Air"
67
+ defaultTitle: "Loj Air"
68
+ titleTemplate: "{title} · Loj Air"
69
+ defaultDescription: "Flight booking demo"
70
+ defaultImage: '@asset("./assets/og-default.png")'
71
+ favicon: '@asset("./assets/favicon.png")'
72
+ navigation:
73
+ - group:
74
+ key: "nav.system"
75
+ defaultMessage: "System"
76
+ items:
77
+ - label:
78
+ key: "nav.users"
79
+ defaultMessage: "Users"
80
+ icon: users
81
+ target: resource.users.list
82
+ ```
83
+
84
+ Rules:
85
+
86
+ - `name` is required.
87
+ - `theme` is `light` or `dark`.
88
+ - `auth` is `none`, `jwt`, or `session`.
89
+ - `style` must use `@style("./styles/x")`.
90
+ - `seo` is optional and currently supports:
91
+ - `siteName`
92
+ - `defaultTitle`
93
+ - `titleTemplate`
94
+ - `defaultDescription`
95
+ - `defaultImage`
96
+ - `favicon`
97
+ - navigation targets are `page.<name>` or `resource.<name>.list`
98
+ - navigation `group` and item `label` currently accept either plain string or shared descriptor
99
+ shape `{ key?, defaultMessage?, values? }`
100
+
101
+ ## Current `MessageLike` / Descriptor Surfaces
102
+
103
+ In the current implemented `.web.loj` slice, these user-facing copy surfaces accept either:
104
+
105
+ - plain string
106
+ - shared descriptor shape `{ key?, defaultMessage?, values? }`
107
+
108
+ Current supported surfaces:
109
+
110
+ - `resource.list.title`
111
+ - `resource.read.title`
112
+ - navigation `group`
113
+ - navigation item `label`
114
+ - `page.title`
115
+ - `page.blocks[].title`
116
+ - page/create handoff `label`
117
+ - read-model `dateNavigation.prevLabel`
118
+ - read-model `dateNavigation.nextLabel`
119
+ - SEO-facing copy directions such as `app.seo.defaultTitle`, `app.seo.titleTemplate`,
120
+ `app.seo.defaultDescription`, and `page.seo.description`
121
+
122
+ Current guardrails:
123
+
124
+ - use plain strings for fixed copy
125
+ - use descriptors when future i18n or scalar-literal interpolation matters
126
+ - descriptor `values` in these UI-copy surfaces currently accept only scalar literals, not
127
+ `{ ref: ... }`
128
+ - do not assume every string field in `.web.loj` is `MessageLike`
129
+
130
+ ## `model <Name>:`
131
+
132
+ Example:
133
+
134
+ ```yaml
135
+ model User:
136
+ name: string @required @minLen(2)
137
+ email: string @required @email @unique
138
+ role: enum(admin, editor, viewer)
139
+ teamId: belongsTo(Team)
140
+ members: hasMany(Member, by: teamId)
141
+ createdAt: datetime @auto
142
+ ```
143
+
144
+ Types:
145
+
146
+ - `string`
147
+ - `number`
148
+ - `boolean`
149
+ - `datetime`
150
+ - `enum(a, b, c)`
151
+ - `belongsTo(Model)`
152
+ - `hasMany(Model, by: field)`
153
+
154
+ Decorators:
155
+
156
+ - `@required`
157
+ - `@email`
158
+ - `@unique`
159
+ - `@minLen(n)`
160
+ - `@auto`
161
+
162
+ Resource-backed records include implicit runtime `id: string`.
163
+
164
+ Relation rules:
165
+
166
+ - `belongsTo(Model)` is the narrow single-record relation field.
167
+ - `hasMany(Model, by: field)` is inverse metadata only; it does not generate a client model field.
168
+ - `hasMany(..., by: ...)` must point at a target-model field declared as
169
+ `belongsTo(CurrentModel)`.
170
+ - `hasMany(...)` does not support field decorators.
171
+
172
+ ## `resource <name>:`
173
+
174
+ Example:
175
+
176
+ ```yaml
177
+ resource bookings:
178
+ model: Booking
179
+ api: /api/bookings
180
+
181
+ list:
182
+ title:
183
+ key: "bookings.list.title"
184
+ defaultMessage: "Bookings"
185
+ style: listShell
186
+ filters: [reference, status, member.name]
187
+ columns:
188
+ - reference @sortable
189
+ - status @badge(DRAFT:gray, READY:blue, CONFIRMED:green)
190
+ - member.name @sortable
191
+
192
+ read:
193
+ title: "Booking Details"
194
+ style: detailShell
195
+ fields:
196
+ - reference
197
+ - member.name
198
+
199
+ create:
200
+ style: formShell
201
+ fields:
202
+ - reference
203
+ - status
204
+ includes:
205
+ - field: passengers
206
+ minItems: 1
207
+ fields:
208
+ - name
209
+ - seat
210
+ rules: '@rules("./rules/passenger-create")'
211
+ rules: '@rules("./rules/booking-create")'
212
+
213
+ edit:
214
+ style: formShell
215
+ fields:
216
+ - reference
217
+ - status
218
+ includes:
219
+ - field: passengers
220
+ fields:
221
+ - id
222
+ - name
223
+ - seat
224
+ rules: '@rules("./rules/passenger-edit")'
225
+ rules: '@rules("./rules/booking-edit")'
226
+
227
+ workflow:
228
+ source: '@flow("./workflows/booking-lifecycle")'
229
+ style: workflowShell
230
+ ```
231
+
232
+ Current resource surface rules:
233
+
234
+ - `workflow:` is optional and may be either:
235
+ - scalar `@flow("./workflows/x")`
236
+ - mapping `{ source: '@flow("./workflows/x")', style: workflowShell }`
237
+ - `list.style`, `read.style`, `create.style`, `edit.style`, and `workflow.style` are optional
238
+ shell-level style hooks only
239
+ - `list.title` / `read.title` currently accept plain string or descriptor
240
+ - `create.rules` / `edit.rules` may still use inline `visibleIf/enabledIf/allowIf/enforce`
241
+ mappings or linked `@rules("./rules/x")`
242
+ - linked form rules currently support only:
243
+ - `eligibility`
244
+ - `validate`
245
+ - `derive`
246
+ - frontend generated form consumers reject linked `allow/deny`
247
+ - linked `derive` currently supports only already-listed scalar form fields
248
+ - `create.includes` / `edit.includes` currently support one-level repeated-child forms over direct
249
+ `hasMany(Target, by: field)` relations
250
+ - repeated-child include rules may also use linked `.rules.loj`
251
+ - generated `edit.includes` submits one-level diff semantics:
252
+ - child rows with `id` update
253
+ - child rows without `id` create
254
+ - omitted existing child rows delete
255
+
256
+ Workflow-linked resource behavior today:
257
+
258
+ - linked workflow `model` must match the resource `model`
259
+ - linked workflow `field` must point to an `enum(...)` field on that model
260
+ - `wizard.steps` may set optional `surface: form | read | workflow`
261
+ - when omitted, the first wizard step defaults to `form` and later steps default to `workflow`
262
+ - create/edit/read/workflow surfaces derive narrow current/next-step summaries
263
+ - create/edit CTA labels become step-aware when a visible next step exists
264
+ - read and fixed `/:id/workflow` prioritize transitions that advance to the next visible step
265
+ - read/workflow also surface narrow `workflowStep` review handoff plus `Redo <previous step>`
266
+
267
+ ## `readModel <name>:`
268
+
269
+ Example:
270
+
271
+ ```yaml
272
+ readModel outwardFlightAvailability:
273
+ api: /api/outward-flight-availability
274
+ inputs:
275
+ outwardDate: date @required
276
+ cabin: enum(ECONOMY, BUSINESS) @required
277
+ result:
278
+ flightNo: string
279
+ fareBrand: string
280
+ quotedFare: number
281
+ rules: '@rules("./rules/outward-flight-availability")'
282
+ list:
283
+ groupBy: [flightNo]
284
+ pivotBy: fareBrand
285
+ columns:
286
+ - flightNo
287
+ - fareBrand
288
+ - quotedFare
289
+ ```
290
+
291
+ Current read-model rules:
292
+
293
+ - `api:` is required and points at a fixed GET endpoint
294
+ - `rules:` is optional and must use `@rules("./rules/x")`
295
+ - `inputs:` and `result:` must be YAML mappings, not field lists
296
+ - `inputs:` and `result:` currently support only scalar and enum field types
297
+ - `list:` is required only for `data: readModel.<name>.list`
298
+ - current frontend-family consumers are:
299
+ - page `table` blocks via `data: readModel.<name>.list`
300
+ - page `metric` blocks via `data: readModel.<name>.count`
301
+ - current frontend `readModel rules` consumption supports only:
302
+ - `eligibility`
303
+ - `validate`
304
+ - `derive`
305
+ - frontend `readModel rules` reject `allow/deny`
306
+ - `derive` runs client-side over fetched rows; it is not query pushdown
307
+
308
+ Grouped/table presentation today:
309
+
310
+ - `queryState: <name>` shares one URL-backed query state across multiple read-model consumers with
311
+ identical `inputs:`
312
+ - `list.groupBy:` is optional on read-model table consumers
313
+ - `list.pivotBy:` is optional on grouped table consumers
314
+ - `dateNavigation:` may set `field`, optional `prevLabel`, and optional `nextLabel`
315
+ - `selectionState: <name>` exposes one selected row to page-level handoff actions
316
+
317
+ Current copy rule:
318
+
319
+ - `dateNavigation.prevLabel` / `nextLabel` accept plain string or descriptor
320
+ - descriptor `values` in these UI-copy fields currently accept only scalar literals
321
+
322
+ ## `page <name>:`
323
+
324
+ Example:
325
+
326
+ ```yaml
327
+ page availability:
328
+ title: "Flight Availability"
329
+ style: pageShell
330
+ seo:
331
+ description: "Search outbound and homeward flights"
332
+ canonicalPath: "/availability"
333
+ image: '@asset("./assets/availability-og.png")'
334
+ actions:
335
+ - create:
336
+ resource: bookings
337
+ label: "Book selected itinerary"
338
+ seed:
339
+ outwardFlightNo:
340
+ selection: outwardFlight.flightNo
341
+ travelDate:
342
+ input: availabilitySearch.outwardDate
343
+ blocks:
344
+ - type: table
345
+ title: "Outbound Flights"
346
+ style: tableShell
347
+ data: readModel.outwardFlightAvailability.list
348
+ queryState: availabilitySearch
349
+ selectionState: outwardFlight
350
+ dateNavigation:
351
+ field: outwardDate
352
+ prevLabel: "Previous day"
353
+ nextLabel: "Next day"
354
+ - type: metric
355
+ title: "Matching flights"
356
+ data: readModel.outwardFlightAvailability.count
357
+ queryState: availabilitySearch
358
+ ```
359
+
360
+ Current page rules:
361
+
362
+ - `page.title` accepts plain string or descriptor
363
+ - `page.style` is optional and must reference a named style from the linked `app.style` program
364
+ - `page.seo` is optional and currently supports:
365
+ - `description`
366
+ - `canonicalPath`
367
+ - `image`
368
+ - `noIndex`
369
+ - `blocks[].title` accepts plain string or descriptor
370
+ - `blocks[].style` is optional and shell-level only
371
+
372
+ Current block types:
373
+
374
+ - `metric`
375
+ - `chart`
376
+ - `table`
377
+ - `custom`
378
+
379
+ Current `table` block behavior:
380
+
381
+ - `data:` may use:
382
+ - `<resource>.list`
383
+ - `readModel.<name>.list`
384
+ - `<resource>.<hasManyField>` on record-scoped relation pages
385
+ - read-model-backed table blocks may also declare:
386
+ - `queryState`
387
+ - `dateNavigation`
388
+ - `selectionState`
389
+ - narrow `rowActions.create`
390
+ - pages may also declare narrow `actions.create` handoff when those same pages already expose
391
+ `selectionState`
392
+ - record-scoped relation pages may reuse target resource list columns/filters/pagination/actions when
393
+ the target resource already defines `list:`
394
+
395
+ Current custom block rule:
396
+
397
+ - `custom` is still the strongest escape hatch tier
398
+ - record-scoped relation pages pass a narrow generated context object including `parentWorkflow` and
399
+ `relations` summaries
400
+
401
+ ## Style DSL (`.style.loj`)
402
+
403
+ Use `.style.loj` when shell-level visual intent is stable enough to stay out of raw CSS but does
404
+ not belong in `.web.loj` business structure.
405
+
406
+ Current linking points:
407
+
408
+ - `app.style: '@style("./styles/theme")'`
409
+ - `page.style`
410
+ - `page.blocks[].style`
411
+ - `resource.list.style`
412
+ - `resource.read.style`
413
+ - `resource.create.style`
414
+ - `resource.edit.style`
415
+ - `resource.workflow.style` through `workflow: { source, style }`
416
+
417
+ Example:
418
+
419
+ ```yaml
420
+ tokens:
421
+ colors:
422
+ surface: "#ffffff"
423
+ border: "#d9dfeb"
424
+ text: "#18212f"
425
+ accent: "#0f5fff"
426
+ spacing:
427
+ sm: 8
428
+ md: 16
429
+ lg: 24
430
+ borderRadius:
431
+ md: 16
432
+ lg: 24
433
+ elevation:
434
+ card: 3
435
+ panel: 5
436
+ typography:
437
+ body:
438
+ fontSize: 16
439
+ fontWeight: 400
440
+ lineHeight: 24
441
+ heading:
442
+ fontSize: 20
443
+ fontWeight: 700
444
+ lineHeight: 28
445
+
446
+ style pageShell:
447
+ display: column
448
+ gap: lg
449
+ padding: lg
450
+ typography: body
451
+ color: text
452
+
453
+ style resultShell:
454
+ extends: pageShell
455
+ maxWidth: 1360
456
+ backgroundColor: surface
457
+ borderRadius: lg
458
+ borderWidth: 1
459
+ borderColor: border
460
+ elevation: panel
461
+ escape:
462
+ css: |
463
+ width: 100%;
464
+ margin: 0 auto;
465
+ ```
466
+
467
+ Current token interpretation:
468
+
469
+ - `fontSize`: numeric px
470
+ - `lineHeight`: numeric px
471
+ - `fontWeight`: numeric CSS font-weight
472
+
473
+ Current token groups:
474
+
475
+ - `colors`
476
+ - `spacing`
477
+ - `borderRadius`
478
+ - `elevation`
479
+ - `typography`
480
+
481
+ Current style properties:
482
+
483
+ - layout:
484
+ - `display: row | column | stack`
485
+ - `gap`
486
+ - `padding`
487
+ - `paddingHorizontal`
488
+ - `paddingVertical`
489
+ - `alignItems: start | center | end | stretch`
490
+ - `justifyContent: start | center | end | spaceBetween | spaceAround`
491
+ - size:
492
+ - `width`
493
+ - `minHeight`
494
+ - `maxWidth`
495
+ - surface:
496
+ - `backgroundColor`
497
+ - `borderRadius`
498
+ - `borderWidth`
499
+ - `borderColor`
500
+ - `elevation`
501
+ - text:
502
+ - `typography`
503
+ - `color`
504
+ - `opacity`
505
+ - inheritance:
506
+ - `extends`
507
+ - escape:
508
+ - `escape.css`
509
+
510
+ Token-reference rules:
511
+
512
+ - `gap`, `padding`, `paddingHorizontal`, `paddingVertical` resolve bare refs from `spacing`
513
+ - `borderRadius` resolves bare refs from `borderRadius`
514
+ - `elevation` resolves bare refs from `elevation`
515
+ - `backgroundColor`, `borderColor`, and `color` resolve bare refs from `colors`
516
+ - `typography` resolves bare refs from `typography`
517
+
518
+ Current style guardrails:
519
+
520
+ - keep `.style.loj` for shell-level style intent
521
+ - do not expect table internals, form sections, read-related panels, or responsive/mobile variants
522
+ to have first-class hooks yet
523
+ - `escape.css` is the current narrow escape hatch for web-only styling details
524
+ - if a visual need is clearly DOM/CSS-structure-specific, prefer raw CSS escape rather than forcing
525
+ it into the shared style layer
526
+
527
+ Proof-driven style guidance from the current flight-booking proof:
528
+
529
+ - avoid stacking two shell systems on the same surface
530
+ - if a node already uses `loj-style-*`, do not also give that same node a heavy proof-local
531
+ `.rdsl-block` card treatment
532
+ - let `.style.loj` own the outer shell; use raw CSS for internals
533
+ - start from a compact business-UI token baseline
534
+ - large `xl/xxl` spacing, large radii, and strong elevation can make generated form/table pages
535
+ feel empty and inflated very quickly
536
+ - prefer tighter tokens first, then expand only after seeing the composed page
537
+ - good `.style.loj` candidates:
538
+ - page shell
539
+ - page block shell
540
+ - resource list/read/create/edit/workflow shell
541
+ - keep these in `escape.css` for now:
542
+ - button alignment and oversized pill fixes caused by current host/runtime flex behavior
543
+ - table overflow, table-cell density, and sticky-header tuning
544
+ - empty-state presentation
545
+ - filter-bar and form-grid internals
546
+ - workflow summary internals
547
+ - repeated-child include section internals
548
+ - if the visual bug comes from current generated DOM behavior rather than stable author-facing shell
549
+ intent, do not propose a new `.style.loj` primitive first
550
+
551
+ Current style escape example:
552
+
553
+ ```yaml
554
+ style bookingListShell:
555
+ extends: resultShell
556
+ backgroundColor: surface
557
+ elevation: panel
558
+ escape:
559
+ css: |
560
+ background-image: linear-gradient(180deg, rgba(219, 232, 255, 0.35), rgba(255, 255, 255, 0.98));
561
+ ```
562
+
563
+ Use `escape.css` for:
564
+
565
+ - gradients
566
+ - browser-specific decorative layering
567
+ - table/form/sidebar internals
568
+ - responsive details that are still outside the shared style contract
569
+
570
+ Do not use it to re-encode the whole shell when the shared style primitives already fit.
571
+
572
+ ## Combined Example: Shared Read-Model Query + Selection Handoff
573
+
574
+ ```yaml
575
+ page availability:
576
+ title: "Flight Availability"
577
+ style: availabilityPageShell
578
+ actions:
579
+ - create:
580
+ resource: bookings
581
+ label: "Book selected itinerary"
582
+ seed:
583
+ outwardFlightNo:
584
+ selection: outwardFlight.flightNo
585
+ homewardFlightNo:
586
+ selection: homewardFlight.flightNo
587
+ travelDate:
588
+ input: availabilitySearch.outwardDate
589
+ blocks:
590
+ - type: table
591
+ title: "Outbound Flights"
592
+ style: availabilityResultShell
593
+ data: readModel.outwardFlightAvailability.list
594
+ queryState: availabilitySearch
595
+ selectionState: outwardFlight
596
+ dateNavigation:
597
+ field: outwardDate
598
+ prevLabel: "Previous day"
599
+ nextLabel: "Next day"
600
+ - type: table
601
+ title: "Homeward Flights"
602
+ style: availabilityResultShell
603
+ data: readModel.homewardFlightAvailability.list
604
+ queryState: availabilitySearch
605
+ selectionState: homewardFlight
606
+ ```
607
+
608
+ ## Combined Example: Resource Workflow + Style + Rules + Includes
609
+
610
+ ```yaml
611
+ resource bookings:
612
+ model: Booking
613
+ api: /api/bookings
614
+
615
+ list:
616
+ title: "Bookings"
617
+ style: bookingListShell
618
+ columns:
619
+ - reference @sortable
620
+ - status @badge(DRAFT:gray, READY:blue, CONFIRMED:green, FAILED:red)
621
+
622
+ read:
623
+ title: "Booking Details"
624
+ style: bookingDetailShell
625
+ fields:
626
+ - reference
627
+ - status
628
+
629
+ create:
630
+ style: bookingFormShell
631
+ fields:
632
+ - reference
633
+ - quotedFare
634
+ includes:
635
+ - field: passengers
636
+ minItems: 1
637
+ fields:
638
+ - name
639
+ - seat
640
+ rules: '@rules("./rules/passenger-create")'
641
+ rules: '@rules("./rules/booking-create")'
642
+
643
+ edit:
644
+ style: bookingFormShell
645
+ fields:
646
+ - reference
647
+ - quotedFare
648
+ includes:
649
+ - field: passengers
650
+ fields:
651
+ - id
652
+ - name
653
+ - seat
654
+ rules: '@rules("./rules/passenger-edit")'
655
+ rules: '@rules("./rules/booking-edit")'
656
+
657
+ workflow:
658
+ source: '@flow("./workflows/booking-lifecycle")'
659
+ style: bookingWorkflowShell
660
+ ```
661
+
662
+ ## Frontend Escape Hatches
663
+
664
+ Use escape hatches only when the current `.web.loj` slice cannot express the behavior directly.
665
+
666
+ Preferred order:
667
+
668
+ - built-in DSL
669
+ - `@expr(...)`
670
+ - `@fn(...)`
671
+ - `@custom(...)`
672
+
673
+ ### `@expr(...)`
674
+
675
+ Use for pure boolean/value logic inside supported expression slots.
676
+
677
+ ```yaml
678
+ edit:
679
+ fields:
680
+ - field: role
681
+ rules:
682
+ enabledIf: '@expr(currentUser?.role === "admin" && record?.status !== "archived")'
683
+ ```
684
+
685
+ Rules:
686
+
687
+ - keep it pure and deterministic
688
+ - current runtime context is narrow:
689
+ - `currentUser`
690
+ - `record`
691
+ - `formData`
692
+ - `item` on repeated-child rows
693
+
694
+ ### `@fn("./logic/x")`
695
+
696
+ Use when the logic is too complex for the shared expression language.
697
+
698
+ ```yaml
699
+ edit:
700
+ rules:
701
+ allowIf: '@fn("./logic/canEditBooking")'
702
+ ```
703
+
704
+ Path rules:
705
+
706
+ - extensionless logical ids are preferred
707
+ - frontend-family resolves extensionless logical ids to `.ts` first, then `.js`
708
+ - explicit `.ts` / `.js` suffixes are accepted as deliberate lock-in
709
+ - the path resolves relative to the `.web.loj` file that declares it
710
+
711
+ Current function shape:
712
+
713
+ ```ts
714
+ export default function canEditBooking(context: {
715
+ currentUser?: unknown;
716
+ record?: unknown;
717
+ formData?: unknown;
718
+ item?: unknown;
719
+ }) {
720
+ return true;
721
+ }
722
+ ```
723
+
724
+ Current file-shape rule:
725
+
726
+ - frontend `@fn(...)` points at a normal `.ts` / `.js` host file with an exported default function
727
+ - it is not a function-body snippet
728
+ - a normal function declaration is expected
729
+ - imports are fine in principle because this is a normal host file, but keep helpers narrow and
730
+ local if you want maximum portability
731
+
732
+ ### `@custom("./components/x.tsx")`
733
+
734
+ Use for React-specific rendering/custom input surfaces.
735
+
736
+ Column custom cell:
737
+
738
+ ```yaml
739
+ columns:
740
+ - fareBrand @custom("./components/FareBrandCell.tsx")
741
+ ```
742
+
743
+ Props:
744
+
745
+ - `{ value, record }`
746
+
747
+ Field custom component:
748
+
749
+ ```yaml
750
+ fields:
751
+ - seat @custom("./components/SeatPicker.tsx")
752
+ ```
753
+
754
+ Props:
755
+
756
+ - `{ value, onChange }`
757
+
758
+ Block custom component:
759
+
760
+ ```yaml
761
+ blocks:
762
+ - type: custom
763
+ title: "Recovery"
764
+ custom: "./components/RecoveryPanel.tsx"
765
+ ```
766
+
767
+ Props:
768
+
769
+ - self-managed by default
770
+ - on record-scoped relation pages, generated props may also include narrow `parentWorkflow` and
771
+ `relations` summaries
772
+
773
+ Host-file rules:
774
+
775
+ - custom `.ts` / `.tsx` / `.js` / `.jsx` escape files may use relative static imports
776
+ - they may also import local `.css` / `.module.css`
777
+ - keep raw CSS inside host files, not inside `.web.loj`
778
+ - build/dev currently preserve those imported dependencies in generated output
779
+
780
+ ## Commands
781
+
782
+ - `rdsl validate <entry.web.loj>`
783
+ - `rdsl build <entry.web.loj> --out-dir <dir>`
784
+ - `rdsl inspect <entry.web.loj|build-dir> [--node <id>]`
785
+ - `rdsl trace <entry.web.loj|build-dir> <generated-file:line[:col]>`
786
+
787
+ ## Guardrails
788
+
789
+ - Do not invent generic query DSL or backend join syntax around `readModel`.
790
+ - Do not invent style hooks for table internals, form sections, or responsive/mobile variants beyond
791
+ the current shell-level slice.
792
+ - Do not invent broader workflow/page router syntax beyond linked resource workflow surfaces.
793
+ - Do not leak React component APIs into `.web.loj`; keep React-specific escape code in `@custom`,
794
+ `@fn`, or host-side files.