mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
* feat(web): pure reducer for Critique Theater states (Phase 7.1)
Pure CritiqueState reducer driven by the contracts-level PanelEvent
(the same shape both the live SSE stream and the recorded transcript
emit), so a single reducer powers both the in-flight panel and the
rerun replay. Lifecycle covers run_started → running → (shipped /
degraded / interrupted / failed), with panelist_open / dim /
must_fix / close / round_end events building per-round
CritiquePanelistView entries as they arrive.
Defensive behaviour that surfaced while writing the spec tests:
- Terminal phases (shipped / degraded / interrupted / failed) are
sticky against further lifecycle events for the same run, except
for parser_warning which can land late and is recorded in a side
channel without changing phase.
- A new run_started for a different runId at any time discards the
prior state and reboots, so the UI can launch consecutive runs
without an explicit reset action.
- Events whose runId does not match the active run return the same
state reference, so React's useReducer doesn't re-render
subscribers on stray traffic.
- Round bookkeeping keys by round number rather than "always last",
so an out-of-order panelist_dim for round 1 arriving after a
round 2 dim does not corrupt the round 2 bucket.
Test coverage: 18 cases covering each transition, the runId guard,
sticky-terminal behaviour, the out-of-order round invariant, and
the stable-identity guarantee. Sets up Phase 7.2 and 7.3 to wire
SSE + replay into the same reducer.
* feat(web): useCritiqueStream hook subscribes to SSE and feeds reducer (Phase 7.2)
createCritiqueEventsConnection is a pure connection manager that
mirrors apps/web/src/providers/project-events.ts: opens an
EventSource at /api/projects/:id/events, listens for every name in
CRITIQUE_SSE_EVENT_NAMES, decodes each frame back into a PanelEvent
(stripping the critique. prefix and merging the data payload), and
hands it to the caller's onEvent. Reconnect uses exponential
backoff (1s → 30s) and resets on `ready`; malformed payloads drop
with a dev-mode warning rather than tearing the stream.
useCritiqueStream wraps the manager in a useReducer that owns the
CritiqueState. enabled=false or a null projectId tears down the
connection cleanly; switching projectId closes the old connection
and opens a fresh one. The returned dispatch lets local UI
synthesise actions (e.g. an Esc keypress firing a synthetic
interrupted while a kill request is in flight); production traffic
comes from the SSE stream.
Test coverage:
- sse.test.ts (10 cases, node env): subscription set covers every
CRITIQUE_SSE_EVENT_NAMES channel; payload decoding lifts the wire
shape back to PanelEvent; malformed JSON is swallowed and does
not stop the stream; exponential backoff schedule and ready-reset
semantics are pinned with a setTimeout seam; close() cancels
pending reconnects and shuts the live source; no-op fallback
when EventSource is unavailable.
- useCritiqueStream.test.tsx (6 cases, jsdom env): idle pre-event,
reducer driven by synthetic actions, no connection when disabled
or projectId is null, clean close on unmount, projectId change
reopens cleanly.
* feat(web): useCritiqueReplay hook drives reducer from transcript file (Phase 7.3)
Fetches the per-run NDJSON transcript (one PanelEvent per line),
parses every line via the shared isPanelEvent predicate, and
dispatches into the same CritiqueState reducer the live SSE stream
uses. A single reducer means the UI rendering a replay can be
identical to the live panel, and a UI mounting both
useCritiqueStream and useCritiqueReplay in parallel does not have
to reconcile two state shapes.
speed knob is `paused | instant | live | { intervalMs: N }`.
- instant flushes every event synchronously, useful for opening a
finished run already at its terminal state.
- intervalMs paces dispatches at a fixed cadence so the reviewer
can watch the run unfold.
- paused parses the transcript but holds events back until the
caller advances speed (consumers can drive a scrubber later).
- live is reserved for the future "playback at original cadence"
feature, currently treated as instant; replay timestamps are not
yet persisted with each event so honest pacing requires a
follow-up Phase 7+ task.
gunzip seam handles `.ndjson.gz` transcripts via
DecompressionStream when present; the production fetch path picks
between text and arrayBuffer based on the URL extension. Both seams
are injectable so the unit tests don't need to spin up a real
network or a real gzip pipeline.
Test coverage (8 cases, jsdom env):
- Idle status before any URL is provided.
- speed=instant flushes the full transcript synchronously to
shipped state.
- speed={intervalMs:N} paces with the setTimeout seam, reaching
done after the last tick.
- speed=paused leaves status=playing with no dispatches.
- Empty transcript reports done with state still idle.
- Fetch rejection surfaces an error status with the message.
- Malformed NDJSON lines are skipped; valid events around them
still land.
- .gz transcripts route through the gunzip seam.
Closes the Phase 7 plan tasks 7.1 / 7.2 / 7.3 (reducer + stream +
replay), all on one branch ready for review. Phases 8+ (Theater
components) consume these from this PR.
* fix(web): close payload-override gap + paused-resume bug in Critique Theater hooks (Phase 7 review)
Two P1 fixes from lefarcen's review on PR #1307:
SSE payload override
`sseToPanelEvent` previously spread `data` after the channel-derived
`type`, so a payload-provided `type` could override the channel and
route a `critique.run_started` frame into the reducer as a `ship`
action. Reversed the spread so the channel-derived `type` is
authoritative, and revalidated the resulting object through the
contracts-level `isPanelEvent` predicate before returning. Frames
that fail validation (missing runId, empty runId, unknown type) are
dropped, so a malformed or compromised SSE frame can no longer
dispatch a wrong-shape action into the reducer.
Three new sse.test.ts cases pin the regression: hostile `type:'ship'`
in the payload still resolves to `run_started`, missing runId is
dropped, empty runId is dropped.
Replay pause/resume
`useCritiqueReplay` had one big effect keyed on `transcriptUrl`
only, so flipping `speed` from `paused` to `instant` never re-fired
and the held events sat undispatched. Split into a parse effect
(depends on URL, fetches and stores events in state) and a pace
effect (depends on parsed-events + speed, owns the cursor + timers).
The playback cursor lives in a ref that survives pause/resume
cycles, so flipping `paused` -> `instant` flushes from the current
position rather than restarting (which would double-dispatch
`run_started` and reset the reducer).
Two new useCritiqueReplay.test.tsx cases:
- paused-then-instant transitions from `playing` to `done` and
reaches the shipped terminal phase
- intervalMs paced playback dispatches one event, pauses to drain
the next scheduled timer, flips to instant, and confirms the
remaining transcript drains exactly once (cursor was preserved)
Doc consistency
The earlier source comment in useCritiqueReplay.ts claimed `live`
"paces by recorded timestamps" while the impl used zero-delay
timers and the PR body said it behaves like `instant`. Aligned to
reality: `live` currently behaves like `{ intervalMs: 0 }` (events
drain on successive microtasks via setTimeoutFn) because transcripts
do not yet carry per-event timestamps. Honest timestamp-driven
pacing is queued as a Phase 7+ follow-up.
Validated: pnpm guard, pnpm --filter @open-design/web typecheck,
Theater suite 47/47 (up from 42, +3 sse + 2 replay), full web suite
96 files / 888 tests.
* feat(i18n): seed Critique Theater key block (en + zh-CN; other locales fall back via spread)
* feat(web): Theater PanelistLane component (Phase 8.1)
* feat(web): Theater ScoreTicker component (Phase 8.2)
* feat(web): Theater RoundDivider component (Phase 8.3)
* feat(web): Theater InterruptButton component with Escape keybind (Phase 8.4)
* feat(web): Theater TheaterDegraded chip (Phase 8.5)
* feat(web): Theater TheaterCollapsed post-run summary (Phase 8.6)
* feat(web): Theater TheaterTranscript replay surface (Phase 8.7)
* feat(web): Theater TheaterStage top-level container (Phase 8.8)
* feat(web): Theater CSS using existing semantic tokens (no hex literals)
* feat(web): Theater public exports barrel
* fix(web): resolve P2 + P3 review feedback on Phase 8 (PR #1314)
Addresses all 4 P2 + 3 P3 items from codex, Siri-Ray, and lefarcen.
State-lifecycle fixes (3 x P2)
1. Reducer learns a synthetic `__reset__` action (`CritiqueResetAction`).
Host hooks dispatch it when their gating prop changes so a stale
run from a prior project / transcript cannot bleed into the next
context. Reset is idempotent on idle (returns the same reference).
2. `useCritiqueStream` dispatches `__reset__` at the top of its
connection effect, so a workspace switch from project A (which
streamed a critique) to project B clears the reducer before the
new EventSource opens. enabled=false also clears.
3. `useCritiqueReplay` dispatches `__reset__` at the top of its
parse effect, so transcriptUrl swaps (including swap-to-null after
a replay reached `shipped`) lift the reducer back to idle before
the new fetch starts.
SSE validation (1 x P2)
4. `sseToPanelEvent` now runs a per-variant `hasValidVariantShape`
check after the cheap `isPanelEvent` predicate. A
`critique.ship` frame missing `composite` / `round` / `status` /
`artifactRef` is rejected before reaching the reducer, so
TheaterCollapsed can no longer crash on `undefined.toFixed(1)`.
Every variant's required fields are validated: run_started
(protocolVersion, non-empty cast, maxRounds, threshold, scale),
panelist_* (round, role, plus variant-specific shape), round_end
(round, composite, mustFix, decision in {continue,ship}, reason),
ship (round, composite, status, artifactRef.{projectId,artifactId},
summary), degraded (reason, adapter), interrupted (bestRound,
composite), failed (cause), parser_warning (kind, position).
Reducer correctness (1 x P2)
5. `panelist_open` now materializes the round + an empty panelist
view (`{dims: [], mustFixes: []}`) so TheaterStage can highlight
the in-progress lane the instant the tag opens. Before this, a
stream that emitted only `panelist_open` after `run_started` left
`rounds = []` and the UI rendered no current round until a later
`panelist_dim` arrived.
Polish (3 x P3)
6. Brand role tint swaps from `var(--magenta, var(--accent))` to
`var(--purple, var(--accent))`. `--purple` is actually defined
across the design systems; `--magenta` is not, so Brand was
silently falling through to `--accent` and looking identical to
Designer.
7. New i18n key `critiqueTheater.interruptedSummary` for the
interrupted-collapse copy ("Interrupted at round N, best
composite X.X"). Previously the interrupted branch reused
`shippedSummary` and the UI read "Shipped at round..." for a run
that specifically did not ship. Native value in en + zh-CN; other
locales fall back via `...en` spread.
8. `TheaterDegraded` heading id comes from `useId()` instead of a
hardcoded `theater-degraded-heading`, so two chips rendered on
the same page (chat history with multiple completed runs) keep
their aria-labelledby references unambiguous.
Tests (15 new cases)
- reducer.test.ts (+5): __reset__ on running/terminal/idle, panelist_open materializes round, panelist_open does not stomp prior panelist data.
- sse.test.ts (+6): variant-level rejection for ship without required fields, degraded without adapter, run_started with empty cast, panelist_dim with non-numeric score, round_end with unknown decision, plus a positive fully-formed ship.
- useCritiqueStream.test.tsx (+2): state reset on projectId change, state reset on enabled flip false.
- useCritiqueReplay.test.tsx (+1): state reset on transcriptUrl swap to null after a replay reached shipped.
- TheaterCollapsed.test.tsx (text-pinning update): asserts the interrupted branch reads "Interrupted at round 1" + "best composite 7.9", and explicitly NOT "Shipped at round...".
- TheaterDegraded.test.tsx (+1): two chips on the same page get unique aria-labelledby ids that each resolve to an `<h3>`.
Validated
- pnpm guard clean
- pnpm --filter @open-design/web typecheck clean
- Theater suite: 13 files, 101 tests (was 86 on the first Phase 8 push, +15 new)
- tests/i18n/locales.test.ts 5 of 5 across 18 locales
* feat(web): CritiqueTheaterMount wires SSE + reducer into a single drop-in (Phase 9.1)
* feat(i18n): Critique Theater strings for de + ja + ko + zh-TW (Phase 9.2)
* fix(web): resolve P1 + P2 review feedback on Phase 9 (PR #1315)
Addresses every blocker from codex, Siri-Ray, and lefarcen. The
three state-lifecycle and SSE-validation issues they also flagged
inherit fixes from PR #1314's review pass that this branch now sits
on top of after rebase.
Real daemon kill on Interrupt (P1)
- CritiqueTheaterMount now POSTs to
/api/projects/:id/critique/:runId/interrupt alongside the
optimistic local dispatch. Before this fix, clicking Interrupt
only flipped the React state to interrupted while the daemon job
kept running. The fetch is best-effort: a 404 (endpoint not wired
yet, lands in Phase 15) is swallowed with a dev-mode console.warn
so the UI still moves to the collapsed badge.
- New fetchInterrupt test seam lets RTL assert on the URL / method
and simulate the "daemon not ready yet" path. Two tests pin both:
the happy URL proj-42/critique/run-abc/interrupt POSTs, and a
rejected fetch still flips the UI.
interruptPending reset on new run (P2)
- A ref-backed effect compares the current runId against the last
one we saw; when it changes, interruptPending is cleared. A user
who interrupts run-1 and then triggers run-2 from the same mount
now gets a fresh, enabled kill button instead of one stuck in
"Interrupting…". Pinned by a new mount test.
Escape keybind scope (P2)
- InterruptButton now checks the keydown target. Escape inside an
input, textarea, select, or contenteditable element is ignored
(and any ancestor of those via closest() is treated the same
way). Body-level focus still fires the keybind so the Theater
area's affordance keeps working. Four new tests cover textarea,
input, contenteditable, and the body-focus positive case.
userFacingName i18n key (P2)
- The spec at specs/current/critique-theater.md:6 mandates a single
critiqueTheater.userFacingName key so the "Design Jury" label can
be renamed without touching code. Phase 8 introduced
critiqueTheater.title by mistake; renamed across types.ts, en.ts,
zh-CN.ts, de.ts, ja.ts, ko.ts, zh-TW.ts, and the lone consumer
TheaterStage.tsx. The locale alignment test stays green.
Validated
- pnpm guard clean
- pnpm --filter @open-design/web typecheck clean
- Theater suite: 14 files, 112 tests (was 101 before, +11 new for
the Phase 9 review pass: 3 mount + 4 InterruptButton focus scope;
the rest were already in #1314's review fix).
- tests/i18n/locales.test.ts 5 of 5 across 18 locales.
* feat(daemon): adapter-degraded registry with TTL (Phase 10.1)
In-memory registry recording adapters that produced malformed or
oversize transcripts so the orchestrator can skip them for a TTL
window (default 24h) instead of cycling through known-bad providers
on every run.
Records carry reason (malformed_block | oversize_block |
missing_artifact), source label, and expiresAt. The test-only
clock seam lets the suite advance time deterministically and prove
that an expired entry stops counting as degraded without anyone
calling clearDegraded.
7/7 vitest cases green.
* feat(daemon): synthetic good + bad adapter fixtures (Phase 10.2)
Two test-only adapters that read the existing v1 transcript
fixtures (happy-3-rounds and malformed-unbalanced) and replay them
as either a full string or a 512-byte chunked stream. The chunked
form is what the conformance harness uses to prove the parser
holds together when the transcript arrives in arbitrary network
slices, not as one buffered blob.
* feat(daemon): adapter conformance harness (Phase 10.3)
runAdapterConformance pulls a transcript through the same
parseCritiqueStream pipeline the orchestrator uses and classifies
the outcome as shipped, degraded, or failed. On a degraded
outcome it forwards the matched reason to the adapter-degraded
registry, so a single nightly conformance run is what populates
the skip list rather than the orchestrator learning each adapter
is broken at request time.
5/5 vitest cases green covering shipped, malformed degraded,
oversize degraded, no-ship failure, and the harness-thrown
failure path.
* fix: resolve P1 + P2 review feedback on Phase 10 (PR #1316)
Six fixes across the surfaces flagged by Codex, Siri-Ray, and
lefarcen on PR #1316.
1. SSE variant guard rejects unknown enum values (lefarcen P2).
hasValidVariantShape now checks role against PANELIST_ROLES and
status / reason / cause / kind / decision against the contracts
literal sets. A malformed frame with role='__proto__' or
status='wat' is dropped before it reaches the reducer.
2. Replay transcript parser reuses the SSE variant guard
(lefarcen + codex P2). parseTranscript in useCritiqueReplay now
layers hasValidVariantShape on top of the shallow isPanelEvent
check, so a corrupt transcript line like
'{type:"ship",runId:"r"}' is dropped instead of crashing the
reducer on composite.toFixed().
3. Conformance harness treats any parser_warning as degraded
(lefarcen P2). The classifier previously returned 'shipped' on
the first ship event even if score_clamped / unknown_role /
composite_mismatch / duplicate_ship warnings were emitted
earlier. New behavior: any parser_warning in the stream
classifies as degraded with local reason 'parser_warning'
(mapped to contracts 'malformed_block' when marking the
registry).
4. Conformance harness verifies panel completeness on ship
(codex P2). Previously a ship event with only some panelists
closed was accepted as shipped because the parser only enforces
the round-1 designer artifact invariant. The harness now
tracks closed roles vs the cast declared in run_started and
returns degraded with local reason 'incomplete_panel' when any
role missed close (mapped to contracts 'missing_artifact').
5. Conformance tests for oversize_block and adapter-throws
(lefarcen P2). Added the two cases the prior PR body claimed
were covered but were not: oversize via a tight
parserMaxBlockBytes on the good fixture, and an async iterable
that throws mid-stream to drive the unexpected_error path. Two
more tests cover the new parser_warning and incomplete_panel
classifications. 9 vitest cases total, all green.
6. Interrupt dispatch waits for daemon ack (Siri-Ray + lefarcen
P1). CritiqueTheaterMount used to optimistically dispatch
'interrupted' synchronously alongside the fetch, so a daemon
that responded 404 or 409 (endpoint not wired, run already
terminal) still moved the UI to the sticky interrupted phase
and ignored every real terminal event. The new flow snapshots
runId / bestRound / composite at click time, awaits the fetch,
and only terminalizes on res.ok. On rejection or non-2xx, it
clears interruptPending so the user can retry and the live SSE
keeps emitting.
7. Native i18n key critiqueTheater.interruptedSummary backfilled
in de / ja / ko / zh-TW (Siri-Ray P2). The other 12 locales
inherit from en via spread, so they were already typecheck-
safe; this commit gives the native locales native interrupted-
summary strings instead of falling through to English.
Tests: 16 daemon + 108 web Theater + locales suite all green;
web typecheck clean.
* fix(daemon): resolve P2 daemon feedback flagged on PR #1317
Two cleanups on Phase 10's daemon surface that the Phase 11 review
caught (lefarcen):
1. Module-URL-anchored fixture paths in synthetic-good.ts and
synthetic-bad.ts. The previous version used
path.join(__dirname, '..', 'v1', ...) which silently breaks if
the directory tree shifts. Replaced with new URL('../v1/...',
import.meta.url) so a fixture move surfaces as a clear ENOENT
pointing at this exact import. readFileSync accepts URL objects
directly so no path conversion is needed at the call site;
FIXTURE_PATH stays exported in string form for callers that
still need it.
2. Surface shipPayload in the shipped ConformanceOutcome. The
parser already hands the artifact bytes back via onArtifact, but
the previous shipped variant dropped them and the body silenced
the lint with void shipPayload. Added an artifact field
(ShipArtifactPayload | null) and removed the lint trick. A new
assertion in critique-conformance.test.ts pins MIME + non-empty
body on the synthetic-good shipped path.
16/16 daemon tests still green. Daemon typecheck clean.
* fix(daemon): drain stream + per-round closedRoles in conformance harness (PR #1316)
Two follow-on P2 fixes lefarcen flagged after the first round of
review on PR #1316:
1. Drain the stream before classifying. The previous revision
returned 'shipped' the moment the parser yielded a ship event,
so any parser_warning emitted AFTER the ship was lost. The
concrete failure mode is a duplicate <SHIP> block: the parser
emits the first ship event, then later emits a parser_warning
of kind 'duplicate_ship' for the second. The old code classified
shipped and returned before the warning arrived. The new code
captures shipEvent (and keeps iterating) until the parser
completes, then classifies at end-of-stream so any post-ship
parser_warning correctly degrades the run.
2. Per-round closed-roles bucket. The previous revision used a
single cumulative Set<string> for closedRoles, so a multi-round
run where round 1 closed every cast role and round 2 only closed
designer + critic before shipping would falsely pass the
cast-complete check (round 1's closes pretended round 2 was
fine). Replaced with Map<number, Set<string>> keyed by the round
on each panelist_close event; at ship time the check looks at
the shipping round's bucket specifically.
Two new test cases pin both behaviors: duplicate-SHIP transcript
returns degraded parser_warning, and a 2-round transcript where
round 2 ships without closing brand/a11y/copy returns degraded
incomplete_panel. 11/11 daemon conformance tests green;
typecheck clean.
* fix(web): restore wait-for-daemon-ack pattern on Theater interrupt (Siri-Ray + lefarcen P1 on PR #1316)
The merge that brought main back into this branch carried Phase 9's
CritiqueTheaterMount.tsx, which had the older optimistic-dispatch
flow: the reducer terminalized on click while the POST was still in
flight. Reviewers caught it on the merge commit (41d5c3f5).
Restored the corrected pattern:
- snapshot runId / bestRound / composite at click time so a fresh
run starting under the in-flight POST does not get attributed the
wrong terminal action
- wait for the daemon response and dispatch interrupted only on
res.ok
- on rejection OR non-2xx, clear interruptPending and log in dev so
the user can retry and the real SSE terminal event still wins
Tests updated to match: the prior 'swallows a rejected fetch and
moves the UI to interrupted' assertion was the regression itself; it
now lives as two negative tests (rejection + 404 both leave the run
running). The kill-button-flips and the fresh-run-resets-pending
cases wait for the daemon ack with waitFor.
Web suite: 1011 / 1011 green; typecheck clean.
* fix(test): add projectKind prop to FileViewer deck render after v0.7.0 merge
---------
Co-authored-by: Nagendhra <nagendhra405@gmail.com>
61 lines
2.4 KiB
TypeScript
61 lines
2.4 KiB
TypeScript
/**
|
|
* Synthetic adapter that emits the canonical happy-path transcript
|
|
* (`happy-3-rounds.txt`). Used by the Phase 10 conformance harness so
|
|
* the parser + orchestrator can be exercised end-to-end against a
|
|
* deterministic input that has no network or model dependency.
|
|
*
|
|
* The harness uses this fixture two ways:
|
|
* 1. In-process via `syntheticGoodTranscript()`, which returns the raw
|
|
* transcript string. Tests wrap it in an `AsyncIterable<string>`
|
|
* and feed `parseCritiqueStream`.
|
|
* 2. As a child-process stub via the sibling `synthetic-good.cli.ts`
|
|
* script, which writes the same transcript to stdout. The CLI form
|
|
* lets the existing daemon CLI-spawn primitive treat this fake
|
|
* adapter identically to a real one (the path the plan calls out
|
|
* for the nightly matrix).
|
|
*/
|
|
|
|
import { readFileSync } from 'node:fs';
|
|
import url from 'node:url';
|
|
|
|
/**
|
|
* Resolve the fixture relative to *this module's URL* rather than `cwd`.
|
|
* `new URL(relative, import.meta.url)` is the module-anchored equivalent
|
|
* of `path.join(__dirname, relative)` and is the form
|
|
* lefarcen P2 on PR #1317 asked for: a directory move of either this
|
|
* file or the fixture would surface as a clear ENOENT pointing at this
|
|
* exact line rather than a stale `path.join('..', 'v1', ...)` that
|
|
* silently resolves to the wrong place.
|
|
*/
|
|
export const SYNTHETIC_GOOD_FIXTURE_URL = new URL(
|
|
'../v1/happy-3-rounds.txt',
|
|
import.meta.url,
|
|
);
|
|
|
|
/** String form of the fixture path so tests and tooling can still `path.join` against it. */
|
|
export const SYNTHETIC_GOOD_FIXTURE_PATH = url.fileURLToPath(
|
|
SYNTHETIC_GOOD_FIXTURE_URL,
|
|
);
|
|
|
|
/**
|
|
* Read the canonical happy-path transcript synchronously. The file ships
|
|
* with the daemon source so the call cannot fail in a packaged build;
|
|
* `readFileSync` accepts URL objects directly.
|
|
*/
|
|
export function syntheticGoodTranscript(): string {
|
|
return readFileSync(SYNTHETIC_GOOD_FIXTURE_URL, 'utf8');
|
|
}
|
|
|
|
/**
|
|
* Async-iterable wrapper used by the conformance harness so the parser
|
|
* sees the same input shape it would from a real adapter's stdout.
|
|
* Splits the transcript into ~512-byte chunks so the parser exercises
|
|
* its incremental-boundary logic instead of seeing one giant chunk.
|
|
*/
|
|
export async function* syntheticGoodStream(): AsyncIterable<string> {
|
|
const raw = syntheticGoodTranscript();
|
|
const chunkSize = 512;
|
|
for (let i = 0; i < raw.length; i += chunkSize) {
|
|
yield raw.slice(i, i + chunkSize);
|
|
}
|
|
}
|