Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions TODO.impl/01-examples-corpus.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# [COMPLETE 2026-09-07] 01 — Examples corpus (P1)

## Goal
A committed set of runnable TS scripts exercising the public API the
way a playground user would. They are the preset gallery (single
source of truth — the client agent renders the `.ts` sources) and a
CI guarantee (they run as tests, so they cannot rot when the API
moves).

## Why
The playground's value is the story its examples tell. Examples that
live only in a UI rot; examples that run in CI are a public-API
regression suite that pays for itself twice.

## Spec

`examples/` directory, each example a self-contained module with one
default-exported behavior and no side effects at import time:

1. `examples/convert.ts` — the hello world: load a map, transliterate
one string. `interscript` top-level API.
2. `examples/detect-chain.ts` — `detect()` to rank candidate systems,
then transliterate through the best hit (composition story).
3. `examples/neural-diacritize.ts` — `interscript/ml`: resolve a model
by id, decode, with `onProgress` wired; runs against the int8 tier
id but tolerates no-network environments (test asserts the module
contract, live decode covered by the site e2e instead).
4. `examples/batch-columns.ts` — paste/CSV-shaped batch conversion
(the cataloger pipeline: normalize a column of names).
5. `examples/server-mode.ts` — the no-download path: `fetch` against
`https://api.interscript.org/v1/infer`, typed response.

Registry: `examples/index.ts` exports `EXAMPLES: readonly Example[]`
with `Example = { id, title, summary, file }` — `file` is the example
module path; the registry is data, not behavior (open/closed: adding
an example = adding a file + one registry line, no switch anywhere).

CI: `test/examples.test.ts` imports the registry, runs examples 1, 2,
4, 5 for real (network allowed for map fetch — same pattern the
existing suite already uses), and asserts each example's documented
expected output. Example 3 asserts its exported contract only.

## Acceptance
- `npx vitest run test/examples.test.ts` green locally.
- Adding a hypothetical 6th example requires no change outside
`examples/` + one registry line (the test iterates the registry).
- No example imports anything outside the package's public exports
(`interscript`, `interscript/ml`).
39 changes: 39 additions & 0 deletions TODO.impl/02-cdn-delivery-recipe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# [COMPLETE 2026-09-07] 02 — CDN delivery recipe (P1)

## Goal
The verified, version-pinned way to load `interscript` in a browser
with no bundler — written down with the evidence of what fails and
why, the kotoshu pattern.

## Why
kotoshu's engine worker documents that esm.sh and esm.run transforms
broke their wasm delivery and that raw jsDelivr files + local
instantiation was the only working path. Our package (ESM, fflate +
js-yaml deps, dynamic onnxruntime path for `/ml`) has the same class
of trap. The playground client agent should consume a proven recipe,
not rediscover it.

## Spec

Probe, then document:

