rip-lang 3.15.4 → 3.16.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 (112) hide show
  1. package/README.md +6 -4
  2. package/bin/rip +167 -12
  3. package/docs/AGENTS.md +1 -1
  4. package/docs/RIP-APP.md +808 -0
  5. package/docs/RIP-DUCKDB.md +477 -0
  6. package/docs/RIP-INTRO.md +396 -0
  7. package/docs/RIP-LANG.md +59 -5
  8. package/docs/RIP-SCHEMA.md +191 -8
  9. package/docs/RIP-TYPES.md +74 -103
  10. package/docs/demo/README.md +4 -3
  11. package/docs/dist/rip.js +3627 -1470
  12. package/docs/dist/rip.min.js +671 -244
  13. package/docs/dist/rip.min.js.br +0 -0
  14. package/docs/example/index.json +7 -7
  15. package/docs/example/index.json.br +0 -0
  16. package/docs/extensions/duckdb/manifest.json +1 -1
  17. package/docs/extensions/duckdb/v1.5.2/linux_amd64/ripdb.duckdb_extension.gz +0 -0
  18. package/docs/extensions/duckdb/v1.5.2/osx_arm64/ripdb.duckdb_extension.gz +0 -0
  19. package/docs/extensions/vscode/print/index.html +2 -1
  20. package/docs/extensions/vscode/print/print-1.0.13.vsix +0 -0
  21. package/docs/extensions/vscode/print/print-1.0.14.vsix +0 -0
  22. package/docs/extensions/vscode/print/print-latest.vsix +0 -0
  23. package/docs/extensions/vscode/rip/rip-0.5.15.vsix +0 -0
  24. package/docs/extensions/vscode/rip/rip-latest.vsix +0 -0
  25. package/docs/ui/bundle.json +61 -0
  26. package/docs/ui/bundle.json.br +0 -0
  27. package/docs/ui/hljs-rip.js +0 -7
  28. package/docs/ui/index.css +66 -23
  29. package/docs/ui/index.html +6 -6
  30. package/package.json +9 -3
  31. package/rip-loader.js +64 -2
  32. package/src/AGENTS.md +63 -36
  33. package/src/browser.js +96 -14
  34. package/src/compiler.js +960 -143
  35. package/src/components.js +794 -88
  36. package/src/{types-emit.js → dts.js} +181 -71
  37. package/src/grammar/README.md +1 -1
  38. package/src/grammar/grammar.rip +111 -97
  39. package/src/lexer.js +132 -18
  40. package/src/parser.js +203 -205
  41. package/src/repl.js +74 -6
  42. package/src/schema/runtime-orm.js +168 -4
  43. package/src/schema/runtime-validate.js +146 -2
  44. package/src/schema/runtime.generated.js +314 -6
  45. package/src/schema/schema.js +5 -5
  46. package/src/sourcemaps.js +277 -1
  47. package/src/stdlib.js +253 -0
  48. package/src/typecheck.js +2023 -106
  49. package/src/types.js +127 -7
  50. package/docs/ui/accordion.rip +0 -103
  51. package/docs/ui/alert-dialog.rip +0 -53
  52. package/docs/ui/autocomplete.rip +0 -115
  53. package/docs/ui/avatar.rip +0 -37
  54. package/docs/ui/badge.rip +0 -15
  55. package/docs/ui/breadcrumb.rip +0 -47
  56. package/docs/ui/button-group.rip +0 -26
  57. package/docs/ui/button.rip +0 -23
  58. package/docs/ui/card.rip +0 -25
  59. package/docs/ui/carousel.rip +0 -110
  60. package/docs/ui/checkbox-group.rip +0 -61
  61. package/docs/ui/checkbox.rip +0 -33
  62. package/docs/ui/collapsible.rip +0 -50
  63. package/docs/ui/combobox.rip +0 -130
  64. package/docs/ui/context-menu.rip +0 -88
  65. package/docs/ui/date-picker.rip +0 -206
  66. package/docs/ui/dialog.rip +0 -60
  67. package/docs/ui/drawer.rip +0 -58
  68. package/docs/ui/editable-value.rip +0 -82
  69. package/docs/ui/field.rip +0 -53
  70. package/docs/ui/fieldset.rip +0 -22
  71. package/docs/ui/form.rip +0 -39
  72. package/docs/ui/grid.rip +0 -901
  73. package/docs/ui/input-group.rip +0 -28
  74. package/docs/ui/input.rip +0 -36
  75. package/docs/ui/label.rip +0 -16
  76. package/docs/ui/menu.rip +0 -134
  77. package/docs/ui/menubar.rip +0 -151
  78. package/docs/ui/meter.rip +0 -36
  79. package/docs/ui/multi-select.rip +0 -203
  80. package/docs/ui/native-select.rip +0 -33
  81. package/docs/ui/nav-menu.rip +0 -126
  82. package/docs/ui/number-field.rip +0 -162
  83. package/docs/ui/otp-field.rip +0 -89
  84. package/docs/ui/pagination.rip +0 -123
  85. package/docs/ui/popover.rip +0 -93
  86. package/docs/ui/preview-card.rip +0 -75
  87. package/docs/ui/progress.rip +0 -25
  88. package/docs/ui/radio-group.rip +0 -57
  89. package/docs/ui/resizable.rip +0 -123
  90. package/docs/ui/scroll-area.rip +0 -145
  91. package/docs/ui/select.rip +0 -151
  92. package/docs/ui/separator.rip +0 -17
  93. package/docs/ui/skeleton.rip +0 -22
  94. package/docs/ui/slider.rip +0 -165
  95. package/docs/ui/spinner.rip +0 -17
  96. package/docs/ui/table.rip +0 -27
  97. package/docs/ui/tabs.rip +0 -113
  98. package/docs/ui/textarea.rip +0 -48
  99. package/docs/ui/toast.rip +0 -87
  100. package/docs/ui/toggle-group.rip +0 -71
  101. package/docs/ui/toggle.rip +0 -24
  102. package/docs/ui/toolbar.rip +0 -38
  103. package/docs/ui/tooltip.rip +0 -85
  104. package/src/app.rip +0 -1571
  105. package/src/sourcemap-merge.js +0 -287
  106. /package/docs/demo/{components → routes}/_layout.rip +0 -0
  107. /package/docs/demo/{components → routes}/about.rip +0 -0
  108. /package/docs/demo/{components → routes}/card.rip +0 -0
  109. /package/docs/demo/{components → routes}/counter.rip +0 -0
  110. /package/docs/demo/{components → routes}/index.rip +0 -0
  111. /package/docs/demo/{components → routes}/todos.rip +0 -0
  112. /package/src/schema/{dts-emit.js → dts.js} +0 -0
