@qorejs/qore 1.0.0 → 1.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.
package/README.md CHANGED
@@ -3,275 +3,107 @@
3
3
  [![npm version](https://img.shields.io/npm/v/%40qorejs%2Fqore?color=0f766e&label=npm)](https://www.npmjs.com/package/@qorejs/qore)
4
4
  [![latest release](https://img.shields.io/github/v/release/qorejs/qore?color=0f766e&label=release)](https://github.com/qorejs/qore/releases/latest)
5
5
  [![ci](https://github.com/qorejs/qore/actions/workflows/ci.yml/badge.svg)](https://github.com/qorejs/qore/actions/workflows/ci.yml)
6
- [![browser smoke](https://img.shields.io/badge/browser-smoke-playwright-45ba63)](#browser-regression)
7
6
  [![release checks](https://github.com/qorejs/qore/actions/workflows/release-check.yml/badge.svg)](https://github.com/qorejs/qore/actions/workflows/release-check.yml)
8
- [![publish github packages](https://github.com/qorejs/qore/actions/workflows/publish-github-packages.yml/badge.svg)](https://github.com/qorejs/qore/actions/workflows/publish-github-packages.yml)
9
- [![GitHub Packages](https://img.shields.io/badge/GitHub-Packages-181717?logo=github)](https://github.com/qorejs/qore/packages)
10
7
  [![node >= 18](https://img.shields.io/badge/node-%3E%3D18-1f6feb)](https://nodejs.org/)
11
8
  [![license: MIT](https://img.shields.io/badge/license-MIT-0f766e)](https://github.com/qorejs/qore/blob/main/LICENSE)
12
9
 
13
- Qore is a streaming-response framework where `stream = signal`.
10
+ Qore is a reactive stream runtime for AI-native interfaces.
14
11
 
15
- Instead of treating data as a snapshot, Qore treats it like a river. Tokens arrive piece by piece, and the UI should respond piece by piece too. No manual string accumulation. No scattered loading state. No partial rendering workaround layered on top of a snapshot-first mental model.
12
+ Its core primitive is `stream = signal`: an async stream is also a readonly signal, so tokens, tool events, status updates, markdown fragments, or diffs can flow directly into UI state.
16
13
 
17
- Quick links:
18
-
19
- - [npm package](https://www.npmjs.com/package/@qorejs/qore)
20
- - [latest release](https://github.com/qorejs/qore/releases/latest)
21
- - [GitHub Packages](https://github.com/qorejs/qore/packages)
22
- - [migration notes](./MIGRATION.md)
23
- - [landing page source](https://github.com/qorejs/qore/blob/main/index.html)
24
- - [streaming demo source](https://github.com/qorejs/qore/blob/main/examples/streaming-response.html)
25
- - [benchmark page](https://github.com/qorejs/qore/blob/main/examples/benchmark.html)
26
- - [continuous integration](https://github.com/qorejs/qore/actions/workflows/ci.yml)
27
- - [release workflow](https://github.com/qorejs/qore/actions/workflows/release-check.yml)
28
- - [contributing guide](https://github.com/qorejs/qore/blob/main/CONTRIBUTING.md)
29
- - [security policy](https://github.com/qorejs/qore/blob/main/SECURITY.md)
30
-
31
- ## Installation
32
-
33
- From npm:
14
+ ## Install
34
15
 
35
16
  ```bash
36
17
  npm i @qorejs/qore
37
18
  ```
38
19
 
39
- From GitHub Packages:
40
-
41
- ```bash
42
- echo "@qorejs:registry=https://npm.pkg.github.com" >> .npmrc
43
- npm i @qorejs/qore
44
- ```
45
-
46
- GitHub Packages installs require an authenticated session against `https://npm.pkg.github.com`.
47
-
48
- For maintainers, the local npm release path now includes a fast preflight:
49
-
50
- ```bash
51
- npm run publish:preflight
52
- npm run publish:npm
53
- ```
54
-
55
- The preflight checks npm auth, confirms the changelog matches `package.json`, and fails early if that exact version is already published.
56
-
57
- - Package name: `@qorejs/qore`
58
- - Module format: `ESM`
59
- - Supported runtime: `Node >= 18`
60
- - CI coverage: `Node 18`, `20`, and `22`
61
- - Browser regression: Playwright desktop + mobile smoke coverage for the homepage, focused demo, and benchmark page
62
- - Registries:
63
- - npm: [npmjs.com/package/@qorejs/qore](https://www.npmjs.com/package/@qorejs/qore)
64
- - GitHub Packages: [github.com/qorejs/qore/packages](https://github.com/qorejs/qore/packages)
65
-
66
- ## Compatibility Matrix
67
-
68
- | Surface | Status | Notes |
69
- | --- | --- | --- |
70
- | Node runtime | Supported | `Node >= 18` |
71
- | Browser runtime | Supported | browser DOM entrypoints require `document` |
72
- | Reactive core | Supported | `signal`, `computed`, `effect`, `batch`, `createRoot`, `onCleanup` |
73
- | Stream runtime | Supported | includes backpressure, retry, orchestration, async iteration, and abort |
74
- | Provider adapters | Supported | OpenAI, Anthropic, OpenRouter, DeepSeek, Ollama, generic SSE, generic line-stream |
75
- | Browser DOM layer | Supported | `h`, `text`, `dynamic`, `list`, `mount`, `createApp(...).mount(...)` |
76
- | Published package maps | Supported | JavaScript source maps and declaration maps ship in `dist/src` for debugger-friendly production diagnostics |
77
- | SSR | Not supported | explicit browser-only DOM boundary in `1.0.x` |
78
- | Hydration | Not supported | deferred until a fully proven implementation exists |
79
-
80
- ## Core Idea
81
-
82
- `stream` is how data flows.
83
- `signal` is how the UI reacts.
84
-
85
- In Qore, they are two sides of the same primitive:
20
+ ## 30-Second Demo
86
21
 
87
22
  ```js
88
- import { createOpenAI, h, stream, text } from '@qorejs/qore';
89
-
90
- const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY });
91
- const answer = stream(openai.chat('hello'));
23
+ import { createSSEResponse, h, mount, stream, text } from '@qorejs/qore';
92
24
 
93
- return h('div', {}, text(() => answer()));
94
- ```
95
-
96
- Here, `answer` is all of the following at once:
97
-
98
- - A read-only `signal`, so `answer()` returns the current accumulated value
99
- - An `AsyncIterable`, so you can still use `for await...of`
100
- - A lifecycle-aware streaming state, with `status()`, `streaming()`, `error()`, and `chunks()`
101
-
102
- ## Performance Model
103
-
104
- Qore keeps the streaming hot path narrow:
105
-
106
- - chunk commits append into an internal log instead of cloning the full history on every token
107
- - public `chunks()` reads still return defensive copies, so consumers cannot corrupt runtime state
108
- - `chunkCount()` tracks the internal log version directly, so status UIs can stay cheap during long generations
109
- - DOM bindings update only the nodes that read the stream signal
110
-
111
- That means a long AI answer can keep flowing through one signal and one text node without turning every token into a full transcript rewrite.
112
-
113
- ## Why Qore
114
-
115
- - React treats streaming as a special case that needs extra machinery
116
- - SolidJS has excellent signals, but no native stream primitive
117
- - Vue has ergonomic refs, but stream handling still lives outside the core model
118
- - Qore makes `stream = signal` the core API from the start
119
-
120
- ## Quick Start
121
-
122
- ```js
123
- import { h, mount, stream, text } from '@qorejs/qore';
124
-
125
- const answer = stream(async function* () {
126
- yield 'stream';
127
- yield ' = ';
128
- yield 'signal';
129
- }());
130
-
131
- mount('#app', () => h('div', { className: 'answer' }, text(() => answer())));
132
- ```
133
-
134
- This updates only the text node that depends on the stream. It does not re-render the whole tree.
135
-
136
- ## Providers
137
-
138
- ### `createOpenAI(options?)`
139
-
140
- ```js
141
- import { createOpenAI, stream } from '@qorejs/qore';
142
-
143
- const openai = createOpenAI({
144
- apiKey: process.env.OPENAI_API_KEY,
145
- model: 'gpt-5'
146
- });
147
-
148
- const answer = stream(openai.chat('Why should stream be signal?'));
149
- ```
150
-
151
- ### `createAnthropic(options?)`
152
-
153
- ```js
154
- import { createAnthropic, stream } from '@qorejs/qore';
25
+ // Server: expose your provider as SSE.
26
+ export function handler() {
27
+ return createSSEResponse(async function* () {
28
+ yield 'stream';
29
+ yield ' = ';
30
+ yield 'signal';
31
+ }());
32
+ }
155
33
 
156
- const anthropic = createAnthropic({
157
- apiKey: process.env.ANTHROPIC_API_KEY,
158
- model: 'claude-sonnet-4-20250514'
159
- });
34
+ // Browser: consume the stream as one reactive value.
35
+ const answer = stream(fetch('/api/chat').then((response) => response.body));
160
36
 
161
- const answer = stream(anthropic.chat('Why should stream be signal?'));
37
+ mount('#app', () =>
38
+ h('main', {}, text(() => answer()))
39
+ );
162
40
  ```
163
41
 
164
- ### `createOpenRouter(options?)`
42
+ `answer` is a readonly signal and an async iterable. The UI reads `answer()`, and Qore updates only the DOM node that depends on it.
165
43
 
166
- ```js
167
- import { createOpenRouter, stream } from '@qorejs/qore';
44
+ ## Why Qore
168
45
 
169
- const openrouter = createOpenRouter({
170
- apiKey: process.env.OPENROUTER_API_KEY,
171
- model: 'openai/gpt-4.1-mini'
172
- });
46
+ React/Vercel AI SDK:
173
47
 
174
- const answer = stream(openrouter.chat('Why should stream be signal?'));
48
+ ```text
49
+ Token -> Hook state -> Component render -> Reconcile
175
50
  ```
176
51
 
177
- ### `createDeepSeek(options?)`
52
+ Qore:
178
53
 
179
- ```js
180
- import { createDeepSeek, stream } from '@qorejs/qore';
181
-
182
- const deepseek = createDeepSeek({
183
- apiKey: process.env.DEEPSEEK_API_KEY,
184
- model: 'deepseek-chat'
185
- });
186
-
187
- const answer = stream(deepseek.chat('Why should stream be signal?'));
54
+ ```text
55
+ Token -> Stream signal -> Text node
188
56
  ```
189
57
 
190
- Provider adapters also accept a request `signal` so you can cancel in-flight streams explicitly:
191
-
192
- ```js
193
- const controller = new AbortController();
194
- const answer = stream(openrouter.chat('Keep streaming', {
195
- signal: controller.signal
196
- }));
197
-
198
- controller.abort('user navigated away');
199
- ```
58
+ That is the core difference. Qore is not trying to be another AI SDK. It is the runtime layer that makes streamed data reactive.
200
59
 
201
- ### `createOllama(options?)`
60
+ ## Event Streams
202
61
 
203
- If you want a local-first provider path, Qore can stream directly from Ollama:
62
+ AI interfaces do not only stream text. They stream status, tool calls, reasoning, diffs, artifacts, retries, and errors.
204
63
 
205
64
  ```js
206
- import { createOllama, stream } from '@qorejs/qore';
65
+ const events = stream.events(agent.run(task));
207
66
 
208
- const ollama = createOllama({
209
- model: 'llama3.2'
67
+ const text = events.select('text', {
68
+ seed: '',
69
+ reduce: (current, event) => current + event.text
210
70
  });
211
-
212
- const answer = stream(ollama.chat('Why should stream be signal?'));
71
+ const tools = events.select('tool_call');
72
+ const status = events.select('status');
73
+ const diff = events.select('diff');
213
74
  ```
214
75
 
215
- ### `createSSEAdapter(options?)`
216
-
217
- If your backend already streams `text/event-stream`, Qore can adopt it directly:
218
-
219
- ```js
220
- import { createSSEAdapter, stream } from '@qorejs/qore';
221
-
222
- const provider = createSSEAdapter({
223
- name: 'Local Chat',
224
- url: 'http://localhost:3000/api/chat',
225
- buildRequest(request) {
226
- return {
227
- method: 'POST',
228
- body: JSON.stringify(request)
229
- };
230
- },
231
- buildChatRequest(input) {
232
- return { prompt: input };
233
- },
234
- eventToText(event) {
235
- return event.data?.type === 'token' ? event.data.text : undefined;
236
- }
237
- });
76
+ Each selected stream is still a signal and an async iterable. A timeline can render every event, while a markdown pane can bind only to `text()`.
238
77
 
239
- const answer = stream(provider.chat('hello'));
240
- ```
78
+ ## Provider Safety
241
79
 
242
- That makes `stream(provider.chat(...))` a general entry point instead of something tied to a single SDK.
80
+ Provider adapters are intended for server-side or trusted runtimes. Do not put `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, or any vendor secret in browser code. For browsers, expose your own SSE or NDJSON endpoint and stream that endpoint into Qore.
243
81
 
244
- ### `createLineAdapter(options?)`
82
+ ## Documentation
245
83
 
246
- If your backend streams newline-delimited JSON instead of `text/event-stream`, Qore can adopt that too:
84
+ - [Concepts](./docs/concepts.md): `stream = signal` and the mental model
85
+ - [Architecture](./docs/architecture.md): provider, runtime, signal, and DOM layers
86
+ - [API Reference](./docs/api.md): core primitives and composition helpers
87
+ - [Providers](./docs/providers.md): OpenAI, Anthropic, OpenRouter, DeepSeek, Ollama, SSE, and NDJSON
88
+ - [Runtime](./docs/runtime.md): backpressure, lifecycle, abort, retry, and orchestration
89
+ - [Comparisons](./docs/comparisons.md): Qore vs React, Vercel AI SDK, Solid, and Vue
90
+ - [Benchmarks](./docs/benchmarks.md): methodology and current evidence
91
+ - [migration notes](./MIGRATION.md)
92
+ - [RELEASE.md](./RELEASE.md)
247
93
 
248
- ```js
249
- import { createLineAdapter, stream } from '@qorejs/qore';
250
-
251
- const provider = createLineAdapter({
252
- name: 'Local NDJSON Chat',
253
- url: 'http://localhost:11434/api/chat',
254
- buildRequest(request) {
255
- return {
256
- method: 'POST',
257
- body: JSON.stringify(request)
258
- };
259
- },
260
- buildChatRequest(input) {
261
- return {
262
- model: 'llama3.2',
263
- messages: [{ role: 'user', content: input }]
264
- };
265
- },
266
- lineToText(event) {
267
- return typeof event.data?.message?.content === 'string'
268
- ? event.data.message.content
269
- : undefined;
270
- }
271
- });
94
+ ## Compatibility Matrix
272
95
 
273
- const answer = stream(provider.chat('hello'));
274
- ```
96
+ | Surface | Status | Notes |
97
+ | --- | --- | --- |
98
+ | Node runtime | Supported | `Node >= 18` |
99
+ | Browser runtime | Supported | DOM entrypoints require `document` |
100
+ | Reactive core | Supported | `signal`, `computed`, `effect`, `batch`, `createRoot`, `onCleanup` |
101
+ | Stream runtime | Supported | backpressure, retry, orchestration, async iteration, and abort |
102
+ | Provider adapters | Supported | OpenAI, Anthropic, OpenRouter, DeepSeek, Ollama, generic SSE, generic line-stream |
103
+ | Browser DOM layer | Supported | `h`, `text`, `dynamic`, `list`, `mount`, `createApp(...).mount(...)` |
104
+ | Published package maps | Supported | JavaScript source maps and declaration maps ship in `dist/src` |
105
+ | SSR | Not supported | explicit browser-only DOM boundary in `1.0.x` |
106
+ | Hydration | Not supported | deferred until a fully proven implementation exists |
275
107
 
276
108
  ## Provider Support Matrix
277
109
 
@@ -285,352 +117,13 @@ const answer = stream(provider.chat('hello'));
285
117
  | `createSSEAdapter(...)` | SSE | Supported | generic hosted or self-managed SSE |
286
118
  | `createLineAdapter(...)` | line-stream / NDJSON | Supported | generic line-delimited transport |
287
119
 
288
- Common guarantees across supported adapters:
289
-
290
- - async iterable text streaming
291
- - request `signal` support for cancellation
292
- - typed event streaming on provider-specific surfaces
293
- - package smoke coverage
294
-
295
- Additional hosted SSE guarantees:
296
-
297
- - retry contract support
298
- - `Last-Event-ID` resume support
299
- - normalized metadata helpers for usage, finish reason, response id, and model identity
300
-
301
- ## API Shape
302
-
303
- ### `stream(source, options?)`
304
-
305
- By default, `stream(...)` accumulates chunks into a text signal:
306
-
307
- ```js
308
- const answer = stream(openai.chat('hello'));
309
-
310
- answer(); // current text
311
- answer.status(); // idle | pending | streaming | completed | error | aborted
312
- answer.streaming(); // boolean
313
- answer.chunks(); // raw chunks
314
- await answer.ready; // wait for completion
315
- ```
316
-
317
- If you need structured streams:
318
-
319
- ```js
320
- const events = stream.list(eventSource);
321
- const latest = stream.latest(modelEvents);
322
- ```
323
-
324
- For append-heavy DOM lists such as chat transcripts, pass a stable key:
325
-
326
- ```js
327
- list(messages, (message) => h('article', {}, message.body), {
328
- key: (message) => message.id
329
- });
330
- ```
331
-
332
- ## Server-Side SSE
333
-
334
- If you want Qore to produce the server stream as well:
335
-
336
- ```js
337
- import { createSSEResponse } from '@qorejs/qore';
338
-
339
- export function handler() {
340
- return createSSEResponse(['hello', ' world']);
341
- }
342
- ```
343
-
344
- If you need orchestration:
345
-
346
- ```js
347
- const merged = stream.merge([openai.chat('a'), anthropic.chat('b')]);
348
- const scripted = stream.concat([retrieve.chat('a'), summarize.chat('a')]);
349
- const pipeline = stream.pipe(openai.chat('hello'), [
350
- (draft) => review.chat(draft),
351
- (reviewed) => format.chat(reviewed)
352
- ]);
353
- const fastest = stream.race([openai.chat('hello'), openrouter.chat('hello')]);
354
- const resilient = stream.retryable(() => openai.chat('retry me'), {
355
- maxRetries: 2,
356
- backoff: 'exponential'
357
- });
358
- const liveAnswer = stream.switchMap(promptChanges, (prompt) => openai.chat(prompt));
359
- ```
360
-
361
- Provider adapters can also retry dropped SSE connections and resume from the last event id:
362
-
363
- ```js
364
- const openai = createOpenAI({
365
- apiKey: process.env.OPENAI_API_KEY,
366
- retry: {
367
- maxAttempts: 3,
368
- backoff: 'exponential'
369
- }
370
- });
371
- ```
372
-
373
- Provider metadata can be normalized into one shared shape:
374
-
375
- ```js
376
- import {
377
- collectProviderMetadata,
378
- extractAnthropicMetadata,
379
- extractOpenAIMetadata
380
- } from '@qorejs/qore';
381
-
382
- const openaiMetadata = await collectProviderMetadata(
383
- 'OpenAI',
384
- openai.responses.stream({ input: 'hello' }),
385
- extractOpenAIMetadata
386
- );
387
-
388
- const anthropicMetadata = await collectProviderMetadata(
389
- 'Anthropic',
390
- anthropic.messages.stream({ messages: [{ role: 'user', content: 'hello' }] }),
391
- extractAnthropicMetadata
392
- );
393
-
394
- openaiMetadata.usage?.totalTokens;
395
- anthropicMetadata.finishReason;
396
- ```
397
-
398
- ### Backpressure
399
-
400
- ```js
401
- const answer = stream.withBackpressure(openai.chat('hello'), {
402
- interval: 16,
403
- buffer: 8,
404
- overflow: 'drop-oldest'
405
- });
406
- ```
407
-
408
- Backpressure is not just a delay wrapper:
409
-
410
- - `interval`: the minimum spacing between chunk delivery into the signal and UI
411
- - `buffer`: the maximum number of queued chunks before the UI catches up
412
- - `overflow`: what to do when the buffer is full: `wait`, `drop-oldest`, `drop-newest`, or `error`
413
-
414
- You can also observe stream pressure directly:
415
-
416
- ```js
417
- answer.buffered(); // how many chunks are queued right now
418
- answer.dropped(); // how many chunks were dropped by the overflow policy
419
- ```
420
-
421
- ### `signal`, `computed`, `effect`, `createRoot`, `onCleanup`
422
-
423
- ```js
424
- import { computed, createRoot, effect, onCleanup, signal, stream } from '@qorejs/qore';
425
-
426
- const answer = stream(openai.chat('hello'));
427
- const length = computed(() => answer().length);
428
-
429
- const dispose = createRoot((dispose) => {
430
- effect(() => {
431
- console.log(length());
432
- onCleanup(() => console.log('effect disposed'));
433
- });
434
-
435
- return dispose;
436
- });
437
-
438
- dispose();
439
- ```
440
-
441
- ### `response`
442
-
443
- `response` still exists, but it is closer to a lower-level state machine escape hatch for custom reducers and aggregators.
444
-
445
- If your goal is to pipe a stream directly into the UI, prefer `stream(...)`.
446
-
447
- ## Demos
448
-
449
- The repository includes a landing page and a focused streaming demo:
450
-
451
- - [Landing Page Source](https://github.com/qorejs/qore/blob/main/index.html)
452
- - [Homepage Logic](https://github.com/qorejs/qore/blob/main/examples/showcase.ts)
453
- - [Homepage Styles](https://github.com/qorejs/qore/blob/main/examples/showcase.css)
454
- - [Benchmark Page](https://github.com/qorejs/qore/blob/main/examples/benchmark.html)
455
- - [Benchmark Logic](https://github.com/qorejs/qore/blob/main/examples/benchmark-page.ts)
456
- - [Benchmark Core](https://github.com/qorejs/qore/blob/main/examples/benchmark-core.ts)
457
- - [Focused Demo](https://github.com/qorejs/qore/blob/main/examples/streaming-response.html)
458
- - [Focused Chat Logic](https://github.com/qorejs/qore/blob/main/examples/qore-chat.ts)
459
- - [React Compare](https://github.com/qorejs/qore/blob/main/examples/react-chat.ts)
460
-
461
- ## Project Layout
462
-
463
- ```text
464
- src/
465
- core/ stream, signal, response, iterable
466
- dom/ app mounting and DOM bindings
467
- providers/ OpenAI, Anthropic, OpenRouter, DeepSeek, Ollama, SSE, and line-stream adapters
468
- shared/ runtime utilities
469
- index.ts public entrypoint
470
-
471
- dist/
472
- src/ compiled package output
473
- examples/ built showcase scripts for local preview
474
- test/ compiled test output
475
- ```
476
-
477
- For a local preview:
478
-
479
- ```bash
480
- git clone git@github.com:qorejs/qore.git
481
- cd qore
482
- npm install
483
- npm run build
484
- python3 -m http.server 4173
485
- ```
486
-
487
- Then open [http://127.0.0.1:4173/](http://127.0.0.1:4173/).
488
-
489
- ## Server And SSR
490
-
491
- Qore's reactive core and stream runtime work in Node and browser environments today.
492
-
493
- The DOM layer is intentionally browser-only right now:
494
-
495
- - `signal`, `computed`, `effect`, `stream`, and provider adapters work in Node and the browser
496
- - `h`, `text`, `mount`, and `createApp(...).mount(...)` require a browser-like `document`
497
- - `canUseDOM()` is exported so integrations can branch cleanly before touching DOM APIs
498
- - `assertCanUseDOM(name?)` is exported if you want to fail fast with the same browser-boundary error shape Qore uses internally
499
- - the published entrypoint is checked against a frozen public API snapshot before release so accidental export drift fails CI early
500
-
501
- If you call DOM helpers without a browser-like runtime, Qore throws an entrypoint-specific error instead of failing later with a generic reference error. For example:
502
-
503
- - `h() requires a browser-like environment`
504
- - `mount() requires a browser-like environment`
505
- - `createApp(...).mount(...) requires a browser-like environment`
506
-
507
- Example:
508
-
509
- ```js
510
- import { assertCanUseDOM, canUseDOM } from '@qorejs/qore';
511
-
512
- if (canUseDOM()) {
513
- // Safe to call mount(), h(), text(), and other DOM entrypoints.
514
- }
515
-
516
- assertCanUseDOM('chat shell hydration');
517
- ```
518
-
519
- That means the current `1.0.0` path is:
520
-
521
- - stable reactive runtime
522
- - stable streaming runtime
523
- - explicit browser DOM boundary
524
- - streaming SSR and hydration as a post-`1.0.0` expansion area unless the implementation is fully proven first
525
-
526
- ## Browser Regression
527
-
528
- Install the browser binary once:
529
-
530
- ```bash
531
- npm run browsers:install
532
- ```
533
-
534
- Then run the browser smoke suite:
535
-
536
- ```bash
537
- npm run test:browser
538
- ```
539
-
540
- It validates:
541
-
542
- - the homepage stream demo
543
- - the focused streaming chat demo
544
- - the dedicated benchmark page
545
-
546
- The suite checks desktop and mobile layouts, watches for runtime console errors, exercises the primary interactions, and runs inside `release:check`.
547
-
548
- CI also uploads the browser regression evidence as workflow artifacts. The bundle includes viewport screenshots, focused page-surface screenshots, the Playwright HTML report, a `benchmark-suite.json` attachment from the dedicated benchmark page, and a human-readable benchmark summary markdown file from the benchmark gate.
549
-
550
- If a locked-down local shell cannot launch a supported headless browser, the script will defer to CI unless you force a hard local failure with `QORE_BROWSER_SMOKE_REQUIRED=1`.
551
-
552
- Local preview ports can also be pinned when another process is already using the default range:
553
-
554
- ```bash
555
- QORE_STATIC_PORT=4300 QORE_STATIC_PORT_END=4400 npm run test:browser
556
- ```
557
-
558
- ## Benchmark Methodology
559
-
560
- Qore now includes a reproducible browser benchmark that compares two rendering paths against the same workload:
561
-
562
- - `Qore stream = signal`: mount the transcript shell once and advance the same live text node as chunks arrive
563
- - `Snapshot rerender baseline`: rebuild the transcript shell from a snapshot string on every chunk
564
-
565
- Both paths use the same transcript history, the same chunk list, and the same final answer text. The benchmark reports first paint time, mutation records, node churn, and regenerated markup so the difference is visible instead of rhetorical.
566
-
567
- ## GitHub Packages
568
-
569
- The repository includes GitHub Actions workflows for both release validation and GitHub Packages publishing.
570
-
571
- - Release validation: `.github/workflows/release-check.yml`
572
- - GitHub Packages publish: `.github/workflows/publish-github-packages.yml`
573
- - Both workflows can be triggered manually from the Actions tab
574
- - Publishing a GitHub Release triggers both the release check and the GitHub Packages publish flow
575
- - The publish workflow validates the release tag, changelog, test suite, and tarball before it pushes the package
576
- - The publish workflow uses the repository `GITHUB_TOKEN`, which GitHub documents as the recommended way to publish packages from the workflow repository
577
- - GitHub Packages starts new packages as private by default, so you may want to switch the package visibility to public after the first publish
578
-
579
- Because the package already includes the correct `repository` field in `package.json`, GitHub Packages can link the package back to `qorejs/qore` when the workflow publishes it.
580
-
581
120
  ## Release Checklist
582
121
 
583
- The canonical release checklist lives in [`RELEASE.md`](./RELEASE.md). You can generate the current version's GitHub release body with `npm run release:notes`, and GitHub Actions publishes with npm provenance enabled.
584
-
585
- For every release candidate or stable release, the minimum local gate is:
122
+ The canonical release checklist lives in [RELEASE.md](./RELEASE.md). The minimum local gate is:
586
123
 
587
124
  ```bash
588
125
  npm ci
589
126
  npm run release:check
590
127
  ```
591
128
 
592
- For npm publishing from a local shell, run:
593
-
594
- ```bash
595
- npm run publish:preflight
596
- ```
597
-
598
- ## Project Hygiene
599
-
600
- The repository also includes:
601
-
602
- - `CONTRIBUTING.md` for contributor expectations and release flow
603
- - `SECURITY.md` for responsible disclosure
604
- - issue templates for bugs and feature requests
605
- - a pull request template
606
- - `.github/release.yml` to keep GitHub release notes structured
607
-
608
- ## Package Boundary
609
-
610
- Qore does not ship a built-in catalog of buttons, dialogs, tabs, or other UI primitives.
611
-
612
- The core package does only three things:
613
-
614
- - Move streams into state
615
- - Move state into the UI
616
- - Keep the whole process finely reactive
617
-
618
- Anything that does not serve `streaming response` belongs in an experimental layer or a separate package.
619
-
620
- ## Testing
621
-
622
- ```bash
623
- npm test
624
- ```
625
-
626
- The current test suite covers:
627
-
628
- - `signal`, `computed`, and `effect`
629
- - The core `stream = signal` behavior
630
- - `response` interoperability with async iterables
631
- - OpenAI, Anthropic, OpenRouter, DeepSeek, Ollama, and generic streaming adapters
632
-
633
- ## Roadmap
634
-
635
- - Tighten the hydration model around server-streamed rendering
636
- - Publish repeatable benchmarks that compare Qore with React and the Vercel AI SDK
129
+ Qore publishes through GitHub Actions trusted publishing with npm provenance enabled.