tablinum 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/.changeset/README.md +8 -0
  2. package/.changeset/config.json +11 -0
  3. package/.context/attachments/pasted_text_2026-03-07_14-02-40.txt +571 -0
  4. package/.context/attachments/pasted_text_2026-03-07_15-48-27.txt +498 -0
  5. package/.context/notes.md +0 -0
  6. package/.context/plans/add-changesets-to-douala-v4.md +48 -0
  7. package/.context/plans/dexie-js-style-query-language-for-localstr.md +115 -0
  8. package/.context/plans/dexie-js-style-query-language-with-per-collection-.md +336 -0
  9. package/.context/plans/implementation-plan-localstr-v0-2.md +263 -0
  10. package/.context/plans/project-init-effect-v4-bun-oxlint-oxfmt-vitest.md +71 -0
  11. package/.context/plans/revise-localstr-prd-v0-2.md +132 -0
  12. package/.context/plans/svelte-5-runes-bindings-for-localstr.md +233 -0
  13. package/.context/todos.md +0 -0
  14. package/.github/workflows/release.yml +36 -0
  15. package/.oxlintrc.json +8 -0
  16. package/README.md +1 -0
  17. package/bun.lock +705 -0
  18. package/examples/svelte/bun.lock +261 -0
  19. package/examples/svelte/package.json +21 -0
  20. package/examples/svelte/src/app.html +11 -0
  21. package/examples/svelte/src/lib/db.ts +44 -0
  22. package/examples/svelte/src/routes/+page.svelte +322 -0
  23. package/examples/svelte/svelte.config.js +16 -0
  24. package/examples/svelte/tsconfig.json +6 -0
  25. package/examples/svelte/vite.config.ts +6 -0
  26. package/examples/vanilla/app.ts +219 -0
  27. package/examples/vanilla/index.html +144 -0
  28. package/examples/vanilla/serve.ts +42 -0
  29. package/package.json +46 -0
  30. package/prds/localstr-v0.2.md +221 -0
  31. package/prek.toml +10 -0
  32. package/scripts/validate.ts +392 -0
  33. package/src/crud/collection-handle.ts +189 -0
  34. package/src/crud/query-builder.ts +414 -0
  35. package/src/crud/watch.ts +78 -0
  36. package/src/db/create-localstr.ts +217 -0
  37. package/src/db/database-handle.ts +16 -0
  38. package/src/db/identity.ts +49 -0
  39. package/src/errors.ts +37 -0
  40. package/src/index.ts +32 -0
  41. package/src/main.ts +10 -0
  42. package/src/schema/collection.ts +53 -0
  43. package/src/schema/field.ts +25 -0
  44. package/src/schema/types.ts +19 -0
  45. package/src/schema/validate.ts +111 -0
  46. package/src/storage/events-store.ts +24 -0
  47. package/src/storage/giftwraps-store.ts +23 -0
  48. package/src/storage/idb.ts +244 -0
  49. package/src/storage/lww.ts +17 -0
  50. package/src/storage/records-store.ts +76 -0
  51. package/src/svelte/collection.svelte.ts +87 -0
  52. package/src/svelte/database.svelte.ts +83 -0
  53. package/src/svelte/index.svelte.ts +52 -0
  54. package/src/svelte/live-query.svelte.ts +29 -0
  55. package/src/svelte/query.svelte.ts +101 -0
  56. package/src/sync/gift-wrap.ts +33 -0
  57. package/src/sync/negentropy.ts +83 -0
  58. package/src/sync/publish-queue.ts +61 -0
  59. package/src/sync/relay.ts +239 -0
  60. package/src/sync/sync-service.ts +183 -0
  61. package/src/sync/sync-status.ts +17 -0
  62. package/src/utils/uuid.ts +22 -0
  63. package/src/vendor/negentropy.js +616 -0
  64. package/tests/db/create-localstr.test.ts +174 -0
  65. package/tests/db/identity.test.ts +33 -0
  66. package/tests/main.test.ts +9 -0
  67. package/tests/schema/collection.test.ts +27 -0
  68. package/tests/schema/field.test.ts +41 -0
  69. package/tests/schema/validate.test.ts +85 -0
  70. package/tests/setup.ts +1 -0
  71. package/tests/storage/idb.test.ts +144 -0
  72. package/tests/storage/lww.test.ts +33 -0
  73. package/tests/sync/gift-wrap.test.ts +56 -0
  74. package/tsconfig.json +18 -0
  75. package/vitest.config.ts +8 -0
