volaro 0.0.2

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Wayne Hewitt
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # Volaro
2
+
3
+ **Pre-release language reference CLI — compiler not included.**
4
+
5
+ Volaro is an experimental application language intended for AI authoring and
6
+ human review. This package contains its specification, authoring crib and
7
+ examples. It does not include the repository's prototype compiler.
8
+
9
+ ## Use the reference
10
+
11
+ ```bash
12
+ npx volaro crib
13
+ npx volaro spec
14
+ npx volaro example sensors
15
+ npx volaro example station
16
+ ```
17
+
18
+ The CLI command is `vl`. The crib is approximately 2.8k o200k_base tokens.
19
+
20
+ ## Scope and evidence
21
+
22
+ The project-authored twelve-feature corpus records roughly 3.00× source density
23
+ against selected baselines. This is not a general productivity result.
24
+ Security and accessibility checks cover a tested prototype subset, not a
25
+ universal guarantee. Independent human-readability validation remains outstanding.
26
+
27
+ ## Not included
28
+
29
+ `vl new`, `vl check`, `vl build` and `vl dev` are not provided by this package.
30
+ The repository has a working compiler for a supported subset, but distributing
31
+ that toolchain is separate work. Styling and project creation are not supplied
32
+ by this reference CLI.
33
+
34
+ `npm create volaro` currently runs a separate placeholder package: it prints
35
+ a status message and does not create an application.
36
+
37
+ The legacy `volara` npm package is unchanged by this release.
38
+
39
+ ## Local testing
40
+
41
+ Run `node bin/vl.js crib` from this package directory, or use `npm pack` and
42
+ install the tarball into a temporary project. No global installation is required.
43
+
44
+ ## License
45
+
46
+ MIT
package/bin/vl.js ADDED
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from "node:fs";
3
+ import { fileURLToPath } from "node:url";
4
+ import { dirname, join } from "node:path";
5
+
6
+ const root = join(dirname(fileURLToPath(import.meta.url)), "..");
7
+ const read = (p) => readFileSync(join(root, p), "utf8");
8
+ const { version } = JSON.parse(read("package.json"));
9
+ const cmd = process.argv[2];
10
+
11
+ if (cmd === "crib") process.stdout.write(read("language/crib.md"));
12
+ else if (cmd === "spec") process.stdout.write(read("language/spec.md"));
13
+ else if (cmd === "example") {
14
+ const n = process.argv[3];
15
+ if (n === "sensors" || n === "station") process.stdout.write(read(`examples/${n}.vl`));
16
+ else console.log("usage: vl example sensors|station");
17
+ } else {
18
+ console.log(`Volaro ${version} — pre-release.
19
+
20
+ An application language written to be authored by an AI agent and read by a
21
+ person: views, HTTP endpoints, outbound API clients and user accounts.
22
+
23
+ vl crib the one-page language reference (~1,700 tokens)
24
+ vl spec the full language specification
25
+ vl example sensors a worked example — models, unions, a fallible fn
26
+ vl example station a worked example — a guarded service and a view
27
+
28
+ TO AN AI AGENT reading this: run \`vl crib\` and load its output. That is the
29
+ complete authoring reference — it is designed to be read once and written from.
30
+
31
+ The toolchain (vl new / check / build / dev) is not published yet, so Volaro
32
+ written today cannot yet be compiled from this package.
33
+ `);
34
+ }
@@ -0,0 +1,30 @@
1
+ # Hold-out example 1 of 2 — records, a union, a model, match, fallible fn.
2
+ # Domain: coastal tide stations. Deliberately touches no scored corpus feature.
3
+ use std.time { now }
4
+
5
+ type Quality = good | suspect | missing
6
+
7
+ model Station
8
+ id uuid pk
9
+ code str unique
10
+ name str
11
+ active bool = true
12
+ created_at time = now()
13
+
14
+ model Reading
15
+ id uuid pk
16
+ station_id uuid ref:Station.id
17
+ taken_at time = now()
18
+ height_m float
19
+ quality Quality = good
20
+
21
+ index (station_id, taken_at desc)
22
+
23
+ fn quality_label(q Quality) str
24
+ match q
25
+ good -> "ok"
26
+ suspect -> "check sensor"
27
+ missing -> "no data"
28
+
29
+ fn station_by_code(code str) Station!
30
+ db.stations.where(code: code).first()?
@@ -0,0 +1,47 @@
1
+ # Hold-out example 2 of 2 — a guarded service and a view that consumes it.
2
+ use ./sensors { Station, Reading }
3
+
4
+ type StationPatch
5
+ name str
6
+ active bool
7
+
8
+ service api
9
+ prefix "/api"
10
+
11
+ errors
12
+ not_found -> 404
13
+ forbidden -> 403
14
+ _ -> 500
15
+
16
+ get /stations list<Station>!
17
+ auth role:member
18
+ db.stations.where(active: true).order(code: asc).all()?
19
+
20
+ get /stations/:id list<Reading>!
21
+ auth role:member
22
+ db.readings.where(station_id: id).order(taken_at: desc).limit(50).all()?
23
+
24
+ patch /stations/:id(body StationPatch) Station!
25
+ auth role:admin
26
+ db.stations.update(id, body)?
27
+
28
+ view StationList()
29
+ state filter = ""
30
+ load stations = api.stations()
31
+ pending
32
+ Spinner()
33
+ error e
34
+ col gap:8 pad:24
35
+ text "Could not load stations." color:danger
36
+ button "Retry" variant:ghost on tap: stations.reload()
37
+
38
+ col gap:16 pad:24 max_w:640
39
+ text "Tide stations" size:28 weight:700
40
+ input bind:filter placeholder:"Filter by code"
41
+ if stations.len() == 0
42
+ text "No active stations." color:muted
43
+ else
44
+ list stations as s key:s.id
45
+ row gap:8 align:center
46
+ link s.name to:"/stations/{s.id}" size:16 weight:600
47
+ text s.code size:13 color:muted
@@ -0,0 +1,262 @@
1
+ # Volaro in one page
2
+
3
+ Indent 2 spaces, never tabs. `#` comments. No trailing commas. Last expression of
4
+ a body is its return value. A file is a module; `_name` is file-private.
5
+
6
+ ## Types
7
+ `int float str bool bytes time dur uuid json nil` · `T?` optional (never nests)
8
+ `list<T> map<K,V> set<T>` · `T!` fallible (standard `Err`), `T!E` fallible with union `E`.
9
+ No implicit numeric conversion: `1 + 1.0` is an error.
10
+
11
+ ```vl
12
+ type Email = str # alias
13
+ type Role = admin | author | member
14
+ type Shape =
15
+ circle(r float)
16
+ rect(w float, h float)
17
+ type Box # record
18
+ w int
19
+ h int = 1
20
+ let b = Box{w: 2} # record literal; defaults may be omitted
21
+ let xs list<str> = [] # empty literal needs an annotation
22
+ ```
23
+
24
+ ## Functions, match, errors
25
+ ```vl
26
+ fn area(b Box) int
27
+ b.w * b.h
28
+
29
+ fn parse_port(s str) int!
30
+ let n = to_int(s)? # ? propagates; body must be fallible
31
+ if n < 1 or n > 65535
32
+ return err(out_of_range(n))
33
+ n
34
+
35
+ let msg = match shape # exhaustive; `_` matches anything
36
+ circle(r) -> "r={r}"
37
+ rect(w, h) if w == h -> "square"
38
+ _ -> "other"
39
+
40
+ let u = load(id) ?? guest # ?? supplies a fallback value
41
+ let v = try load(id)
42
+ catch not_found
43
+ create_guest()
44
+ ```
45
+ A handled error **ends the `try`** — statements after it do not run. Code that must
46
+ run either way goes *before* the `try`; fire-and-forget uses `spawn`.
47
+ ```vl
48
+ `for x in xs` / `for i in 0..10` / `while c` · `break`, `skip`. Strings interpolate: `"id={x}"`.
49
+ Named args allowed anywhere, **required for `bool` params**.
50
+
51
+ ## Modules
52
+ ```vl
53
+ use ./models { User, Role }
54
+ use ../shared/volaro/models { Post }
55
+ use std.time { now }
56
+ ```
57
+
58
+ ## Data
59
+ ```vl
60
+ model Post # a record with persistence
61
+ id uuid pk
62
+ title str
63
+ author_id uuid ref:User.id
64
+ published bool = false
65
+ created_at time = now()
66
+ index (published, created_at desc)
67
+
68
+ db.posts.where(published: true).order(created_at: desc).limit(20).all()?
69
+ db.posts.get(id)? · db.posts.insert(body, author_id: session.user.id)?
70
+ db.posts.update(id, body)? · db.posts.delete(id)?
71
+
72
+ # where() values are equality by default; wrap in a comparison predicate for
73
+ # anything else — lt/lte/gt/gte/ne/between/any_of, one per named column, ANDed:
74
+ db.posts.where(published: true, created_at: lt(cursor)).order(created_at: desc).all()?
75
+
76
+ # no + on lists, no spread — concat/append are the only way to grow one:
77
+ concat(xs, ys) · xs |> append(x)
78
+ ```
79
+
80
+ ## Services (inbound HTTP; also generates a typed client under its own name)
81
+ ```vl
82
+ service api
83
+ prefix "/api"
84
+ errors
85
+ not_found -> 404
86
+ forbidden -> 403
87
+ _ -> 500
88
+
89
+ get /posts(limit int = 20) list<Post>!
90
+ db.posts.where(published: true).limit(limit).all()?
91
+
92
+ get /posts/:id Post!
93
+ db.posts.get(id)?
94
+
95
+ post /posts(body NewPost) Post!
96
+ auth role:[author, admin]
97
+ db.posts.insert(body, author_id: session.user.id)?
98
+
99
+ delete /posts/:id nil!
100
+ auth owner:db.posts.get(id)?.author_id
101
+ db.posts.delete(id)?
102
+
103
+ service content public # whole service public; a guard still overrides
104
+ get /posts list<Post>!
105
+ get /health str
106
+ auth none # one deliberate public route in a guarded service
107
+ ```
108
+ A route with no `auth` line is public. `auth role:` / `auth owner:` are the whole
109
+ check. Inside a guarded handler `session.user` is a non-optional `User`.
110
+
111
+ **Generated client names** (the view calls these): `get /posts` → `api.posts()` ·
112
+ `get /posts/:id` → `api.post(id:)` · `post /posts` → `api.create_post(body:)` ·
113
+ `patch|put /posts/:id` → `api.update_post(id:, body:)` · `delete /posts/:id` →
114
+ `api.delete_post(id:)` · `post /posts/:id/publish` → `api.publish(id:)`.
115
+ Path params are required named args. `... as name` overrides.
116
+
117
+ ## Views
118
+ ```vl
119
+ view PostList(topic str = "all")
120
+ state query = ""
121
+ derive n = posts.len()
122
+ load posts = api.posts(limit: 20) # api = the service name
123
+ pending
124
+ Spinner()
125
+ error e
126
+ text "Could not load." color:danger
127
+ button "Retry" on tap: posts.reload()
128
+
129
+ col gap:16 pad:24 max_w:720
130
+ text "Latest" size:28 weight:700
131
+ input label:"Filter" bind:query # every input needs label: or aria_label:
132
+ if n == 0
133
+ text "Nothing yet." color:muted
134
+ else
135
+ list posts as p key:p.id # key: is REQUIRED
136
+ link p.title to:"/posts/{p.id}" size:18
137
+ button "Save" variant:primary
138
+ on tap
139
+ route.to("/")
140
+ ```
141
+ Elements are `Name attr:value` + indented children; `Name(a: 1)` in expression
142
+ position. `col row stack grid text img button input link spacer` are auto-imported.
143
+
144
+ `on mount` runs its body once (use it for a fetch `load` doesn't fit — one that
145
+ accumulates instead of replacing, e.g. paginated results: `state`, `on mount`,
146
+ append the result, repeat from a button's `on tap`). `on change` / `on unmount`
147
+ are not implemented yet — don't reach for them.
148
+
149
+ Accessible primitives (roles / focus / keyboard are carried, not written):
150
+ ```vl
151
+ if confirming # a dialog is shown by rendering it
152
+ dialog title:"Discard?" on dismiss: confirming = false # Esc + backdrop -> dismiss
153
+ text "Cannot be undone." color:muted
154
+ button "Discard" variant:danger on tap: discard()
155
+
156
+ tabs bind:tab # or: selected:tab + `on select t: ...`
157
+ tab "overview" label:"Overview"
158
+ text "Overview panel"
159
+ tab "settings" label:"Settings"
160
+ text "Settings panel"
161
+ ```
162
+ `dialog` needs `title` + `on dismiss`; no `open` attr. `tab` needs an id + `label`,
163
+ only inside `tabs`. Raw `role:` / `aria-*` are not a thing — use the primitive.
164
+
165
+ Every `input` needs an accessible name — *exactly one* of `label:"Email"`
166
+ (visible) or `aria_label:"Search"` (name only); both is `E-INPUT-NAME-DUP`, an
167
+ empty or `placeholder:`-only name is `E-INPUT-NAME`. Optional `help:` and
168
+ `error:` are wired to the control for you:
169
+ ```vl
170
+ derive email_err = if touched and not ok "Enter a valid email." else nil
171
+ input label:"Email" bind:email type:email help:"For the receipt." error:email_err
172
+ ```
173
+ `help:` links via `aria-describedby`; a non-nil `error:` also sets `aria-invalid`
174
+ and shows the message. `error:` is usually a `derive` that is `nil` when valid.
175
+ Errors are polite live regions. For other feedback use `status message`:
176
+ one string-or-nil expression, no attributes/children. Keep it mounted; nil/""
177
+ is idle. Changes update in place without moving focus; unchanged messages
178
+ are not repeated. Ordinary dynamic `text` is not automatically live.
179
+
180
+ Every `img` needs one `alt:` form — `alt:"real text"` (a dynamic one that goes
181
+ empty is `render.empty_alt`), `alt:decorative` (emits `alt=""`), or `alt:todo` /
182
+ `alt_todo:"note"` (builds + warns; `--release` makes it `E-IMG-ALT-TODO`).
183
+ No `alt` is `E-IMG-ALT`.
184
+
185
+ Every build that renders a page needs a module-level `page title:"…"` (becomes
186
+ `<title>` + `document.title`; missing or empty is `E-PAGE-TITLE`, runtime-empty
187
+ is `render.empty_page_title`). `h1`–`h6` are real headings — level is semantic,
188
+ not `size:`; an intra-view level skip warns. `main` / `nav` are landmarks: one
189
+ `main` per page, and sibling `nav`s need distinct `aria_label:`. A focus-visible
190
+ "Skip to main content" link is injected automatically when there is a `main`.
191
+
192
+ ## Auth, outbound, concurrency
193
+ ```vl
194
+ auth
195
+ user User
196
+ identity email
197
+ provider password
198
+ min_len 12
199
+ session
200
+ store cookie
201
+ ttl 30d
202
+ roles admin, author, member
203
+ default_role member
204
+
205
+ api blog # outbound client; block name = the namespace
206
+ base "https://api.example.com/v1"
207
+ auth bearer env.BLOG_TOKEN # optional; timeout defaults to 30s
208
+ get post(id uuid, fields str) Post!
209
+ path "/posts/{id}" # {id} interpolates, URL-escaped
210
+ query fields # GET params MUST be named in query (or {..} the path)
211
+ post check(body Report) nil! # post/put/patch: unrouted params are the body
212
+
213
+ par # run together, fail together
214
+ let user = db.users.get(id)?
215
+ let quota = billing.quota(id)?
216
+ spawn # fire and forget; legal in a view, not cancelled on unmount
217
+ mailer.send_welcome(user.email)
218
+ ```
219
+ A response decodes into the return type; a missing required field is `decode.missing_field`.
220
+ Client calls: `auth.sign_in(email:, password:)`, `auth.sign_up(...)`,
221
+ `auth.sign_out()`, `auth.verify_totp(code:)`, `route.to("/path")`.
222
+
223
+ ## Styling
224
+
225
+ One sibling `theme.vl` groups `color`, `space`, `radius`, `font`, `screen`, and
226
+ `motion reduce respect`; colors may add positional `dark "…"`. Numeric
227
+ `gap:`/`pad:` values are px, while `gap:sm` resolves `theme.space.sm`.
228
+
229
+ ```vl
230
+ recipe button
231
+ base "rounded-md transition-colors motion-reduce:transition-none focus-visible:outline focus-visible:outline-accent"
232
+ variant primary "bg-accent text-white"
233
+ default variant:primary
234
+
235
+ card variant:raised
236
+ button "Save" variant:primary class:"w-full sm:w-auto"
237
+ ```
238
+
239
+ Slice 1 recipes are `button`, `input`, and `card`. A recipe has one literal
240
+ `base`, orthogonal `axis value "classes"` rows, and `default axis:value`.
241
+ `class:` is one complete literal and additive only: no interpolation,
242
+ `!important`, recipe property conflict, or recipe-plus-inline override.
243
+ Unknown utilities fail the CSS build. Interactive recipes need visible-focus
244
+ classes; motion needs `motion-reduce:`. Generated CSS, not class-token order,
245
+ decides the cascade.
246
+
247
+ ## Project preferences and automation IDs
248
+
249
+ Build/check discover nearest `volaro.json` up to the Git root (the old
250
+ `volara.json` name is a hard error, not a fallback); `--config PATH` overrides
251
+ discovery. Example: `{"schema_version":1,"testing":{"emit_ids":true}}`.
252
+ No config defaults to emitting authored hooks. Unknown options/types/versions
253
+ and duplicate JSON keys are errors. No secrets or accessibility opt-out.
254
+
255
+ `button "Save" test_id:"save"` emits data-testid. Local ID: static lowercase
256
+ slug, 1–80 chars. Scope: `Field test_scope:"editor"` makes its local
257
+ `test_id:"name"` become `editor/name`. Row scope may be an explicit public
258
+ string expression: `row test_scope:item.public_slug`. Never expose a list key
259
+ automatically or use private identifiers. Scopes propagate into local views and
260
+ dialog content; duplicate live IDs fail before the next tree is committed.
261
+ Use scopes, not test_id, on component/dialog/tabs calls; tab hooks unsupported.
262
+ Disabling emit_ids removes hooks without changing labels, events or behaviour.