volaro 0.1.0-alpha.3 → 0.1.0-alpha.4
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/README.md +69 -7
- package/bin/vl.js +372 -15
- package/compiler/SOURCE_INFO.json +2 -2
- package/compiler/SOURCE_REV +1 -1
- package/compiler/validator/vlcheck/__main__.py +82 -13
- package/compiler/validator/vlcheck/ast_nodes.py +59 -0
- package/compiler/validator/vlcheck/benchmark_signal.py +86 -0
- package/compiler/validator/vlcheck/checks.py +1663 -18
- package/compiler/validator/vlcheck/diagnostics.py +1 -1
- package/compiler/validator/vlcheck/elements.py +1191 -0
- package/compiler/validator/vlcheck/layoutcompose.py +385 -0
- package/compiler/validator/vlcheck/lexer.py +41 -0
- package/compiler/validator/vlcheck/pagemanifest.py +389 -0
- package/compiler/validator/vlcheck/pageroutes.py +468 -0
- package/compiler/validator/vlcheck/parser.py +172 -0
- package/compiler/validator/vlcheck/resolve.py +69 -8
- package/compiler/validator/vlcheck/routes_cli.py +172 -0
- package/compiler/validator/vlcheck/test_ids.py +4 -3
- package/compiler/vlbuild/vlbuild/__main__.py +135 -9
- package/compiler/vlbuild/vlbuild/assets/vlrouter.js +396 -0
- package/compiler/vlbuild/vlbuild/assets/vlrt.js +279 -8
- package/compiler/vlbuild/vlbuild/emit.py +1547 -125
- package/compiler/vlbuild/vlbuild/pages_build.py +368 -0
- package/compiler/vlbuild/vlbuild/project.py +338 -0
- package/compiler/vlbuild/vlbuild/server_emit.py +595 -49
- package/compiler/vlbuild/vlbuild/style_config.py +1 -1
- package/language/crib.md +439 -19
- package/language/spec.md +495 -3
- package/language/supported.md +472 -18
- package/package.json +1 -1
- package/scripts/selftest.mjs +36 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Volaro
|
|
2
2
|
|
|
3
|
-
**Limited alpha, published
|
|
3
|
+
**Limited alpha, published under the `alpha` dist-tag** (this is `0.1.0-alpha.4`) —
|
|
4
4
|
`npm install volaro@alpha`. Not `latest` (that still resolves to an earlier
|
|
5
5
|
`0.0.x` placeholder) — always specify `@alpha` or the exact version.
|
|
6
6
|
|
|
@@ -59,9 +59,56 @@ volaro example sensors # a worked example (also: station)
|
|
|
59
59
|
volaro version | volaro help
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
+
### Benchmark timing log
|
|
63
|
+
|
|
64
|
+
`check` and `build` can append machine-readable compile attempts to a JSONL
|
|
65
|
+
file. Use one unique run ID for the first attempt and every repair attempt in a
|
|
66
|
+
single trial:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
volaro check app.vl --benchmark-log benchmark.jsonl --benchmark-run test-006-v-01
|
|
70
|
+
# If it fails, repair app.vl and repeat the exact command with the same run ID.
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
A first-pass success writes a `clean` summary with `t_clean_ms`. A failure
|
|
74
|
+
followed by a success writes a `recovered` summary with `t_fail_ms`,
|
|
75
|
+
`t_repair_ms`, `t_recompile_ms`, and `t_recover_ms`. The log also retains every
|
|
76
|
+
raw attempt, the number of failures, and compiler overhead not attributable to
|
|
77
|
+
repair. A completed run ID cannot be reused, and a run cannot mix `check` and
|
|
78
|
+
`build` attempts or change compiler arguments between attempts.
|
|
79
|
+
|
|
80
|
+
The timer measures the compiler subprocess. `T-repair` is the observed wall
|
|
81
|
+
time between a failed compiler exit and the next compiler start. It measures
|
|
82
|
+
the autonomous repair loop, including agent and tool overhead; it is not a
|
|
83
|
+
claim about model-inference time alone.
|
|
84
|
+
|
|
85
|
+
**Every failed attempt is classified, not just counted.** Each raw `attempt`
|
|
86
|
+
record carries a `classification`: `source_diagnostic` (the compiler ran to
|
|
87
|
+
completion and correctly reported a problem with your source — the only case
|
|
88
|
+
`t_fail_ms` is ever populated for), `operational_failure` (the compiler process
|
|
89
|
+
itself crashed, was killed, or stopped on an invocation/configuration problem —
|
|
90
|
+
`crash_exception_type`/`crash_exception_message` are set when it was a crash),
|
|
91
|
+
`silent_failure` (a non-zero exit with no output at all), or `unknown_failure`
|
|
92
|
+
(a non-zero exit with output that carried none of the compiler's own
|
|
93
|
+
diagnostic signal). This is reported by the compiler itself over a private
|
|
94
|
+
side channel — never guessed from whether stdout/stderr happened to have
|
|
95
|
+
anything in it, since an internal crash prints output too. A `recovered`
|
|
96
|
+
summary whose original failing attempt was not `source_diagnostic` has
|
|
97
|
+
`t_fail_ms: null`, `timing_complete: false`, and a `timing_incomplete_reason`
|
|
98
|
+
explaining why — treat it as an invalid recovery-time sample, not a slow one.
|
|
99
|
+
|
|
100
|
+
**A `recovered` summary can also be invalidated by the wall clock.**
|
|
101
|
+
`t_repair_ms`/`t_recover_ms` are computed from separate processes' own
|
|
102
|
+
timestamps; if one of those timestamps predates an earlier one (a system
|
|
103
|
+
clock step between two attempts), the summary has `clock_anomaly: true`,
|
|
104
|
+
`timing_complete: false`, and the affected fields are `null` — never a
|
|
105
|
+
clamped, misleadingly small positive number.
|
|
106
|
+
|
|
62
107
|
`volaro dev` serves a static bundle for a single-page app. If the app compiles
|
|
63
108
|
to a full-stack server (`server.js`), it runs that instead and needs
|
|
64
|
-
Node 22.5+ for `node:sqlite`.
|
|
109
|
+
Node 22.5+ for `node:sqlite`. A multi-page project build always emits a
|
|
110
|
+
`server.js` (it serves `public/` and answers a direct link or refresh on a
|
|
111
|
+
dynamic route), so `volaro dev` on a project directory runs that server.
|
|
65
112
|
|
|
66
113
|
## Scaffold a project
|
|
67
114
|
|
|
@@ -82,8 +129,20 @@ density against selected baselines — not a general productivity result.
|
|
|
82
129
|
Security and accessibility checks cover a tested prototype subset, not a
|
|
83
130
|
universal guarantee. Independent human-readability validation is outstanding.
|
|
84
131
|
|
|
85
|
-
**
|
|
86
|
-
|
|
132
|
+
**Pages, routing and nested layouts are implemented** (2026-09-23). A project
|
|
133
|
+
directory with any `app/**/index.vl` builds as a real multi-page app: nested
|
|
134
|
+
`_layout.vl` shells placing content with `@children`, `(group)` folders that add
|
|
135
|
+
layout ancestry but no URL segment, typed `[name=type]` dynamic parameters, and
|
|
136
|
+
optional `_loading.vl` / `_error.vl` / `_not-found.vl` boundaries, served by a
|
|
137
|
+
client router with a direct-link/refresh fallback in the generated server. Point
|
|
138
|
+
`volaro build` / `volaro dev` at the project directory; `volaro routes` prints
|
|
139
|
+
what was discovered. A single `.vl` entry file still builds exactly as before.
|
|
140
|
+
|
|
141
|
+
**Still out of scope:** server-side rendering and hydration (the shell is
|
|
142
|
+
route-agnostic and the client does the first render); `theme` / `recipe` styling
|
|
143
|
+
in a multi-page build, so styled output is single-entry only; catch-all routes,
|
|
144
|
+
per-file route prefixes, and inherited authorization. **`npm create volaro`
|
|
145
|
+
scaffolds a single-page starter** — a multi-page starter is later work.
|
|
87
146
|
|
|
88
147
|
## Not included
|
|
89
148
|
|
|
@@ -93,9 +152,12 @@ builds (`theme` / `recipe`) additionally need a one-time
|
|
|
93
152
|
`npm ci` inside `compiler/vlbuild/styling`; the single-page starter does not
|
|
94
153
|
use them.
|
|
95
154
|
|
|
96
|
-
**Form controls:**
|
|
97
|
-
|
|
98
|
-
|
|
155
|
+
**Form controls:** `input` (a full type matrix, including `checkbox`/`radio`
|
|
156
|
+
binding a `checked` state), `textarea`, `select`/`option`/`optgroup`,
|
|
157
|
+
`fieldset`/`legend`, `datalist`, `button`, `link`. `disabled:` works on
|
|
158
|
+
`button input textarea select fieldset optgroup option`. There is no
|
|
159
|
+
standalone `label` element and no semantic `switch`/`aria-pressed` toggle —
|
|
160
|
+
model one as a `button` reading a `state` bool. `variant:` is a literal
|
|
99
161
|
style name, not a computed expression. `if` used as an expression is binary
|
|
100
162
|
(`if c a else b`); `match` and block `if` are statements only. A module-level
|
|
101
163
|
`fn` is not available to a view. **`volaro supported` and `volaro crib` are
|
package/bin/vl.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// volaro check <path>... validate .vl source (lexer / parser / resolver)
|
|
6
6
|
// volaro build <file.vl> transpile to a runnable bundle
|
|
7
7
|
// volaro dev [file.vl] build, serve on localhost, rebuild on change
|
|
8
|
+
// volaro routes [project] print discovered pages and the page-server route manifest
|
|
8
9
|
// volaro crib | volaro spec print the language reference
|
|
9
10
|
// volaro example <name> print a worked example
|
|
10
11
|
// volaro version | volaro help
|
|
@@ -12,7 +13,7 @@
|
|
|
12
13
|
// Python is located and version-checked up front with an actionable message
|
|
13
14
|
// (see ../lib/env.js). Nothing here depends on the Volaro repository layout.
|
|
14
15
|
|
|
15
|
-
import { readFileSync, existsSync, statSync, lstatSync, readdirSync } from "node:fs";
|
|
16
|
+
import { appendFileSync, readFileSync, existsSync, statSync, lstatSync, readdirSync } from "node:fs";
|
|
16
17
|
import { fileURLToPath } from "node:url";
|
|
17
18
|
import { dirname, join, resolve as resolvePath, extname, relative, sep, delimiter } from "node:path";
|
|
18
19
|
import { spawn, spawnSync } from "node:child_process";
|
|
@@ -72,12 +73,334 @@ function pyInvocation(pp, moduleName, moduleArgs) {
|
|
|
72
73
|
return { cmd: py.cmd, argv: ["-B", "-m", moduleName, ...moduleArgs], env };
|
|
73
74
|
}
|
|
74
75
|
|
|
76
|
+
function elapsedMs(start) {
|
|
77
|
+
return Number(process.hrtime.bigint() - start) / 1e6;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function roundedMs(value) {
|
|
81
|
+
return Math.round(value * 1000) / 1000;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function benchmarkRecords(path, runId) {
|
|
85
|
+
if (!existsSync(path)) return [];
|
|
86
|
+
let raw;
|
|
87
|
+
try {
|
|
88
|
+
const st = statSync(path);
|
|
89
|
+
if (st.isDirectory()) {
|
|
90
|
+
fail(`${CLI}: benchmark log ${path} is a directory, not a file`);
|
|
91
|
+
}
|
|
92
|
+
raw = readFileSync(path, "utf8");
|
|
93
|
+
} catch (err) {
|
|
94
|
+
fail(`${CLI}: cannot read benchmark log ${path}: ${err.message}`);
|
|
95
|
+
}
|
|
96
|
+
return raw
|
|
97
|
+
.split(/\r?\n/)
|
|
98
|
+
.filter(Boolean)
|
|
99
|
+
.map((line, index) => {
|
|
100
|
+
try {
|
|
101
|
+
return JSON.parse(line);
|
|
102
|
+
} catch {
|
|
103
|
+
fail(`${CLI}: invalid JSONL in benchmark log ${path} at line ${index + 1}: ` +
|
|
104
|
+
`${JSON.stringify(line.length > 80 ? line.slice(0, 80) + "…" : line)}`);
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
.filter((row) => row.schema === "volaro.compile-timing.v1" && row.run_id === runId);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function appendBenchmark(path, record) {
|
|
111
|
+
try {
|
|
112
|
+
appendFileSync(path, JSON.stringify(record) + "\n", { encoding: "utf8", flag: "a" });
|
|
113
|
+
} catch (err) {
|
|
114
|
+
fail(`${CLI}: cannot append benchmark log ${path}: ${err.message}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function ensureBenchmarkLog(path) {
|
|
119
|
+
try {
|
|
120
|
+
appendFileSync(path, "", { encoding: "utf8", flag: "a" });
|
|
121
|
+
} catch (err) {
|
|
122
|
+
fail(`${CLI}: cannot open benchmark log ${path}: ${err.message}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function compilerProvenance() {
|
|
127
|
+
try {
|
|
128
|
+
return JSON.parse(readFileSync(join(COMPILER, "SOURCE_INFO.json"), "utf8"));
|
|
129
|
+
} catch {
|
|
130
|
+
return {};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function parseBenchmarkArgs(args, command) {
|
|
135
|
+
let log = null;
|
|
136
|
+
let runId = null;
|
|
137
|
+
let logCount = 0;
|
|
138
|
+
let runCount = 0;
|
|
139
|
+
const compilerArgs = [];
|
|
140
|
+
for (let i = 0; i < args.length; i++) {
|
|
141
|
+
const arg = args[i];
|
|
142
|
+
if (arg === "--benchmark-log" || arg === "--benchmark-run") {
|
|
143
|
+
if (i + 1 >= args.length || args[i + 1].startsWith("--")) {
|
|
144
|
+
fail(`${CLI} ${command}: ${arg} requires a value`, 2);
|
|
145
|
+
}
|
|
146
|
+
if (arg === "--benchmark-log") {
|
|
147
|
+
log = args[++i];
|
|
148
|
+
logCount++;
|
|
149
|
+
} else {
|
|
150
|
+
runId = args[++i];
|
|
151
|
+
runCount++;
|
|
152
|
+
}
|
|
153
|
+
} else if (arg.startsWith("--benchmark-log=")) {
|
|
154
|
+
log = arg.slice("--benchmark-log=".length);
|
|
155
|
+
logCount++;
|
|
156
|
+
} else if (arg.startsWith("--benchmark-run=")) {
|
|
157
|
+
runId = arg.slice("--benchmark-run=".length);
|
|
158
|
+
runCount++;
|
|
159
|
+
} else {
|
|
160
|
+
compilerArgs.push(arg);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if ((log === null) !== (runId === null)) {
|
|
164
|
+
fail(`${CLI} ${command}: --benchmark-log and --benchmark-run must be supplied together`, 2);
|
|
165
|
+
}
|
|
166
|
+
if (logCount > 1 || runCount > 1) {
|
|
167
|
+
fail(`${CLI} ${command}: benchmark options may be supplied only once`, 2);
|
|
168
|
+
}
|
|
169
|
+
if (log !== null && log.trim() === "") {
|
|
170
|
+
fail(`${CLI} ${command}: --benchmark-log must not be empty`, 2);
|
|
171
|
+
}
|
|
172
|
+
if (runId !== null && runId.trim() === "") {
|
|
173
|
+
fail(`${CLI} ${command}: --benchmark-run must not be empty`, 2);
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
args: compilerArgs,
|
|
177
|
+
benchmark: log === null ? null : { log: resolvePath(log), runId },
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Four possible outcomes for a FAILED attempt (never called for exitCode 0).
|
|
182
|
+
// This is the entire classification: it never inspects captured stdout/
|
|
183
|
+
// stderr TEXT, only (a) whether the OS delivered a kill signal and (b)
|
|
184
|
+
// the compiler's own structural report of what kind of stop it made (see
|
|
185
|
+
// `vlcheck/benchmark_signal.py`'s module docstring). "Any output happened"
|
|
186
|
+
// is deliberately NOT a diagnostic signal on its own -- an internal
|
|
187
|
+
// traceback is output too.
|
|
188
|
+
function classifyAttempt(signal, benchmarkSignal, firstOutputMs) {
|
|
189
|
+
if (signal) return "operational_failure"; // killed by the OS, never a diagnostic
|
|
190
|
+
if (benchmarkSignal) {
|
|
191
|
+
if (benchmarkSignal.kind === "crash") return "operational_failure";
|
|
192
|
+
if (benchmarkSignal.kind === "diagnostic") {
|
|
193
|
+
// Each module's OWN reserved "diagnostics found in valid input" code
|
|
194
|
+
// is exactly 1 (see vlcheck/vlbuild __main__.py); any other code the
|
|
195
|
+
// compiler intentionally stopped with (a usage/configuration error,
|
|
196
|
+
// for instance) is a real, honest stop but not a SOURCE diagnostic.
|
|
197
|
+
return benchmarkSignal.exit_code === 1 ? "source_diagnostic" : "operational_failure";
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// No structured signal reached us at all: the compiler crashed before
|
|
201
|
+
// reaching its own signal-emission wrapper (an import-time failure, an
|
|
202
|
+
// interpreter that never started, a segfault) or the side channel was
|
|
203
|
+
// lost. Distinguished only by whether anything was captured on stdout/
|
|
204
|
+
// stderr, not by reading it.
|
|
205
|
+
return firstOutputMs === null ? "silent_failure" : "unknown_failure";
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function writeBenchmarkResult(benchmark, command, compilerArgv, code, signal, startedAt,
|
|
209
|
+
startedMono, firstOutputMs, benchmarkSignal) {
|
|
210
|
+
const finishedEpochMs = Date.now();
|
|
211
|
+
const durationMs = elapsedMs(startedMono);
|
|
212
|
+
const prior = benchmarkRecords(benchmark.log, benchmark.runId);
|
|
213
|
+
if (prior.some((row) => row.record === "summary")) {
|
|
214
|
+
fail(`${CLI}: benchmark run ${JSON.stringify(benchmark.runId)} is already complete`, 2);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const attempts = prior.filter((row) => row.record === "attempt");
|
|
218
|
+
const exitCode = signal ? 1 : code ?? 0;
|
|
219
|
+
const provenance = compilerProvenance();
|
|
220
|
+
const classification = exitCode === 0 ? null : classifyAttempt(signal, benchmarkSignal, firstOutputMs);
|
|
221
|
+
const attempt = {
|
|
222
|
+
schema: "volaro.compile-timing.v1",
|
|
223
|
+
record: "attempt",
|
|
224
|
+
run_id: benchmark.runId,
|
|
225
|
+
attempt: attempts.length + 1,
|
|
226
|
+
command,
|
|
227
|
+
compiler_argv: compilerArgv,
|
|
228
|
+
compiler_version: pkg.version,
|
|
229
|
+
compiler_revision: provenance.revision || null,
|
|
230
|
+
compiler_content_sha256: provenance.content_sha256 || null,
|
|
231
|
+
started_at: startedAt,
|
|
232
|
+
finished_at: new Date(finishedEpochMs).toISOString(),
|
|
233
|
+
exit_code: exitCode,
|
|
234
|
+
signal: signal || null,
|
|
235
|
+
outcome: exitCode === 0 ? "success" : "failure",
|
|
236
|
+
classification,
|
|
237
|
+
duration_ms: roundedMs(durationMs),
|
|
238
|
+
// Raw fact, any classification: did output arrive, and when -- kept
|
|
239
|
+
// even for a crash/silent/unknown failure as the "available output
|
|
240
|
+
// metadata" a human investigating an invalid run needs.
|
|
241
|
+
first_output_ms: firstOutputMs === null ? null : roundedMs(firstOutputMs),
|
|
242
|
+
// Only ever populated for a genuine source diagnostic -- T-fail's
|
|
243
|
+
// whole point is "how long until the compiler told you about YOUR
|
|
244
|
+
// bug", which an operational/silent/unknown failure never did.
|
|
245
|
+
first_diagnostic_ms: classification === "source_diagnostic" && firstOutputMs !== null
|
|
246
|
+
? roundedMs(firstOutputMs) : null,
|
|
247
|
+
crash_exception_type: benchmarkSignal && benchmarkSignal.kind === "crash"
|
|
248
|
+
? benchmarkSignal.exception_type : null,
|
|
249
|
+
crash_exception_message: benchmarkSignal && benchmarkSignal.kind === "crash"
|
|
250
|
+
? benchmarkSignal.exception_message : null,
|
|
251
|
+
};
|
|
252
|
+
appendBenchmark(benchmark.log, attempt);
|
|
253
|
+
|
|
254
|
+
if (exitCode !== 0) return;
|
|
255
|
+
|
|
256
|
+
const allAttempts = [...attempts, attempt];
|
|
257
|
+
const failures = allAttempts.filter((row) => row.outcome === "failure");
|
|
258
|
+
if (failures.length === 0) {
|
|
259
|
+
appendBenchmark(benchmark.log, {
|
|
260
|
+
schema: "volaro.compile-timing.v1",
|
|
261
|
+
record: "summary",
|
|
262
|
+
run_id: benchmark.runId,
|
|
263
|
+
command,
|
|
264
|
+
outcome: "clean",
|
|
265
|
+
timing_complete: true,
|
|
266
|
+
timing_incomplete_reason: null,
|
|
267
|
+
clock_anomaly: false,
|
|
268
|
+
attempt_count: 1,
|
|
269
|
+
failed_attempts: 0,
|
|
270
|
+
t_clean_ms: attempt.duration_ms,
|
|
271
|
+
t_fail_ms: null,
|
|
272
|
+
t_repair_ms: null,
|
|
273
|
+
t_recompile_ms: null,
|
|
274
|
+
t_recover_ms: null,
|
|
275
|
+
compiler_overhead_ms: null,
|
|
276
|
+
});
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Wall-clock gaps between separate process invocations, computed from
|
|
281
|
+
// each process's own Date.now()-derived timestamps -- vulnerable to a
|
|
282
|
+
// backward system clock step (an NTP sync) between two attempts. A
|
|
283
|
+
// negative gap is never clamped to zero and reported as if it were a
|
|
284
|
+
// real, small repair time: that would silently corrupt the median/p90
|
|
285
|
+
// aggregation STAGE3-PROTOCOL.md §6.1 defines over this data. Instead
|
|
286
|
+
// the whole run is marked invalid for recovery-time comparison, with
|
|
287
|
+
// every raw timestamp still preserved untouched in the attempts above.
|
|
288
|
+
let repairMs = 0;
|
|
289
|
+
let clockAnomaly = false;
|
|
290
|
+
for (let i = 1; i < allAttempts.length; i++) {
|
|
291
|
+
const previous = allAttempts[i - 1];
|
|
292
|
+
if (previous.outcome === "failure") {
|
|
293
|
+
const gap = Date.parse(allAttempts[i].started_at) - Date.parse(previous.finished_at);
|
|
294
|
+
if (gap < 0) clockAnomaly = true;
|
|
295
|
+
repairMs += gap;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const first = allAttempts[0];
|
|
299
|
+
const recoverMs = finishedEpochMs - Date.parse(first.started_at);
|
|
300
|
+
if (recoverMs < 0) clockAnomaly = true;
|
|
301
|
+
|
|
302
|
+
const firstIsDiagnostic = first.classification === "source_diagnostic";
|
|
303
|
+
const failMs = firstIsDiagnostic ? first.first_diagnostic_ms : null;
|
|
304
|
+
const timingComplete = firstIsDiagnostic && !clockAnomaly;
|
|
305
|
+
const compilerOverheadMs = timingComplete
|
|
306
|
+
? recoverMs - failMs - repairMs - attempt.duration_ms
|
|
307
|
+
: null;
|
|
308
|
+
|
|
309
|
+
let timingIncompleteReason = null;
|
|
310
|
+
if (!firstIsDiagnostic) {
|
|
311
|
+
timingIncompleteReason = `the original failing attempt was classified ` +
|
|
312
|
+
`${JSON.stringify(first.classification)}, not "source_diagnostic" -- ` +
|
|
313
|
+
`t_fail_ms and compiler_overhead_ms are not valid recovery-time samples for this run`;
|
|
314
|
+
} else if (clockAnomaly) {
|
|
315
|
+
timingIncompleteReason = "a wall-clock timestamp in this run predates an earlier one " +
|
|
316
|
+
"(a backward system clock step between two compiler invocations) -- " +
|
|
317
|
+
"t_repair_ms/t_recover_ms/compiler_overhead_ms are not valid recovery-time samples for this run";
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
appendBenchmark(benchmark.log, {
|
|
321
|
+
schema: "volaro.compile-timing.v1",
|
|
322
|
+
record: "summary",
|
|
323
|
+
run_id: benchmark.runId,
|
|
324
|
+
command,
|
|
325
|
+
outcome: "recovered",
|
|
326
|
+
timing_complete: timingComplete,
|
|
327
|
+
timing_incomplete_reason: timingIncompleteReason,
|
|
328
|
+
clock_anomaly: clockAnomaly,
|
|
329
|
+
attempt_count: allAttempts.length,
|
|
330
|
+
failed_attempts: failures.length,
|
|
331
|
+
t_clean_ms: null,
|
|
332
|
+
t_fail_ms: failMs,
|
|
333
|
+
t_repair_ms: clockAnomaly ? null : roundedMs(repairMs),
|
|
334
|
+
t_recompile_ms: attempt.duration_ms,
|
|
335
|
+
t_recover_ms: clockAnomaly ? null : roundedMs(recoverMs),
|
|
336
|
+
compiler_overhead_ms: timingComplete ? roundedMs(Math.max(0, compilerOverheadMs)) : null,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
75
340
|
// Run a Python module, streaming its output, and exit with its status.
|
|
76
|
-
function runPython(pp, moduleName, moduleArgs) {
|
|
341
|
+
function runPython(pp, moduleName, moduleArgs, benchmark = null, command = moduleName) {
|
|
77
342
|
const { cmd, argv, env } = pyInvocation(pp, moduleName, moduleArgs);
|
|
78
|
-
|
|
343
|
+
if (!benchmark) {
|
|
344
|
+
const child = spawn(cmd, argv, { stdio: "inherit", env });
|
|
345
|
+
child.on("error", (e) => fail(`${CLI}: failed to start Python (${cmd}): ${e.message}`));
|
|
346
|
+
child.on("exit", (code, signal) => process.exit(signal ? 1 : code ?? 0));
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
ensureBenchmarkLog(benchmark.log);
|
|
351
|
+
const existing = benchmarkRecords(benchmark.log, benchmark.runId);
|
|
352
|
+
if (existing.some((row) => row.record === "summary")) {
|
|
353
|
+
fail(`${CLI}: benchmark run ${JSON.stringify(benchmark.runId)} is already complete`, 2);
|
|
354
|
+
}
|
|
355
|
+
if (existing.some((row) => row.record === "attempt" && row.command !== command)) {
|
|
356
|
+
fail(`${CLI}: benchmark run ${JSON.stringify(benchmark.runId)} cannot mix check and build`, 2);
|
|
357
|
+
}
|
|
358
|
+
const compilerArgv = [moduleName, ...moduleArgs];
|
|
359
|
+
if (existing.some((row) => row.record === "attempt" &&
|
|
360
|
+
JSON.stringify(row.compiler_argv) !== JSON.stringify(compilerArgv))) {
|
|
361
|
+
fail(`${CLI}: benchmark run ${JSON.stringify(benchmark.runId)} must reuse the exact compiler command`, 2);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const startedEpochMs = Date.now();
|
|
365
|
+
const startedAt = new Date(startedEpochMs).toISOString();
|
|
366
|
+
const startedMono = process.hrtime.bigint();
|
|
367
|
+
let firstOutputMs = null;
|
|
368
|
+
// fd 3: a private side channel the compiler writes its own structural
|
|
369
|
+
// diagnostic/crash signal to (see vlcheck/benchmark_signal.py) -- never
|
|
370
|
+
// stdout/stderr, so ordinary output forwarded to the terminal below is
|
|
371
|
+
// untouched by this. VOLARO_BENCHMARK_SIGNAL_FD tells the compiler that
|
|
372
|
+
// channel exists; it is a documented no-op when unset, which is exactly
|
|
373
|
+
// every non-benchmark invocation.
|
|
374
|
+
const child = spawn(cmd, argv, {
|
|
375
|
+
stdio: ["inherit", "pipe", "pipe", "pipe"],
|
|
376
|
+
env: { ...env, PYTHONUNBUFFERED: "1", VOLARO_BENCHMARK_SIGNAL_FD: "3" },
|
|
377
|
+
});
|
|
378
|
+
const forward = (stream, target) => stream.on("data", (chunk) => {
|
|
379
|
+
if (firstOutputMs === null) firstOutputMs = elapsedMs(startedMono);
|
|
380
|
+
target.write(chunk);
|
|
381
|
+
});
|
|
382
|
+
forward(child.stdout, process.stdout);
|
|
383
|
+
forward(child.stderr, process.stderr);
|
|
384
|
+
const signalChunks = [];
|
|
385
|
+
child.stdio[3].on("data", (chunk) => signalChunks.push(chunk));
|
|
79
386
|
child.on("error", (e) => fail(`${CLI}: failed to start Python (${cmd}): ${e.message}`));
|
|
80
|
-
child.on("
|
|
387
|
+
child.on("close", (code, signal) => {
|
|
388
|
+
let benchmarkSignal = null;
|
|
389
|
+
const signalRaw = Buffer.concat(signalChunks).toString("utf8").trim();
|
|
390
|
+
if (signalRaw) {
|
|
391
|
+
// The compiler writes exactly one JSON line and closes the stream
|
|
392
|
+
// (benchmark_signal.py); take the first line defensively rather than
|
|
393
|
+
// assuming that held.
|
|
394
|
+
try {
|
|
395
|
+
benchmarkSignal = JSON.parse(signalRaw.split(/\r?\n/)[0]);
|
|
396
|
+
} catch {
|
|
397
|
+
benchmarkSignal = null; // malformed signal -> falls through to unknown_failure
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
writeBenchmarkResult(benchmark, command, compilerArgv, code, signal, startedAt,
|
|
401
|
+
startedMono, firstOutputMs, benchmarkSignal);
|
|
402
|
+
process.exit(signal ? 1 : code ?? 0);
|
|
403
|
+
});
|
|
81
404
|
}
|
|
82
405
|
|
|
83
406
|
function pyCapture(pp, moduleName, moduleArgs) {
|
|
@@ -88,9 +411,10 @@ function pyCapture(pp, moduleName, moduleArgs) {
|
|
|
88
411
|
// ---- subcommands -----------------------------------------------------------
|
|
89
412
|
|
|
90
413
|
function cmdCheck(args) {
|
|
91
|
-
|
|
414
|
+
const parsed = parseBenchmarkArgs(args, "check");
|
|
415
|
+
if (parsed.args.length === 0) fail("usage: volaro check <file-or-dir>... [--release] [--config PATH]");
|
|
92
416
|
// Always resolve: a bare structural pass is weaker than the real check.
|
|
93
|
-
runPython(VALIDATOR_PP, "vlcheck", ["--resolve", ...args]);
|
|
417
|
+
runPython(VALIDATOR_PP, "vlcheck", ["--resolve", ...parsed.args], parsed.benchmark, "check");
|
|
94
418
|
}
|
|
95
419
|
|
|
96
420
|
function parseBuildArgs(args) {
|
|
@@ -123,14 +447,15 @@ function resolveEntry(explicit) {
|
|
|
123
447
|
}
|
|
124
448
|
|
|
125
449
|
function cmdBuild(args) {
|
|
126
|
-
const
|
|
450
|
+
const parsed = parseBenchmarkArgs(args, "build");
|
|
451
|
+
const b = parseBuildArgs(parsed.args);
|
|
127
452
|
const source = resolveEntry(b.source);
|
|
128
453
|
const outDir = b.o ? resolvePath(b.o) : resolvePath("build");
|
|
129
454
|
const pyArgs = [source, "-o", outDir];
|
|
130
455
|
if (b.data) pyArgs.push("--data", resolvePath(b.data));
|
|
131
456
|
if (b.config) pyArgs.push("--config", resolvePath(b.config));
|
|
132
457
|
if (b.release) pyArgs.push("--release");
|
|
133
|
-
runPython(VLBUILD_PP, "vlbuild", pyArgs);
|
|
458
|
+
runPython(VLBUILD_PP, "vlbuild", pyArgs, parsed.benchmark, "build");
|
|
134
459
|
}
|
|
135
460
|
|
|
136
461
|
// Synchronous single build for `volaro dev`; returns { ok, server } or exits on env error.
|
|
@@ -263,7 +588,9 @@ function cmdDev(args) {
|
|
|
263
588
|
if (!Number.isInteger(port) || port < 1 || port > 65535) fail("volaro dev: --port must be 1..65535");
|
|
264
589
|
const entry = resolveEntry(source);
|
|
265
590
|
const out = outDir ? resolvePath(outDir) : resolvePath("build");
|
|
266
|
-
|
|
591
|
+
// A project build's entry is the project directory itself (`volaro dev .`);
|
|
592
|
+
// watching its dirname would watch the PARENT directory instead.
|
|
593
|
+
const watchDir = statSync(entry).isDirectory() ? entry : dirname(entry);
|
|
267
594
|
|
|
268
595
|
process.stdout.write(`volaro dev: building ${relative(process.cwd(), entry) || entry}\n`);
|
|
269
596
|
const first = buildOnce(entry, out);
|
|
@@ -388,6 +715,20 @@ function cmdExample(name) {
|
|
|
388
715
|
else fail("usage: volaro example sensors|station");
|
|
389
716
|
}
|
|
390
717
|
|
|
718
|
+
function cmdRoutes(args) {
|
|
719
|
+
let json = false;
|
|
720
|
+
let project = null;
|
|
721
|
+
for (const a of args) {
|
|
722
|
+
if (a === "--json") json = true;
|
|
723
|
+
else if (a.startsWith("-")) fail(`usage: volaro routes [project] [--json]`, 2);
|
|
724
|
+
else if (!project) project = a;
|
|
725
|
+
else fail(`usage: volaro routes [project] [--json]`, 2);
|
|
726
|
+
}
|
|
727
|
+
const pyArgs = [resolvePath(project || ".")];
|
|
728
|
+
if (json) pyArgs.push("--json");
|
|
729
|
+
runPython(VALIDATOR_PP, "vlcheck.routes_cli", pyArgs);
|
|
730
|
+
}
|
|
731
|
+
|
|
391
732
|
const HELP = `Volaro ${pkg.version} — an application language for AI authoring and human review.
|
|
392
733
|
|
|
393
734
|
USAGE
|
|
@@ -397,15 +738,27 @@ USAGE
|
|
|
397
738
|
COMMANDS
|
|
398
739
|
check <path>... Validate .vl source. Runs the full resolver, not just
|
|
399
740
|
a structural pass. Exit non-zero on any error.
|
|
741
|
+
--benchmark-log <file> append raw timing records as JSONL
|
|
742
|
+
--benchmark-run <id> group attempts into one clean/recovery trial
|
|
400
743
|
build <file.vl> Transpile to a runnable bundle (default: ./build).
|
|
401
744
|
-o <dir> output directory
|
|
402
745
|
--data <file> data.json to bundle
|
|
403
746
|
--release fail on unresolved placeholders (e.g. img alt_todo:)
|
|
747
|
+
--benchmark-log <file> append raw timing records as JSONL
|
|
748
|
+
--benchmark-run <id> group attempts into one clean/recovery trial
|
|
404
749
|
dev [file.vl] Build, serve on http://127.0.0.1:5173, and rebuild
|
|
405
750
|
when a .vl file changes. Defaults to ./app.vl. A
|
|
406
751
|
full-stack build runs server.js instead (Node 22.5+)
|
|
407
752
|
and restarts it after each successful rebuild.
|
|
408
753
|
--port <n> dev server port (passed to server.js as $PORT)
|
|
754
|
+
routes [project] Print discovered browser pages (spec §8.15: URL,
|
|
755
|
+
source, layout chain, boundaries) and the
|
|
756
|
+
page-server route manifest (spec §10.2: final URL,
|
|
757
|
+
method, source file, parameter types, generated
|
|
758
|
+
client member, authorization). Project defaults to
|
|
759
|
+
".". Exits non-zero on any collision or project-
|
|
760
|
+
validity error (page/route or layout-composition).
|
|
761
|
+
--json emit the language-neutral manifest as JSON
|
|
409
762
|
crib The authoring reference (write from this).
|
|
410
763
|
supported What version ${pkg.version} actually accepts.
|
|
411
764
|
spec The full language design (a SUPERSET of this build).
|
|
@@ -416,12 +769,13 @@ COMMANDS
|
|
|
416
769
|
The compiler is Python and ships inside this package. Set VOLARO_PYTHON to
|
|
417
770
|
choose the interpreter if \`python3\` is not the one you want.
|
|
418
771
|
|
|
419
|
-
SHIPPED SUBSET: single page (no router);
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
772
|
+
SHIPPED SUBSET: single page (no router); \`input\` (full type matrix incl.
|
|
773
|
+
checkbox/radio), \`textarea\`, \`select\`/\`option\`/\`optgroup\`,
|
|
774
|
+
\`fieldset\`/\`legend\`, \`datalist\`, \`button\`, \`link\` (no standalone
|
|
775
|
+
\`label\`, no semantic switch/aria-pressed toggle); \`if\` as an expression is
|
|
776
|
+
binary; \`match\` is a statement; a module-level \`fn\` cannot be called from a
|
|
777
|
+
view. \`volaro crib\` + \`volaro supported\` are authoritative for this build;
|
|
778
|
+
\`volaro spec\` is the wider design, not what it accepts.
|
|
425
779
|
|
|
426
780
|
Run commands via your project's \`npm run …\` scripts or \`npx --no-install
|
|
427
781
|
volaro …\` inside the project. A bare \`npx volaro …\` from elsewhere may fetch
|
|
@@ -444,6 +798,9 @@ switch (cmd) {
|
|
|
444
798
|
case "dev":
|
|
445
799
|
cmdDev(rest);
|
|
446
800
|
break;
|
|
801
|
+
case "routes":
|
|
802
|
+
cmdRoutes(rest);
|
|
803
|
+
break;
|
|
447
804
|
case "crib":
|
|
448
805
|
cmdReference("crib");
|
|
449
806
|
break;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"revision": "
|
|
2
|
+
"revision": "0d5741da1385dd81e277b16715a93592744d8c87",
|
|
3
3
|
"source": "git",
|
|
4
4
|
"dirty": false,
|
|
5
|
-
"content_sha256": "
|
|
5
|
+
"content_sha256": "611292080bf797022be28aea42376ca55bbeba3e680c2ec638ae4a1417981ff0"
|
|
6
6
|
}
|
package/compiler/SOURCE_REV
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
0d5741da1385dd81e277b16715a93592744d8c87
|