1. **Probe (evidence, not assumption)**: in a real browser
(Playwright, run from the site repo's installed tooling):
- `import("https://esm.sh/interscript@5.3.0")` — does it load, and
does a map conversion complete?
- the raw-jsDelivr alternative if esm.sh fails any step;
- for `/ml`: whether `interscript/ml` imports without
onnxruntime-web present, and how ORT + its wasm files resolve
from CDN when it is imported from esm.sh's ORT pin.
2. **Artifact**: `docs/CDN.md` — the working recipe(s), exact-version
pin style, the failure modes found with their causes, and the
ORT/wasm note for the `/ml` path. Follows the kotoshu
engine-worker comment tradition: the *why* is the deliverable.
3. **Light regression**: `test/cdn-urls.test.ts` — asserts the pinned
CDN entry URLs respond 200 with a JS content-type (network test,
same allowance as the examples suite).

## Acceptance
- The probe ran; CDN.md states only what was observed.
- The recipe in CDN.md is the exact one exercised by item 03's worker
test (no drift between doc and proof).
30 changes: 30 additions & 0 deletions TODO.impl/03-worker-mode-guarantee.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# [COMPLETE 2026-09-07] 03 — Worker-mode guarantee (P2)

## Goal
Proof, in a real browser, that the package runs inside a Web Worker —
the architecture the playground will use to keep every conversion off
the main thread (kotoshu runs its engine exclusively in a worker).

## Why
The 5.2.1 browser-safety fix guarded window/document access via
optional globalThis chains — verified in page context, never in worker
context. A worker also changes asset-loading conditions (no DOM, but
Cache API and fetch exist). One test pins all of it.

## Spec

`e2e/worker-engine.spec.ts` in the site repo (it owns Playwright +
the browser runtime peers):

- the page spawns `new Worker(blob-url)`;
- the worker imports the package via the CDN recipe from item 02;
- the worker transliterates a fixed string and `postMessage`s the
result;
- the test asserts the expected output on the page side.

This simultaneously re-proves item 02's recipe per CI run.

## Acceptance
- Spec green in site CI.
- No change to the library itself (if the worker path fails, the fix
lands in the library with its own test — the spec is the tripwire).
26 changes: 26 additions & 0 deletions TODO.impl/04-playground-ambient-types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# [COMPLETE 2026-09-07] 04 — Playground ambient types (P2)

## Goal
A shipped `playground.d.ts` so playground users get autocomplete
against the real published types from the first keystroke.

## Why
The playground injects the package as a global (or via a pinned CDN
import); without ambient declarations the editor cannot offer
completion, and the published `.d.ts` alone doesn't declare the
injected binding.

## Spec

- `src/playground.d.ts`: ambient `declare const interscript: typeof
import("interscript")` (+ the `ml` namespace import where the
runtime exposes it), written so a playground can reference it via
triple-slash or config include.
- Emitted with the build (already inside `dist` via `files`).
- Documented in `docs/CDN.md`'s recipe (one line: how to wire it into
the editor's tsconfig).

## Acceptance
- `dist/playground.d.ts` present in the built output.
- Referencing it makes the injected global type-check (verified by a
type-level test or tsc smoke in CI).
24 changes: 24 additions & 0 deletions TODO.impl/05-api-server-mode-cors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# [COMPLETE 2026-09-07] 05 — API server-mode CORS (P3)

## Goal
The playground's "no 300 MB download" path — calling
`api.interscript.org` from browser scripts — verified end to end.

## Why
`examples/server-mode.ts` (item 01) documents the fetch path; if the
API does not send `Access-Control-Allow-Origin`, the example is a lie
in the exact context it exists for.

## Spec

- Live probe: OPTIONS preflight + POST `/v1/infer` from an `Origin:
https://interscript.org` request; assert ACAO present and the infer
round-trips.
- If broken: fix in the api worker (CORS middleware) via its normal
release chain (this escalates the item to a release — flag to the
owner rather than shipping a production change unannounced if the
fix is anything beyond additive middleware).

## Acceptance
- Probe documented in the example's docblock (or the fix shipped).
- No API behavior change beyond CORS headers if a fix was needed.
25 changes: 25 additions & 0 deletions TODO.impl/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# TODO.impl — playground support lane

Audience-facing scripting is the direction: people want to write a TS
script that uses Interscript directly, the way kotoshu's website
playground runs its engine. The playground CLIENT is another agent's
lane. Status: ALL 5 ITEMS COMPLETE (2026-09-07). This register was OUR side: everything the playground runs on,
delivered so their agent never has to rediscover it.

Priorities: P1 = unblocks the client agent immediately; P2 = hardens
the guarantee; P3 = completes the surface. All items are independently
shippable PRs against `main` of interscript-ts (one touches the site
repo, one verifies the API).

| # | Item | Priority | Repo |
|---|------|----------|------|
| 01 | [Examples corpus](01-examples-corpus.md) | P1 | interscript-ts |
| 02 | [CDN delivery recipe](02-cdn-delivery-recipe.md) | P1 | interscript-ts |
| 03 | [Worker-mode guarantee](03-worker-mode-guarantee.md) | P2 | interscript.github.io |
| 04 | [Playground ambient types](04-playground-ambient-types.md) | P2 | interscript-ts |
| 05 | [API server-mode CORS](05-api-server-mode-cors.md) | P3 | verify live; fix in api if broken |

Standing rules (unchanged from the campaign): every claim measured
before it ships; staged sets verified before every commit; no
attribution trailers; PR bodies via `--body-file`; the register closes
only when every item is COMPLETE — new items require the owner.
58 changes: 58 additions & 0 deletions docs/CDN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# CDN delivery — using interscript from a browser script

The no-bundler recipe, verified by browser probe (Playwright/Chromium,
2026-09-07) against `interscript@5.3.0`.

## The recipe

```ts
// Maps + interpreter — works as-is:
import { configure, iscStrategy, httpStrategy, transliterateAsync } from
"https://esm.sh/interscript@5.3.0"

configure({
strategies: [
iscStrategy({ baseUrl: "https://interscript.org/maps" }),
httpStrategy({ baseUrl: "https://interscript.org/maps", cacheKeyPrefix: "isx-libs:" }),
],
})

const out = await transliterateAsync("bgnpcgn-ukr-Cyrl-Latn-2019", "Антон Олегович")
// => "Anton Olehovych"
```

```ts
// Neural models — the namespace loads; sessions additionally need
// onnxruntime-web, pinned from the same CDN by the host page:
import { imf } from "https://esm.sh/interscript@5.3.0/ml"
```

## What was probed

| URL | Result |
|---|---|
| `esm.sh/interscript@5.3.0` | loads; full export surface; **conversion completes** ("Anton Olehovych") |
| `esm.sh/interscript@5.3.0/ml` | loads; `imf` namespace present; no import-time onnxruntime requirement |

## Failure modes — none found on the maps path

kotoshu's engine documents that esm.sh/esm.run transforms broke their
wasm-bindgen delivery (raw jsDelivr files + local instantiation were
the only working path). Our package has no such entanglement on the
maps path: plain ESM with `fflate`/`js-yaml` dependencies, which esm.sh
resolves. No raw-file fallback is needed.

## Pins and rules

- **Pin exact versions** (`@5.3.0`, never `@5`): CDN transforms are
cached per URL; a moving pin means a silently changing program.
- Model bytes never come from esm.sh — they resolve through the
models.yaml index (tag-pinned release assets; the browser path goes
through the API's CORS asset front door, see the `/neural` demo).
- `onnxruntime-web` is not a dependency of this package; hosts that
use `interscript/ml` sessions provide it (the website pins it as a
peer). Pair both imports from the same CDN origin to avoid duplicate
wasm loads.
- See `examples/` for the runnable forms of every pattern above, and
`dist/playground.d.ts` for editor autocomplete of the injected
global.
18 changes: 18 additions & 0 deletions examples/_lib/stack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* The browser-shaped map-loading stack every example shares — the same
* strategy order the website runtime uses (ISC form first, compiled
* JSON fallback for the .iml-only libraries).
*/
import { configure, httpStrategy, iscStrategy, reset } from "../../src/index.js"

export const MAPS_BASE = "https://interscript.org/maps"

export function configureSharedStack(): void {
reset()
configure({
strategies: [
iscStrategy({ baseUrl: MAPS_BASE }),
httpStrategy({ baseUrl: MAPS_BASE, cacheKeyPrefix: "isx-libs:" }),
],
})
}
23 changes: 23 additions & 0 deletions examples/batch-columns.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* The cataloger pipeline: transliterate a pasted column of names. The
* loader caches each map after first use, so the whole column is one
* map fetch.
*/
import { transliterateAsync } from "../src/index.js"
import { configureSharedStack } from "./_lib/stack.js"

export const title = "Batch a column of names"
export const summary = "Tab-separated input in, transliterated column out — the MARC batch shape."

export const expected = ["Anton Olehovych", "Solomiia", "Kyiv"].join("\n")

const COLUMN = ["Антон Олегович", "Соломія", "Київ"]

export async function run(): Promise<string> {
configureSharedStack()
const converted = new Array<string>(COLUMN.length)
for (const [i, name] of COLUMN.entries()) {
converted[i] = await transliterateAsync("bgnpcgn-ukr-Cyrl-Latn-2019", name)
}
return converted.join("\n")
}
15 changes: 15 additions & 0 deletions examples/convert.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Hello world: configure the map stack, transliterate one string.
*/
import { transliterateAsync } from "../src/index.js"
import { configureSharedStack } from "./_lib/stack.js"

export const title = "Convert one string"
export const summary = "Configure the map loader, transliterate Ukrainian by BGN/PCGN 2019."

export const expected = "Anton Olehovych"

export async function run(): Promise<string> {
configureSharedStack()
return transliterateAsync("bgnpcgn-ukr-Cyrl-Latn-2019", "Антон Олегович")
}
23 changes: 23 additions & 0 deletions examples/detect-chain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Composition: detect which loaded system best explains an input/output
* pair, then transliterate through the winner.
*/
import { detect, loadMapAsync, transliterateAsync } from "../src/index.js"
import { configureSharedStack } from "./_lib/stack.js"

export const title = "Detect the system, then convert"
export const summary =
"Rank the loaded maps by how well they explain a source/target pair, and use the best hit."

export const expected = "Anton Olehovych"

const CANDIDATES = ["bgnpcgn-ukr-Cyrl-Latn-2019", "un-tam-Taml-Latn-1972"] as const

export async function run(): Promise<string> {
configureSharedStack()
for (const system of CANDIDATES) {
await loadMapAsync(system)
}
const [best] = detect("Антон Олегович", "Anton Olehovych")
return transliterateAsync(best.mapName, "Антон Олегович")
}
30 changes: 30 additions & 0 deletions examples/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* The example registry — data, not behavior. Adding an example is a new
* file plus one line here; the test suite iterates this list.
*/
export interface ExampleEntry {
readonly id: string
readonly file: string
/** Executes only under INTERSCRIPT_PLAYGROUND_LIVE=1 (large downloads). */
readonly liveOnly?: boolean
}

export const EXAMPLES: readonly ExampleEntry[] = [
{ id: "convert", file: "./convert.js" },
{ id: "detect-chain", file: "./detect-chain.js" },
{ id: "neural-diacritize", file: "./neural-diacritize.js", liveOnly: true },
{ id: "batch-columns", file: "./batch-columns.js" },
{ id: "server-mode", file: "./server-mode.js" },
]

export interface LoadedExample {
readonly title: string
readonly summary: string
readonly expected: string
run(): Promise<string>
}

export async function loadExample(entry: ExampleEntry): Promise<LoadedExample> {
const mod = (await import(entry.file)) as LoadedExample
return mod
}
Loading