@@ -0,0 +1,396 @@
1
+ # Rip — An Introduction
2
+
3
+ > A deep dive into the Rip language, its schema system, and the MedLabs reference application — plus an honest assessment of what's strong, what's risky, and what's worth watching.
4
+
5
+ ---
6
+
7
+ ## Table of Contents
8
+
9
+ 1. [Rip — the big idea in one sentence](#1-rip--the-big-idea-in-one-sentence)
10
+ 2. [Operators — the coherent sigil system](#2-operators--the-coherent-sigil-system)
11
+ 3. [Reactivity — the `~` always family](#3-reactivity--the--always-family)
12
+ 4. [Rip Schema — one keyword, three libraries](#4-rip-schema--one-keyword-three-libraries)
13
+ 5. [MedLabs — architecture tour](#5-medlabs--architecture-tour)
14
+ 6. [Risks and open questions](#6-risks-and-open-questions)
15
+ 7. [Verdict](#7-verdict)
16
+
17
+ ---
18
+
19
+ ## 1. Rip — the big idea in one sentence
20
+
21
+ Rip is CoffeeScript's ergonomic syntax retargeted at ES2022, with **reactivity as language primitives** (`:=`, `~=`, `~>`, `<=>`) and **schemas as language primitives** (`schema :model`, `:shape`, `:enum`, `:mixin`, `:input`), delivered as a **zero-dependency, self-hosting compiler**.
22
+
23
+ The three commitments that make it distinctive:
24
+
25
+ | Commitment | What it buys | What it costs |
26
+ |---|---|---|
27
+ | Modern output (ES2022) | Native `class`, `?.`, `??`, modules — smaller codegen, no polyfill baggage | Bun-only runtime (WebCrypto, `Bun.spawn`, import-rewriting loader) |
28
+ | Reactivity in syntax | `count := 0`, `doubled ~= count * 2`, `~> log count` compile to `__state / __computed / __effect` runtime primitives the UI framework hooks for fine-grained DOM updates | Dependency tracking is compiler-recognized, not data-flow inferred — users must follow discipline (e.g. `theme = @app.data.theme` to get a reactive root) |
29
+ | Zero deps, self-hosting | `{ "dependencies": {} }`, `bun run parser` rebuilds the parser from source | Everything bespoke: JWT, source maps, parser generator (Solar), test runner, even the browser bundle |
30
+
31
+ **Pipeline** (deliberately small):
32
+
33
+ ```
34
+ Source → Lexer → emitTypes → Parser → S-expressions → CodeEmitter → JavaScript
35
+ (types.js) (arrays + .loc) + source map
36
+ ```
37
+
38
+ S-expressions are plain arrays with `.loc` metadata (`["=", "x", 42]`), not AST classes. That one choice is the main reason the compiler is so small — and it is a deliberate **maintainability tradeoff**: fewer static guarantees, harder to evolve, but compact and readable.
39
+
40
+ ---
41
+
42
+ ## 2. Operators — the coherent sigil system
43
+
44
+ Rip extends CoffeeScript with about a dozen new operators. The easy mistake is to look at them in isolation and call the accumulation "cognitive debt." The better frame is to see them as **a small set of orthogonal axes**.
45
+
46
+ ### Assignment and function arrows — one grid, two axes
47
+
48
+ | | value form | function form |
49
+ |----------|-----------|-------------------|
50
+ | regular | `x = 5` | `f ->` |
51
+ | bound | — | `f =>` |
52
+ | state | `x := 5` | — |
53
+ | reactive | `x ~= …` | `f ~>` |
54
+
55
+ Two axes: **"value vs function"** (`=` vs `>`) × **"what modifier"** (none / bound / state / reactive).
56
+
57
+ The symbols compose:
58
+
59
+ - `=` — ordinary value binding
60
+ - `->` — ordinary function (call-site `this`)
61
+ - `=>` — bound function (lexical `this`) — the `=` inside the arrow is the "equals this" mnemonic
62
+ - `:=` — state binding (the `:` is the "gets state" marker)
63
+ - `~=` — reactive value: **always equals** the expression
64
+ - `~>` — reactive function: **always calls** on dependency change
65
+ - `<=>` — two-way bind (bidirectional data flow — a separate axis)
66
+ - `=!` — readonly const: "equals, dammit!"
67
+
68
+ Read as families, not atoms:
69
+
70
+ - `= / := / ~=` — "is 5" / "gets state 5" / "always gets computed"
71
+ - `-> / => / ~>` — "calls" / "this-bound calls" / "always calls"
72
+
73
+ `~` is consistently **"always"**. `~=` is the reactive variant of `=`; `~>` is the reactive variant of `->`. Once you see the grid, `~>` is not opaque — it is the most naturally named arrow in the family.
74
+
75
+ ### Other operator families
76
+
77
+ | Family | Members | Theme |
78
+ |---|---|---|
79
+ | Existence / safety | `x?` · `x ?? y` · `a?.b` · `a?.[0]` · `a?.()` · `a?[0]` · `a?(x)` · `el?.prop = v` · `?!` (presence / Houdini) · `?? throw` | Nothing-safe access and guards |
80
+ | Dammit / await | `fetch! url` · `user.save!` · `User.find! 1` | One glyph: "call it and await" |
81
+ | Void / required | `def process!` (suppresses implicit return) · `name! string` (required field) · `email!#` (required + unique) | Same `!` glyph, context-disambiguated |
82
+ | Math | `//` floor div · `%%` true mod · `1 < x < 10` chained compare · `arr[-1]` negative index · `"-" * 40` string repeat | Math you can read |
83
+ | Regex | `str =~ /re/` with `_[1]` captures · `str[/re/, 1]` · `///...///` heregex | Pattern matching as an expression |
84
+ | Assignment sugar | `.=` method-assign (`x .= trim()`) · `?.=` optional-chain assign · `*>obj = {a:1}` merge-assign | "Mutate this thing" |
85
+ | Data literals | `:name` interned Symbol · `*{...}` real Map · `%w[foo bar]` word array · `{a.b: 1}` dotted keys · `$"..."` tagged template | Real runtime values, not just syntax |
86
+ | Flow | `or return err` · `?? throw err` · `loop` · `loop 5` · `for x as iter` · `for x as! async` · postfix `a if x else b` · `x ? a : b` | Guards and iteration |
87
+
88
+ A few pieces of language behavior bear calling out:
89
+
90
+ - **Dammit (`!`) is the idiomatic async marker.** `fetch! url` compiles to `await fetch(url)`; `user.save!` to `await user.save()`. Raw `await` is reserved for JS interop.
91
+ - **Binary existential `x ? y` was intentionally removed.** Forces `x ?? y` (nullish coalescing). The full ternary `x ? a : b` still works.
92
+ - **Implicit commas before trailing arrows.** `get '/x' ->` becomes `get('/x', ->)`. A classic "rescue what would be a syntax error" trick — elegant for DSL-style call sites like route handlers.
93
+ - **Implicit `it` parameter.** `users.filter -> it.active` — no need to name a throwaway variable.
94
+
95
+ ### Honest tradeoffs
96
+
97
+ - **Density is real.** `!` means four different things depending on position; `:symbol` does double duty as symbol literal and as schema-kind selector. Individually fine; aggregated, the surface is non-trivial.
98
+ - **Denser is not automatically clearer** for newcomers. It *is* clearer for fluent users — the grid above is a teaching tool precisely because once you see it, you stop memorizing.
99
+ - **"Implicit commas" is parser magic.** It works well for the narrow DSL case it targets; it would be dangerous as a general pattern.
100
+
101
+ ---
102
+
103
+ ## 3. Reactivity — the `~` always family
104
+
105
+ Reactivity is the piece of Rip most often compared to React/Vue/Solid. The comparison is fair, but the framing should be:
106
+
107
+ | Concept | React | Vue | Solid | Rip |
108
+ |---|---|---|---|---|
109
+ | State | `useState()` | `ref()` | `createSignal()` | `x := 0` |
110
+ | Computed | `useMemo()` | `computed()` | `createMemo()` | `x ~= y * 2` |
111
+ | Effect | `useEffect()` | `watch()` | `createEffect()` | `~> body` |
112
+
113
+ Rip's forms are **not shorthand for a library API** — they are compiler-recognized syntax targeting a specific reactive runtime contract (`__state / __computed / __effect`). That distinction matters:
114
+
115
+ - The `~` "always" mnemonic is visible at the call site.
116
+ - Computed and effect are recognizable as the **reactive variants of `=` and `->`**, so users learn the family, not three unrelated names.
117
+ - Fine-grained DOM updates fall out of the runtime hooks; no diffing, no hook order, no dependency arrays.
118
+
119
+ ### What's strong
120
+
121
+ - `:=` is immediately legible as "state."
122
+ - `~=` / `~>` **name the contract**: "this value is always up to date" / "this function always runs when dependencies change." That's better than `createMemo` / `createEffect`, which name the API verb but not the guarantee.
123
+ - The family relationship is teachable in one sentence: *"`~` means always — `~=` is always-equal, `~>` is always-call."*
124
+
125
+ ### What still needs attention
126
+
127
+ The operator *names the contract* beautifully. The remaining work is making the *scope* of "always" obvious:
128
+
129
+ - **Which reads inside a `~>` / `~=` body are tracked?**
130
+ Per the component framework rule: inside `render`, only expressions rooted at `this` (`@app.data...`, component members) are tracked. Shared-scope variables render once and never update.
131
+ - **Aliasing boundaries.** The canonical pattern is to write `theme = @app.data.theme` inside a component to create a reactive root; bare references to shared-scope state are static.
132
+ - **Debuggability.** Answering *"why didn't this always-call fire?"* needs tooling — a lint, a devtool panel, or at minimum very explicit docs.
133
+
134
+ The operator design is well-chosen. The gap is runtime-semantics documentation and tooling, not syntax.
135
+
136
+ ---
137
+
138
+ ## 4. Rip Schema — one keyword, three libraries
139
+
140
+ One `schema` keyword covers what usually takes three libraries: a validator (Zod-style), an ORM (Prisma/ActiveRecord-style), and a migration tool. Five kinds, selected by a `:symbol`:
141
+
142
+ | Kind | Role |
143
+ |---|---|
144
+ | `:input` (default) | Field validator |
145
+ | `:shape` | Validator + methods + computed getters (e.g. `Money`, `Address`) |
146
+ | `:enum` | Closed symbol set; `.parse()` accepts name or value |
147
+ | `:mixin` | Reusable field bundle with diamond-dedup and cycle detection |
148
+ | `:model` | DB-backed — async ORM (`find/where/create/save/destroy`), DDL emission (`toSQL`), 10 Rails lifecycle hooks, `@belongs_to / @has_many / @has_one` relations |
149
+
150
+ ### Body is a declarative sub-DSL
151
+
152
+ Six line forms, all declarative:
153
+
154
+ 1. **Fields** — `name! type, min..max` (type optional, defaults to `string`)
155
+ 2. **Inline field transforms** — `name!, -> fn(it)` (comma-terminal; runs on `.parse()` only)
156
+ 3. **Directives** — `@timestamps`, `@mixin Name`, `@belongs_to User?`
157
+ 4. **Methods** — `name: -> body`
158
+ 5. **Computed getters** — `name: ~> body` (lazy; re-runs on every access)
159
+ 6. **Eager-derived fields** — `name: !> body` (materialized once at parse/hydrate, stored as own property)
160
+
161
+ Plus the cross-field refinement directive:
162
+
163
+ - **`@ensure "msg", (u) -> predicate`** — schema-level invariants, one per line or grouped as `@ensure [...]` with `msg, fn` pairs
164
+
165
+ Field modifiers: `!` required, `#` unique, `?` optional. Type slot is optional; omitting it means `string`. Constraints self-identify: `n..m` for ranges, `[value]` for defaults, `/regex/` for patterns, `{key: val}` for attrs. String-literal unions (`"M" | "F" | "U"`) substitute for small enum sets in the type slot.
166
+
167
+ ```coffee
168
+ # Validator with a cross-field refinement
169
+ SignupInput = schema
170
+ email! email
171
+ password! 8..100
172
+ password2! 8..100
173
+
174
+ @ensure "passwords must match", (u) -> u.password is u.password2
175
+
176
+ # Shape — validator with behavior
177
+ Address = schema :shape
178
+ street! string
179
+ city! string
180
+ full: ~> "#{@street}, #{@city}"
181
+
182
+ # Enumeration
183
+ Status = schema
184
+ :pending 0
185
+ :active 1
186
+ :done 2
187
+
188
+ # DB-backed model
189
+ User = schema :model
190
+ name! string
191
+ email!# email
192
+ role? "admin" | "user"
193
+ @timestamps
194
+ @has_many Order
195
+ beforeValidation: -> @email = @email.toLowerCase()
196
+ ```
197
+
198
+ ### The three-method runtime API
199
+
200
+ Every instantiable schema exposes the same trio, with async `!` variants:
201
+
202
+ - `.parse(data)` — returns cleaned value; throws `SchemaError` with structured `.issues`
203
+ - `.safe(data)` — returns `{ok, value, errors}`; never throws
204
+ - `.ok(data)` — boolean fast path; allocates no error arrays
205
+
206
+ ### Where `:model` converges
207
+
208
+ A single declaration gives you:
209
+
210
+ - A validator (the `.parse/.safe/.ok` trio)
211
+ - A class: fields as enumerable own properties, methods/getters on the prototype
212
+ - A chainable async query builder: `User.where(active: true).order("last_name").all!`
213
+ - Migration DDL: `User.toSQL()` — works standalone, never touches the DB
214
+ - `@belongs_to` / `@has_many` accessors that resolve cross-module through a process-global registry
215
+ - Full shadow TypeScript with `ModelSchema<Instance, Data>` typing that propagates through schema algebra
216
+
217
+ Hydrated instances carry **both snake_case and camelCase aliases** on DB-derived columns (`order.user_id === order.userId`), so raw-SQL helpers and ORM access coexist cleanly.
218
+
219
+ ### Architecture highlights
220
+
221
+ - **Single-function adapter.** `adapter.query(sql, params)` is the entire DB interface. Tests use in-memory mocks; production uses `rip-db`; the ORM doesn't care.
222
+ - **Schema algebra** — `.pick`, `.omit`, `.partial`, `.required`, `.extend` — always returns a `:shape` and **drops behavior**. `User.omit "password"` won't have `.find()` or the `beforeSave` hook. Enforced at runtime *and* at the TypeScript level.
223
+ - **Four-layer lazy runtime**: raw descriptor → normalized metadata → validator plan → ORM/DDL plan. Migration scripts never build the ORM plan; validator-only consumers never build the class machinery.
224
+ - **~54% sidecar** (`src/schema/schema.js`), with **<100 lines of core compiler wiring**.
225
+
226
+ ### Where it wins — and where it doesn't
227
+
228
+ Wins (genuinely):
229
+
230
+ - One source of truth — validator, domain class, DDL all derive from the same declaration.
231
+ - No drift between Zod input type and Prisma model type.
232
+ - Consistent `parse / safe / ok` contract across every shape, input, and model in your app.
233
+ - Lifecycle hooks and relations are tied to the same metadata model as validation.
234
+
235
+ Loses (honestly):
236
+
237
+ - **Couples domains that often evolve independently.** API input shape is not always a DB row; domain aggregate is not always a table. The `:input` / `:shape` / `:model` split mitigates this, but users will still try to push edge cases into it.
238
+ - **Schema algebra dropping to `:shape` is correct but surprising** — users will expect model-ness to survive. Very documentation-sensitive.
239
+ - **Process-global relation registry is a real architectural smell.** It creates real issues for test isolation, hot reload, multi-tenant runtimes in one process, plugin load order, circular init, and leak on re-registration. Needs to at minimum be namespace-scoped, idempotent, and resettable.
240
+ - **Single-function adapter is too thin once you need transactions.** `query(sql, params)` can't express `begin / commit / rollback`, savepoints, streaming results, connection-scoped settings, advisory locks, or capability introspection. MedLabs already wants transactions, so this interface will grow.
241
+
242
+ **Frame:** Rip Schema is a strong *coherence* play. It wins when validation and persistence models are intentionally close; it loses when you need backend-specific features or independent evolution of API / domain / storage schemas. *More Rails than Lego.*
243
+
244
+ ---
245
+
246
+ ## 5. MedLabs — architecture tour
247
+
248
+ **MedLabs** is a clinical lab-order portal for Labcorp test ordering, multi-tenant, SPA, built on the full Rip stack.
249
+
250
+ ### Layout
251
+
252
+ ```
253
+ index.rip Server entry — site resolution, middleware, SPA shell fallback
254
+ config.rip Env-derived app / DB / OAuth / Labcorp / Postmark config
255
+ api/
256
+ db.rip Adapter config + raw sql / rows / row helpers
257
+ models.rip 8 :model schemas (User, Patient, Provider, Account, Test,
258
+ Partner, Order, OrderItem) + 4 enums + createOrderWithItems
259
+ lib/ auth, labcorp, orders, email, migrate, stash, npi
260
+ routes/ auth, user, patients, tests, orders, labcorp
261
+ migrations/ Timestamped SQL (YYYYMMDDHHMMSS-*.sql)
262
+ scripts/ labcorp-token refresh, smoke test (in-memory adapter)
263
+ app/
264
+ index.html data-state, data-mount=App, data-src="ui app"
265
+ shell.rip SECTIONS registry, URL sync, session, App + AppShell
266
+ sections/ home, order-entry, orders, patients, test-catalog, settings
267
+ components/ auth-screen, onboarding, patient-search, test-panel, ...
268
+ sites/
269
+ common/ ola/ Per-tenant config + tailwind + public assets
270
+ ```
271
+
272
+ ### Nine design choices worth pointing out
273
+
274
+ 1. **Multi-tenancy via hostname → site bundle with deep-merge cascade.** `EXACT_HOSTS` / `SUFFIX_HOSTS` in `index.rip` map host → site id; `loadSiteBundle` deep-merges `sites/common/config.rip` with `sites/{site}/config.rip` (same for `tailwind.rip`). Bundle cached per site. Config is serialized as `data-state` on the bootstrap script tag and becomes `@app.data` reactively in components.
275
+
276
+ 2. **Stash = deep-path proxy over config.** Both `config.labcorp.api.patientUrl` and `config.get 'labcorp.api.patientUrl'` work. One `Proxy` with regex-based path detection handles both shapes.
277
+
278
+ 3. **DB bootstrap is delightfully aggressive.** `setup!` pings `http://localhost:4213/sql`, spawns `rip-db <file>` detached if absent, polls 10× 200ms, runs migrations. First-run UX is `bun apps/medlabs/index.rip` and nothing else. Great for dev; operationally weird for prod (worker races on start, orphan processes, unclear readiness).
279
+
280
+ 4. **Dual-auth API pattern.** `apiScope!` accepts either a Bearer partner token (validated against `partners.token` with regex `/^\d{8}-.{16}-(.+)$/`) *or* a session cookie. Returns `{mode: 'api' | 'session', partner | user}`. Partner path also updates `last_used_at`.
281
+
282
+ 5. **Labcorp token refresh loop.** Tokens live in a `labcorp_tokens` table, refreshed every 11h by `api/scripts/labcorp-token.rip` (cron), cached per-worker with a refresh buffer. JWT verify for Apple + Google is hand-rolled RSA-PKCS via `crypto.subtle` with JWK caching.
283
+
284
+ 6. **Order submission is a staged state machine with a `step` tracer.**
285
+ ```
286
+ patient → provider → account → validate → requisition → payload → submit
287
+ ```
288
+ Each step name lives in a `step` variable; `error!` throws `"Order failed at #{step}: …"` on trip. If the local DB insert fails *after* Labcorp accepts the order, the response returns a `dbError` field — **Labcorp is authoritative**, local DB reconciles.
289
+
290
+ 7. **`createOrderWithItems` factory.** Pre-computes `totalPrice = Σ items.price` because the FK forbids inserting `order_items` before the Order exists, so this can't be a `beforeSave` hook. Called from both `/v1/orders/create` (web) and `/v1/labcorp/orders` (partner API) to enforce the invariant at both entry points.
291
+
292
+ 8. **SPA chassis in `shell.rip`.** `SECTIONS` registers every screen with `{label, public?, icon?}`. `goTo(key)` is the router — pushes history, redirects unauth'd users to `/auth` while preserving intent via `pendingRedirect`. `refreshSession` on mount transitions `boot → ready | error`. Stash reactivity requires a component-local binding (`theme = @app.data.theme`) because only `this`-rooted expressions are tracked in `render`.
293
+
294
+ 9. **`test-panel.rip` is the reactivity showcase.** Clipboard-aware paste of 6-digit test codes → matching tests into cart, keyboard nav (↑↓ / Enter / Shift+Enter / Esc), reactive cart sort (`sortedCart ~=` switches on `cartSort` / `cartSortReverse`) — everything using `:=` signals and `~=` computed over `@app.data.cart`. A good concrete demonstration of the `~` family in anger.
295
+
296
+ ---
297
+
298
+ ## 6. Risks and open questions
299
+
300
+ ### MedLabs — healthcare correctness
301
+
302
+ | # | Risk | Severity | Fix direction |
303
+ |---|---|---|---|
304
+ | 1 | `orders.raw_request` / `raw_response` store upstream Labcorp payloads as TEXT — contains PHI (name, DOB, address, tests) and probably tokens in headers | **High** | Redact before persist, encrypt column, retention policy, explicit data classification |
305
+ | 2 | No idempotency key on order submission — retries will duplicate at Labcorp; local / remote state can diverge permanently with only a log-and-return-`dbError` recovery | **High** | Persist a pending submission record keyed by client-supplied idempotency token *before* the Labcorp call; unique constraint on `labcorp_order_id`; reconciliation worker |
306
+ | 3 | `upsertPatient` is where-then-save-or-create with a fallback catch — race on concurrent Labcorp submissions for the same patient | Medium | DB-native `INSERT … ON CONFLICT`; use `labcorp_id` unique constraint |
307
+ | 4 | No transactions in order creation — the 7-step flow has observable partial failures | Medium | Wrap the local write in a txn even if the Labcorp call stays outside |
308
+ | 5 | Hand-rolled JWT verify (Apple + Google) — "correct but risky" until externally reviewed (alg confusion, `nbf` / `exp` / `iat`, JWK cache invalidation, clock skew) | Medium | Write test vectors against the JOSE spec; consider a vetted micro-lib (violates zero-dep — tradeoff call) |
309
+ | 6 | Partner tokens stored plaintext for lookup (`WHERE token = $1`) | Medium | Prefix-lookup + hashed secret compare, display-once issuance |
310
+ | 7 | `x-site-override` header / `?__site` param is gated on `NODE_ENV !== 'production'` but relies on a single env var — classic footgun if staging mis-sets it | Low-Med | Whitelist hostnames that can override; deny-by-default; audit log |
311
+ | 8 | DuckDB auto-spawn pattern in a multi-worker server — worker race at boot | Low (dev-only risk) | Externalize DB process in prod; a lockfile or explicit "only worker 0 starts it" check |
312
+
313
+ ### Rip / Schema — architectural
314
+
315
+ - **Process-global schema registry.** Scope it, make it resettable, define load-order guarantees.
316
+ - **Adapter interface needs transactions.** `begin / commit / rollback` or a `withTransaction(fn)` variant.
317
+ - **Reactive tracking semantics need explicit docs + a lint.** What's tracked, when aliasing breaks tracking, how to ask *"why didn't this re-run?"*
318
+ - **Shadow TS drift.** The `.d.ts` emitted from `type` / `interface` and the runtime schema shape need conformance tests; if they diverge silently, IDE reality and runtime reality split.
319
+
320
+ ### SPA chassis — most likely failure mode under real traffic
321
+
322
+ Not "traffic" in the server sense. In the client, the most likely failure mode is **auth / navigation / bootstrap race causing incorrect screen state**:
323
+
324
+ - Hit private route with stale session → brief flash to target, auth, back.
325
+ - `pendingRedirect` overwritten or lost across async session-refresh completions.
326
+ - Browser back after expired session lands on an invalid intermediate state.
327
+ - `refreshSession` resolving after a route change and stomping the current section.
328
+ - Two concurrent refreshes producing inconsistent UI.
329
+
330
+ **Fix direction:** model auth / boot / route as an explicit state machine; make route transitions serial and cancellable; centralize `pendingRedirect` logic; test back / forward with slow network and expired session.
331
+
332
+ A close second is **non-reactive reads due to aliasing / stash access patterns** causing stale UI — the `theme = @app.data.theme` discipline must be lintable.
333
+
334
+ ---
335
+
336
+ ## 7. Verdict
337
+
338
+ **Strongest positive:** Rip Schema is the most differentiated piece of the whole stack. The four-layer lazy runtime + single-adapter contract + schema-algebra-drops-to-`:shape` design is genuinely novel. MedLabs' `models.rip` (182 lines) replaces what would be ~3 files and ~2 dependencies in a Node/TS app. The reactivity family (`:= / ~= / ~>`) isn't sigil soup — it's a teachable grid where `~` means *"always"* across value and function forms, making the contract visible at the call site in a way `createMemo` / `createEffect` can't.
339
+
340
+ **Strongest negative:** the system is **coherence-heavy and therefore fragile at integration boundaries**. When everything is designed together — compiler, runtime, schema, server, UI framework — it composes beautifully. When one boundary meets the messy world (Labcorp partial failures, DuckDB process lifecycle, hand-rolled JWT, PHI retention), the lack of battle-tested external primitives shows.
341
+
342
+ **Rip itself:** better for fluent users, and genuinely teachable once the grids are visible. The operator set *is* dense, but it isn't incoherent — `~` / `!` / `?` each mean something specific and consistent. The aggregate is a fairly opinionated language, not "small JS with better syntax."
343
+
344
+ **MedLabs specifically:** the biggest risk is **not code style or architecture — it's distributed consistency and PHI handling**. The app is productive and readable; it just needs to grow up on idempotency, transactions, and raw-payload hygiene before it carries real clinical traffic.
345
+
346
+ ---
347
+
348
+ ## Appendix — the teachable one-pagers
349
+
350
+ ### The assignment × function grid
351
+
352
+ | | value form | function form | meaning of the modifier |
353
+ |----------|-----------|-------------------|---------------------------|
354
+ | regular | `x = 5` | `f = -> …` | the boring default |
355
+ | bound | — | `f = => …` | lexical `this` |
356
+ | state | `x := 5` | — | observable state container|
357
+ | reactive | `x ~= …` | `f = ~> …` | `~` = **always** |
358
+
359
+ ### The `!` family
360
+
361
+ | Form | Meaning |
362
+ |---|---|
363
+ | `fetch!` | dammit — call + await |
364
+ | `def fn!` | void — suppress implicit return |
365
+ | `name! string` | required field (in `schema` body) |
366
+ | `email!#` | required + unique (in `schema :model` body) |
367
+ | `MAX =! 100` | readonly const |
368
+
369
+ ### The `?` family
370
+
371
+ | Form | Meaning |
372
+ |---|---|
373
+ | `x?` | existence (`x != null`) |
374
+ | `x ?? y` | nullish coalescing |
375
+ | `a?.b` / `a?.[0]` / `a?.()` | optional chain |
376
+ | `a?[0]` / `a?(x)` | optional chain shorthand |
377
+ | `el?.prop = v` | optional chain assignment |
378
+ | `@checked?!` | presence (Houdini — truthy or `undefined`) |
379
+ | `x ?? throw err` | nullish guard |
380
+
381
+ ### The schema declaration grid
382
+
383
+ | Line form | Example |
384
+ |---|---|
385
+ | Field (type implicit string) | `name! 1..50` |
386
+ | Field + modifiers | `email!# email` (required + unique) |
387
+ | Field + range | `password! 8..100` |
388
+ | Field + literal union | `sex? "M" \| "F" \| "U"` |
389
+ | Inline field transform | `email!, -> it.email.toLowerCase()` |
390
+ | Directive | `@timestamps`, `@has_many Order`, `@mixin Address` |
391
+ | Method | `toPublic: -> {id: @id, email: @email}` |
392
+ | Computed getter (lazy) | `fullName: ~> "#{@firstName} #{@lastName}"` |
393
+ | Eager-derived field | `slug: !> @name.toLowerCase()` |
394
+ | Cross-field refinement | `@ensure "passwords match", (u) -> u.password is u.password2` |
395
+
396
+ The body is data, not code — and that's what makes the whole thing compile into a validator, a class, a query builder, DDL, and TypeScript types at once.
package/docs/RIP-LANG.md CHANGED
@@ -1525,6 +1525,61 @@ Card = component
1525
1525
  Card title: "Hello", count: 42
1526
1526
  ```
1527
1527
 
1528
+ **Inherited Props (`extends <tag>`):**
1529
+
1530
+ A component can declare that it wraps a specific HTML element by writing
1531
+ `component extends <tag>`. Two things follow:
1532
+
1533
+ 1. **Type:** the component inherits the element's attribute type, so
1534
+ editor tooling lets parents pass `disabled`, `aria-label`,
1535
+ `data-id`, `@click`, etc., without each one being re-declared on the
1536
+ component.
1537
+ 2. **Runtime:** any prop the component does *not* declare itself is
1538
+ collected into a reactive `@rest` signal and **auto-spread onto the
1539
+ first matching tag** that appears in the render block.
1540
+
1541
+ ```coffee
1542
+ Button = component extends button
1543
+ @variant := "primary" # declared — handled by the component
1544
+ render
1545
+ button class: [@variant, @rest.class]
1546
+ slot
1547
+ ```
1548
+
1549
+ The `class:` attribute accepts an array (or object) and flattens it via
1550
+ the same `__clsx` runtime that powers `.card.("flex-1 p-4")` — so
1551
+ `@rest.class` is forwarded as-is whether the parent passed a string,
1552
+ an array, or a `{name: bool}` object.
1553
+
1554
+ ```coffee
1555
+ # Parent
1556
+ Button variant: "secondary", class: "mt-4", disabled: true, @click: save
1557
+ "Save"
1558
+ ```
1559
+
1560
+ What happens at the rendered `<button>`:
1561
+
1562
+ | Source | How it lands |
1563
+ | ----------------------------------- | ------------------------------------------------- |
1564
+ | `disabled: true`, `@click: save` | auto-spread from `@rest` (sync, runs first) |
1565
+ | `class: [...]` | explicit attr — runs second, last write wins |
1566
+
1567
+ This **spread-first** order is what makes class/style merging work: the
1568
+ parent's `class` enters via `@rest`, then the explicit `class: [...]`
1569
+ write overwrites it with an array that includes `@rest.class` plus the
1570
+ component's own parts. Any attribute the component does not touch
1571
+ (`disabled`, `aria-*`, `data-*`, event handlers) flows straight through.
1572
+
1573
+ Notes:
1574
+
1575
+ - `@rest` is a reactive `Signal` — reads in computeds and effects track
1576
+ it, and parent updates re-fire the consumers.
1577
+ - Auto-spread targets only the **first** element whose tag matches
1578
+ `extends <tag>`. If the render block has no matching tag, the rest
1579
+ props are still collected but never written.
1580
+ - Declared props (`@variant` above) are removed from `@rest` — the
1581
+ component owns them.
1582
+
1528
1583
  **Methods:**
1529
1584
 
1530
1585
  ```coffee
@@ -1538,12 +1593,11 @@ App = component
1538
1593
 
1539
1594
  ```coffee
1540
1595
  App = component
1541
- beforeMount = -> p "about to mount"
1542
- mounted = -> p "mounted"
1543
- updated = -> p "updated"
1596
+ beforeMount = -> p "about to mount"
1597
+ mounted = -> p "mounted"
1544
1598
  beforeUnmount = -> p "about to unmount"
1545
- unmounted = -> p "unmounted"
1546
- onError = (err, comp) -> p "caught: #{err.message}"
1599
+ unmounted = -> p "unmounted"
1600
+ onError = (err) -> p "caught: #{err.message}"
1547
1601
  ```
1548
1602
 
1549
1603
  **Effects:**