@@ -0,0 +1,8 @@
1
+ # Changesets
2
+
3
+ Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
4
+ with multi-package repos, or single-package repos to help you version and publish your code. You can
5
+ find the full documentation for it [in our repository](https://github.com/changesets/changesets).
6
+
7
+ We have a quick list of common questions to get you started engaging with this project in
8
+ [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md).
@@ -0,0 +1,11 @@
1
+ {
2
+ "$schema": "https://unpkg.com/@changesets/config@3.1.3/schema.json",
3
+ "changelog": "@changesets/cli/changelog",
4
+ "commit": false,
5
+ "fixed": [],
6
+ "linked": [],
7
+ "access": "public",
8
+ "baseBranch": "main",
9
+ "updateInternalDependencies": "patch",
10
+ "ignore": []
11
+ }
@@ -0,0 +1,571 @@
1
+ import { exec, type ChildProcess } from 'node:child_process';
2
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
3
+ import { basename, dirname, join } from 'node:path';
4
+
5
+ interface ValidationError {
6
+ tool: string;
7
+ file?: string;
8
+ line?: number;
9
+ column?: number;
10
+ message: string;
11
+ code?: string;
12
+ severity: 'error' | 'warning';
13
+ }
14
+
15
+ interface ValidationResult {
16
+ success: boolean;
17
+ errors: ValidationError[];
18
+ summary: string;
19
+ duration_ms: number;
20
+ stopped_early: boolean;
21
+ next_action?: string;
22
+ }
23
+
24
+ const MAX_ERRORS = 3;
25
+ const E2E_SKIP_RUNTIME = process.env.VALIDATE_SKIP_E2E_RUNTIME === '1';
26
+ const ANSI_ESCAPE_PATTERN = new RegExp(String.raw`\u001B\[[0-9;?]*[A-Za-z]`, 'g');
27
+
28
+ // Shared state for fail-fast behavior
29
+ let collectedErrors: ValidationError[] = [];
30
+ let failedTools: string[] = [];
31
+ let aborted = false;
32
+ const runningProcesses: ChildProcess[] = [];
33
+
34
+ function abort() {
35
+ aborted = true;
36
+ // Kill all running child processes
37
+ for (const proc of runningProcesses) {
38
+ proc.kill('SIGTERM');
39
+ }
40
+ }
41
+
42
+ function addErrors(toolName: string, errors: ValidationError[]): boolean {
43
+ if (aborted || errors.length === 0) return aborted;
44
+
45
+ if (!failedTools.includes(toolName)) {
46
+ failedTools.push(toolName);
47
+ }
48
+
49
+ for (const error of errors) {
50
+ if (collectedErrors.length >= MAX_ERRORS) {
51
+ abort();
52
+ return true;
53
+ }
54
+ collectedErrors.push(error);
55
+ }
56
+
57
+ if (collectedErrors.length >= MAX_ERRORS) {
58
+ abort();
59
+ return true;
60
+ }
61
+
62
+ return false;
63
+ }
64
+
65
+ function run(cmd: string): Promise<{ stdout: string; stderr: string; success: boolean }> {
66
+ return new Promise((resolve) => {
67
+ if (aborted) {
68
+ resolve({ stdout: '', stderr: '', success: false });
69
+ return;
70
+ }
71
+
72
+ const proc = exec(
73
+ cmd,
74
+ { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 },
75
+ (error, stdout, stderr) => {
76
+ const idx = runningProcesses.indexOf(proc);
77
+ if (idx !== -1) runningProcesses.splice(idx, 1);
78
+
79
+ if (aborted) {
80
+ resolve({ stdout: '', stderr: '', success: false });
81
+ return;
82
+ }
83
+
84
+ resolve({
85
+ stdout: stdout ?? '',
86
+ stderr: stderr ?? '',
87
+ success: !error
88
+ });
89
+ }
90
+ );
91
+
92
+ runningProcesses.push(proc);
93
+ });
94
+ }
95
+
96
+ function summarizeFailureOutput(text: string): string | undefined {
97
+ const lines = normalizeOutputLines(text);
98
+
99
+ if (lines.length === 0) return undefined;
100
+
101
+ const prioritized = lines.find((line) =>
102
+ /(^error\b|failed|ELIFECYCLE|\[WebServer\])/i.test(line)
103
+ );
104
+ return prioritized ?? lines[0];
105
+ }
106
+
107
+ function normalizeOutputLines(text: string): string[] {
108
+ return text
109
+ .split('\n')
110
+ .map((line) => line.replace(ANSI_ESCAPE_PATTERN, '').trim())
111
+ .filter((line) => line.length > 0);
112
+ }
113
+
114
+ function truncateText(text: string, maxLength = 220): string {
115
+ return text.length <= maxLength ? text : `${text.slice(0, maxLength - 1)}...`;
116
+ }
117
+
118
+ function summarizePlaywrightFailure(text: string): string | undefined {
119
+ const lines = normalizeOutputLines(text);
120
+ const failureIndex = lines.findIndex((line) => /^\d+\)\s+\[[^\]]+\]\s+›/.test(line));
121
+ const failureLine = failureIndex >= 0 ? lines[failureIndex] : undefined;
122
+ const detailLine =
123
+ (failureIndex >= 0 &&
124
+ lines
125
+ .slice(failureIndex + 1)
126
+ .find((line) => /^Error:/i.test(line) || /expect\(.+\)\..+\sfailed/i.test(line))) ??
127
+ lines.find((line) => /^Error:/i.test(line));
128
+
129
+ if (failureLine && detailLine) {
130
+ return truncateText(`${failureLine} :: ${detailLine}`);
131
+ }
132
+
133
+ if (failureLine) {
134
+ return truncateText(failureLine);
135
+ }
136
+
137
+ return undefined;
138
+ }
139
+
140
+ function countListedTestsForProject(output: string, projectName: string): number {
141
+ return output
142
+ .split('\n')
143
+ .map((line) => line.trim())
144
+ .filter((line) => line.startsWith(`[${projectName}]`) && line.includes('›')).length;
145
+ }
146
+
147
+ async function runTestCoverage(): Promise<ValidationError[]> {
148
+ if (aborted) return [];
149
+ const errors: ValidationError[] = [];
150
+
151
+ const { stdout } = await run('git ls-files "src/**/*.ts" "src/**/*.svelte"');
152
+ if (aborted) return [];
153
+
154
+ const sourceFiles = stdout.split('\n').filter(Boolean);
155
+
156
+ const skipPatterns = [
157
+ /\.test\.ts$/,
158
+ /\.spec\.ts$/,
159
+ /\.d\.ts$/,
160
+ /index\.ts$/,
161
+ /types\.ts$/,
162
+ /\.styles\.ts$/,
163
+ /\.stories\.svelte$/,
164
+ /\.test-wrapper\.svelte$/,
165
+ /\+error\.svelte$/,
166
+ /\/data\/Data\.svelte$/,
167
+ /\.remote\.ts$/,
168
+ /\/server\/auth\.ts$/,
169
+ /\/server\/access\.ts$/,
170
+ /\/server\/auth-guards\.ts$/,
171
+ /\/server\/db\/schema\//,
172
+ /\/testing\/mocks\.ts$/,
173
+ /\+layout\.(server\.)?ts$/,
174
+ /\+layout\.svelte$/,
175
+ /\+page\.server\.ts$/,
176
+ /\+page\.svelte$/,
177
+ /\+page\.ts$/
178
+ ];
179
+
180
+ for (const file of sourceFiles) {
181
+ if (aborted) break;
182
+ if (skipPatterns.some((p) => p.test(file))) continue;
183
+
184
+ const dir = dirname(file);
185
+ const name = basename(file).replace(/\.(ts|svelte)$/, '');
186
+ const alternateName = name.startsWith('+') ? name.slice(1) : null;
187
+
188
+ const testPaths = [
189
+ join(dir, `${name}.test.ts`),
190
+ join(dir, `${name}.spec.ts`),
191
+ join(dir, `${name}.svelte.test.ts`),
192
+ join(dir, `${name}.svelte.spec.ts`),
193
+ join(dir, `${name}.ssr.test.ts`),
194
+ join(dir, `${name}.ssr.spec.ts`),
195
+ join(dir, '__tests__', `${name}.test.ts`),
196
+ join(dir, '__tests__', `${name}.spec.ts`)
197
+ ];
198
+ if (alternateName) {
199
+ testPaths.push(
200
+ join(dir, `${alternateName}.test.ts`),
201
+ join(dir, `${alternateName}.spec.ts`),
202
+ join(dir, '__tests__', `${alternateName}.test.ts`),
203
+ join(dir, '__tests__', `${alternateName}.spec.ts`)
204
+ );
205
+ }
206
+
207
+ const hasTest = testPaths.some((p) => existsSync(p));
208
+
209
+ if (!hasTest) {
210
+ errors.push({
211
+ tool: 'test-coverage',
212
+ file,
213
+ severity: 'error',
214
+ message: `Missing test file. Expected: ${name}.test.ts${alternateName ? ` or ${alternateName}.test.ts` : ''}`
215
+ });
216
+ }
217
+ }
218
+
219
+ return errors;
220
+ }
221
+
222
+ async function runPrettier(): Promise<ValidationError[]> {
223
+ if (aborted) return [];
224
+ const errors: ValidationError[] = [];
225
+ const { stdout, success } = await run(
226
+ 'pnpm exec prettier --check --plugin prettier-plugin-svelte .'
227
+ );
228
+
229
+ if (aborted) return [];
230
+
231
+ if (!success) {
232
+ for (const line of stdout.split('\n')) {
233
+ if (line.startsWith('[warn]') || line.includes('Forgot to run') || line.includes('Checking'))
234
+ continue;
235
+ if (line.trim()) {
236
+ errors.push({
237
+ tool: 'prettier',
238
+ file: line.trim(),
239
+ severity: 'error',
240
+ message: 'File needs formatting. Run: pnpm exec prettier --write ' + line.trim()
241
+ });
242
+ }
243
+ }
244
+ }
245
+
246
+ return errors;
247
+ }
248
+
249
+ async function runOxlint(): Promise<ValidationError[]> {
250
+ if (aborted) return [];
251
+ const errors: ValidationError[] = [];
252
+ const { stdout } = await run('pnpm exec oxlint --format json');
253
+
254
+ if (aborted) return [];
255
+
256
+ try {
257
+ // oxlint outputs JSON with diagnostics array
258
+ const result = JSON.parse(stdout);
259
+ for (const diagnostic of result.diagnostics ?? []) {
260
+ if (diagnostic.severity === 'error') {
261
+ const label = diagnostic.labels?.[0];
262
+ errors.push({
263
+ tool: 'oxlint',
264
+ file: diagnostic.filename,
265
+ line: label?.span?.line,
266
+ column: label?.span?.column,
267
+ code: diagnostic.code,
268
+ severity: 'error',
269
+ message: diagnostic.message
270
+ });
271
+ }
272
+ }
273
+ } catch {
274
+ // JSON parse failed, no errors to report
275
+ }
276
+
277
+ return errors;
278
+ }
279
+
280
+ async function runSvelteCheck(): Promise<ValidationError[]> {
281
+ if (aborted) return [];
282
+ const errors: ValidationError[] = [];
283
+ const { stdout } = await run('pnpm exec svelte-check --output machine-verbose');
284
+
285
+ if (aborted) return [];
286
+
287
+ for (const line of stdout.split('\n')) {
288
+ const match = line.match(/^(\w+)\s+(.+):(\d+):(\d+)\s+(.+)$/);
289
+ if (match && match[1] === 'ERROR') {
290
+ errors.push({
291
+ tool: 'svelte-check',
292
+ file: match[2],
293
+ line: parseInt(match[3]),
294
+ column: parseInt(match[4]),
295
+ severity: 'error',
296
+ message: match[5]
297
+ });
298
+ }
299
+ }
300
+
301
+ return errors;
302
+ }
303
+
304
+ async function runTypeScript(): Promise<ValidationError[]> {
305
+ if (aborted) return [];
306
+ const errors: ValidationError[] = [];
307
+ const { stdout } = await run('pnpm exec tsc --noEmit --pretty false');
308
+
309
+ if (aborted) return [];
310
+
311
+ for (const line of stdout.split('\n')) {
312
+ const match = line.match(/^(.+)\((\d+),(\d+)\):\s+error\s+(TS\d+):\s+(.+)$/);
313
+ if (match) {
314
+ errors.push({
315
+ tool: 'tsc',
316
+ file: match[1],
317
+ line: parseInt(match[2]),
318
+ column: parseInt(match[3]),
319
+ code: match[4],
320
+ severity: 'error',
321
+ message: match[5]
322
+ });
323
+ }
324
+ }
325
+
326
+ return errors;
327
+ }
328
+
329
+ const E2E_REQUIRED_ROUTES = ['/', '/login', '/login/check-email', '/account', '/dashboard'];
330
+ const E2E_MIN_CHROMIUM_TESTS = 10;
331
+ const E2E_MIN_CHROMIUM_AUTH_TESTS = 1;
332
+
333
+ async function runE2ECoverage(): Promise<ValidationError[]> {
334
+ if (aborted) return [];
335
+ const errors: ValidationError[] = [];
336
+ const e2eDir = 'e2e';
337
+
338
+ if (!existsSync(e2eDir)) {
339
+ errors.push({
340
+ tool: 'e2e-coverage',
341
+ severity: 'error',
342
+ message: 'Missing e2e/ directory. Run: mkdir e2e'
343
+ });
344
+ return errors;
345
+ }
346
+
347
+ // Run Playwright discovery for chromium.
348
+ const {
349
+ stdout: chromiumListStdout,
350
+ stderr: chromiumListStderr,
351
+ success: chromiumListSuccess
352
+ } = await run('pnpm exec playwright test --project=chromium --list');
353
+ if (aborted) return [];
354
+
355
+ if (!chromiumListSuccess) {
356
+ const output = `${chromiumListStderr}\n${chromiumListStdout}`.trim();
357
+ const firstLine = summarizeFailureOutput(output);
358
+ errors.push({
359
+ tool: 'e2e-coverage',
360
+ severity: 'error',
361
+ message: `Playwright chromium discovery failed${firstLine ? `: ${firstLine}` : ''}`
362
+ });
363
+ return errors;
364
+ }
365
+
366
+ const chromiumTotal = countListedTestsForProject(chromiumListStdout, 'chromium');
367
+
368
+ if (chromiumTotal < E2E_MIN_CHROMIUM_TESTS) {
369
+ errors.push({
370
+ tool: 'e2e-coverage',
371
+ severity: 'error',
372
+ message: `Chromium E2E test count too low (${chromiumTotal}). Minimum required: ${E2E_MIN_CHROMIUM_TESTS}`
373
+ });
374
+ }
375
+
376
+ // Ensure authenticated project is still covered.
377
+ const {
378
+ stdout: chromiumAuthListStdout,
379
+ stderr: chromiumAuthListStderr,
380
+ success: chromiumAuthListSuccess
381
+ } = await run('pnpm exec playwright test --project=chromium-auth --list');
382
+ if (aborted) return [];
383
+
384
+ if (!chromiumAuthListSuccess) {
385
+ const output = `${chromiumAuthListStderr}\n${chromiumAuthListStdout}`.trim();
386
+ const firstLine = summarizeFailureOutput(output);
387
+ errors.push({
388
+ tool: 'e2e-coverage',
389
+ severity: 'error',
390
+ message: `Playwright chromium-auth discovery failed${firstLine ? `: ${firstLine}` : ''}`
391
+ });
392
+ return errors;
393
+ }
394
+
395
+ const chromiumAuthTotal = countListedTestsForProject(chromiumAuthListStdout, 'chromium-auth');
396
+
397
+ if (chromiumAuthTotal < E2E_MIN_CHROMIUM_AUTH_TESTS) {
398
+ errors.push({
399
+ tool: 'e2e-coverage',
400
+ severity: 'error',
401
+ message: `Chromium-auth E2E test count too low (${chromiumAuthTotal}). Minimum required: ${E2E_MIN_CHROMIUM_AUTH_TESTS}`
402
+ });
403
+ }
404
+
405
+ const testFiles: string[] = [];
406
+ const walk = (dir: string) => {
407
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
408
+ const fullPath = join(dir, entry.name);
409
+ if (entry.isDirectory()) {
410
+ walk(fullPath);
411
+ continue;
412
+ }
413
+ if (entry.isFile() && entry.name.endsWith('.spec.ts')) {
414
+ testFiles.push(fullPath);
415
+ }
416
+ }
417
+ };
418
+ walk(e2eDir);
419
+
420
+ if (testFiles.length === 0) {
421
+ errors.push({
422
+ tool: 'e2e-coverage',
423
+ severity: 'error',
424
+ message: 'No E2E test files found in e2e/ directory'
425
+ });
426
+ return errors;
427
+ }
428
+
429
+ const testedRoutes = new Set<string>();
430
+ for (const file of testFiles) {
431
+ const content = readFileSync(file, 'utf-8');
432
+ const matches = content.matchAll(/page\.goto\(['"]([^'"]+)['"]\)/g);
433
+ for (const match of matches) {
434
+ testedRoutes.add(match[1]);
435
+ }
436
+ }
437
+
438
+ for (const route of E2E_REQUIRED_ROUTES) {
439
+ if (!testedRoutes.has(route)) {
440
+ errors.push({
441
+ tool: 'e2e-coverage',
442
+ severity: 'error',
443
+ message: `Missing E2E test for critical route: ${route}`
444
+ });
445
+ }
446
+ }
447
+
448
+ return errors;
449
+ }
450
+
451
+ async function runE2EChromium(): Promise<ValidationError[]> {
452
+ if (aborted || E2E_SKIP_RUNTIME) return [];
453
+
454
+ const errors: ValidationError[] = [];
455
+ const { stdout, stderr, success } = await run(
456
+ 'pnpm exec playwright test --project=chromium --workers=1 --reporter=line'
457
+ );
458
+
459
+ if (aborted) return [];
460
+
461
+ if (!success) {
462
+ const output = `${stderr}\n${stdout}`.trim();
463
+ const firstLine = summarizePlaywrightFailure(output) ?? summarizeFailureOutput(output);
464
+ errors.push({
465
+ tool: 'e2e-chromium',
466
+ severity: 'error',
467
+ message: `Chromium E2E run failed${firstLine ? `: ${firstLine}` : ''}`
468
+ });
469
+ }
470
+
471
+ return errors;
472
+ }
473
+
474
+ async function runVitest(): Promise<ValidationError[]> {
475
+ if (aborted) return [];
476
+ const errors: ValidationError[] = [];
477
+ const { stdout } = await run('pnpm exec vitest run --reporter=json');
478
+
479
+ if (aborted) return [];
480
+
481
+ try {
482
+ const result = JSON.parse(stdout);
483
+ for (const file of result.testResults ?? []) {
484
+ for (const test of file.assertionResults ?? []) {
485
+ if (test.status === 'failed') {
486
+ errors.push({
487
+ tool: 'vitest',
488
+ file: file.name,
489
+ line: test.location?.line,
490
+ severity: 'error',
491
+ message: `${test.ancestorTitles.join(' > ')} > ${test.title}: ${test.failureMessages?.[0]?.split('\n')[0] ?? 'failed'}`
492
+ });
493
+ }
494
+ }
495
+ }
496
+ } catch {
497
+ // JSON parse failed, no errors to report
498
+ }
499
+
500
+ return errors;
501
+ }
502
+
503
+ interface Check {
504
+ name: string;
505
+ fn: () => Promise<ValidationError[]>;
506
+ }
507
+
508
+ async function validate(): Promise<ValidationResult> {
509
+ const start = performance.now();
510
+
511
+ // Reset global state
512
+ collectedErrors = [];
513
+ failedTools = [];
514
+ aborted = false;
515
+
516
+ const allChecks: Check[] = [
517
+ { name: 'prettier', fn: runPrettier },
518
+ { name: 'oxlint', fn: runOxlint },
519
+ { name: 'svelte-check', fn: runSvelteCheck },
520
+ { name: 'tsc', fn: runTypeScript },
521
+ { name: 'test-coverage', fn: runTestCoverage },
522
+ { name: 'e2e-coverage', fn: runE2ECoverage },
523
+ { name: 'vitest', fn: runVitest }
524
+ ];
525
+
526
+ if (!E2E_SKIP_RUNTIME) {
527
+ allChecks.push({ name: 'e2e-chromium', fn: runE2EChromium });
528
+ }
529
+
530
+ console.error(`→ Running ${allChecks.length} checks in parallel...`);
531
+
532
+ // Run all checks in parallel with fail-fast
533
+ await Promise.all(
534
+ allChecks.map(async (check) => {
535
+ const checkStart = performance.now();
536
+ const errors = await check.fn();
537
+ const checkDuration = Math.round(performance.now() - checkStart);
538
+
539
+ // Add errors and check if we should abort
540
+ const wasAborted = addErrors(check.name, errors);
541
+
542
+ const status = aborted && errors.length === 0 ? '○' : errors.length === 0 ? '✓' : '✗';
543
+ const suffix = wasAborted && errors.length === 0 ? ' (cancelled)' : '';
544
+ console.error(` ${status} ${check.name} (${checkDuration}ms)${suffix}`);
545
+ })
546
+ );
547
+
548
+ const duration_ms = Math.round(performance.now() - start);
549
+
550
+ const errorSummary = `✗ ${collectedErrors.length} error(s) from ${failedTools.join(', ')}`;
551
+
552
+ return {
553
+ success: collectedErrors.length === 0,
554
+ errors: collectedErrors,
555
+ summary:
556
+ collectedErrors.length === 0
557
+ ? `✓ All ${allChecks.length} checks passed in ${duration_ms}ms`
558
+ : errorSummary,
559
+ duration_ms,
560
+ stopped_early: aborted,
561
+ next_action:
562
+ collectedErrors.length > 0
563
+ ? 'Spawn a sub-agent to fix these errors, then re-run validation.'
564
+ : undefined
565
+ };
566
+ }
567
+
568
+ validate().then((result) => {
569
+ console.log(JSON.stringify(result, null, 2));
570
+ process.exit(result.success ? 0 : 1);
571
+ });