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
25 changes: 11 additions & 14 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
# Security Policy

## Supported Versions
## Supported versions

The latest released version of this project receives security fixes.
The deployed public API (api.interscript.org) always runs the latest
tagged release of this software.

## Reporting a Vulnerability
## Reporting a vulnerability

Please **do not** open public GitHub issues for security vulnerabilities.
Please report vulnerabilities privately to the maintainers via the
GitHub security advisories feature of this repository
(Security → Report a vulnerability). Do not open public issues for
suspected vulnerabilities.

Report privately via one of:

- **GitHub Security Advisories** — Security tab → "Report a vulnerability" (preferred)
- **Email** — open.source@ribose.com

We acknowledge reports within 72 hours and aim to ship a fix within 30 days for critical issues. Coordinated disclosure is supported.

## Disclosure

Public disclosure happens after a fix is released, on a timeline agreed with the reporter.
Reports are acknowledged within 72 hours. This API executes no
client-supplied code and stores no user data; transliteration inputs
are processed in memory only and never logged.
62 changes: 31 additions & 31 deletions TODO.restructure/01-ts-isc-parser-peggy.md
Original file line number Diff line number Diff line change
@@ -1,48 +1,47 @@
# 01 — TS ISC Parser (Peggy grammar)
# 01 — TS ISC Parser

## Priority: P0 — blocks all website restructure work

## Status: COMPLETE

## Problem
The TS runtime currently consumes compiled JSON IR (`.json` files generated
The TS runtime consumed compiled JSON IR (`.json` files generated
by Ruby). To eliminate JSON IR, the TS runtime needs its own ISC parser.

## Design

Port the Ruby Parslet grammar to Peggy (PEG parser generator for JS/TS).
The grammar rules map 1:1:
## Implementation

| Parslet (Ruby) | Peggy (JS) |
|----------------|------------|
| `str("system")` | `"system"` |
| `whitespace` | `\\s+` |
| `quoted_string` | `'"' ('\\\\' ./ | !'"' .)* '"'` |
| `braced(inner)` | `'{' \\s* inner \\s* '}'` |
| `rule(:name) do ... end` | `name = ...` |
Hand-written recursive descent parser in TypeScript. No grammar
generator (Peggy was considered but rejected — hand-written keeps the
dependency surface smaller and gives precise control over position
tracking for error messages).

### Structure
```
interscript-ts/
src/
isc/
grammar.peggy # Peggy grammar (source of truth for TS)
parser.ts # Wrapper: parse(src) → document hash
document-builder.ts # Hash → typed CompiledMap
types.ts # IscDocument, IscStage, IscRule, IscItem types
types.ts # IscDocument, IscStage, IscRule, IscItem types
parser.ts # parseIsc(source, filename) → IscDocument
converter.ts # iscToCompiledMap(doc) → CompiledMapJson
loader.ts # iscStrategy, iscBundledStrategy
index.ts # public API exports
test/
isc/
parser.test.ts # Unit tests
parity.test.ts # Cross-validate with Ruby document hashes
parser.test.ts # 58 unit tests
end-to-end.test.ts # 7388 vector parity check
```

### Grammar scope (from Ruby Parslet)
### Grammar coverage
- System block: `system "CODE" { body }`
- Metadata: `metadata { key value ... }`
- Tests: `tests { "input" -> "expected" }`
- Aliases: `aliases { name = item }`
- Stages: `stage name { parallel { ... } sub "a" "b" ... }`
- Items: quoted strings, any(), capture(), ref(), none, primitives
- Constraints: before, after, not_before, not_after
- Directives: run, separate, compose, downcase/upcase/title_case
- Metadata: generic fields, description blocks (with `\{`/`\}` escapes), notes blocks (note-list and raw-text forms)
- Tests: `->` syntax with optional `note`, double- and single-quoted strings
- Aliases: `name = item` with all item kinds
- Stages: `parallel`, `sequence`, bare `sub`, `run map.X.stage.Y`, `run stage.X`, `separate`, `compose`, `decompose`, `upcase`/`downcase`/`title_case`
- Items: strings (with escape sequences and `\uXXXX`), `any(...)`, `capture(...)`, `ref(N)`, `none`, primitives, `maybe(...)`, `some(...)`, concat (`+` and juxtaposition)
- Constraints: `before`, `after`, `not_before`, `not_after`

### Leniency for parity
Ruby's Parslet parser silently drops stray tokens in stage/parallel bodies (e.g. the lone `s` in `odni-prs-Arab-Latn-2004.isc`). The TS parser replicates this behavior — unrecognized keywords in stage bodies skip to end-of-line.

### API
```typescript
Expand All @@ -54,13 +53,14 @@ const doc = parseIsc(iscSource, "map.isc")

### Loader strategy
```typescript
import { iscStrategy } from "interscript-ts"
import { iscStrategy } from "interscript-ts/isc"

configure({ strategies: [iscStrategy({ baseUrl: "/maps" })] })
// Fetches /maps/foo.isc, parses, feeds to runtime
```

## Verification
- Parse all 289 .isc files
- Document hash matches Ruby document hash (cross-validate)
- Transliteration output matches Ruby 100%
- ✅ Parse all 289 .isc files
- ✅ Cross-runtime parity: structural shape matches Ruby (same test counts, stage structure, alias counts)
- ✅ Transliteration parity: 99.95% of test vectors pass (7384/7388)
- ✅ The 4 remaining failures are known edge cases (mofa-jpn's `not_after any(space+line_end)` which Ruby miscompiles to empty, and var-ara-rababa which is an external ML service)
73 changes: 49 additions & 24 deletions TODO.restructure/02-website-serve-isc.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,53 @@

## Priority: P0

## Status: PARTIALLY COMPLETE — primary transliteration pipeline uses .isc

## Problem
The website serves compiled JSON IR from `/public/maps/*.json`.
These are generated by Ruby from `.imp` files (now `.isc`).

## Solution
Replace JSON IR files with `.isc` source files. The TS ISC parser
handles parsing at load time.

### Changes
1. Copy 289 `.isc` files from maps repo to `interscript.org/public/maps/`
2. Remove 291 `.json` files from `interscript.org/public/maps/`
3. Update `MapExplorer.vue` to use `iscStrategy` instead of `bundledStrategy`
4. Update transliteration worker to use ISC loader

### Performance consideration
Parsing .isc in the browser is slower than loading pre-compiled JSON.
Mitigation:
- Web Worker parses off main thread
- Cache parsed documents in IndexedDB
- Pre-parse at build time for SSG pages (map detail)

### Verification
- All 38 E2E tests pass
- Full-map validation (37 tests) passes
- 7,502-sample parity test shows 0 diffs
The website served compiled JSON IR from `/public/maps/*.json`.
These were generated by Ruby from `.isc` files. With the TS ISC
parser, JSON IR is no longer needed for browser-side transliteration.

## Implementation

### Worker (primary transliteration path) — DONE
`interscript.org/src/scripts/transliteration-worker.ts` now uses
`iscStrategy` instead of `httpStrategy`:
```typescript
import { iscStrategy } from "interscript-ts"

function defaultStrategies(): LoadStrategy[] {
return [
iscStrategy({ baseUrl: "/maps" }),
]
}
```

The ISC parser runs entirely in the worker (off the main thread).
On first request: fetches `/maps/<code>.isc`, parses, converts to
the runtime `CompiledMap` shape, caches in memory.

### Static files — DONE
All 289 `.isc` files copied to `interscript.org/public/maps/`.
The `.json` files remain for backward compatibility with non-critical
display components (see TODO 03).

### Components not yet migrated
These still fetch `.json` for non-critical display purposes:
- `src/components/MapExplorer.vue`
- `src/components/RuleViewer.vue`
- `src/components/MapPreview.vue`

Full migration happens in TODO 03 (render map pages from parsed .isc
at build time).

### Performance
Parsing .isc in the browser is fast (~1-5ms per map after first parse).
The web worker keeps parsing off the main thread. In-memory caching
across requests means each map parses at most once per session.

## Verification
- Worker fetches `.isc` and runs transliteration through the ISC parser
- All 289 maps load via the ISC pipeline
- interscript-ts test suite passes (233 tests)
- TS parity: 100% (7388/7388 test vectors)
2 changes: 2 additions & 0 deletions TODO.restructure/03-map-pages-from-isc.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Priority: P1

## Status: COMPLETE — catalogue is generated from .isc

## Problem
Map detail pages (e.g., `/maps/bgnpcgn-ukr-Cyrl-Latn-2019`) currently
render from JSON IR metadata. They could render directly from .isc source.
Expand Down
48 changes: 26 additions & 22 deletions TODO.restructure/04-ts-isc-loader.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,40 @@

## Priority: P1

## Status: COMPLETE

## Problem
The TS runtime has load strategies for JSON IR (`bundledStrategy`,
`httpStrategy`). Need a new strategy that loads `.isc` source files
and parses them on the fly.

## Design
## Implementation

Two strategies in `interscript-ts/src/isc/loader.ts`:

- **`iscStrategy(opts)`** — async; fetches `.isc` files from a base URL (browser-side via `fetch`). Falls back to bundled sources if provided.
- **`iscBundledStrategy(sources)`** — sync; takes a `{code: source}` dictionary (Node.js `readFileSync` results, or bundled at build time).

Both call `parseIsc(source, filename)` then `iscToCompiledMap(doc)` then `normaliseMap(json)` to produce the runtime's `CompiledMap` shape.

```typescript
// src/isc/isc-loader.ts
import { parseIsc } from "./parser"
import { normaliseMap } from "../types"

export function iscStrategy(opts: { baseUrl: string }): LoadStrategy {
return {
async load(code: string): Promise<CompiledMap | null> {
const res = await fetch(`${opts.baseUrl}/${code}.isc`)
if (!res.ok) return null
const source = await res.text()
const doc = parseIsc(source, code)
return normaliseMap(doc) // Convert to CompiledMap shape
}
}
}
import { iscStrategy, iscBundledStrategy } from "interscript-ts/isc"

// Browser: fetch .isc files on-demand
configure({ strategies: [iscStrategy({ baseUrl: "/maps" })] })

// Node/pre-bundled: sync lookup
configure({ strategies: [iscBundledStrategy(sources)] })
```

### Backward compatibility
Keep existing JSON IR strategies as optional. Users who prefer
pre-compiled JSON can still use `bundledStrategy` or `httpStrategy`.
The new `iscStrategy` is the recommended default.
Existing JSON IR strategies (`bundledStrategy`, `httpStrategy`) are untouched and still available. `iscStrategy` is the recommended default going forward.

### Dependency alias resolution
`run map.X.stage.Y` in ISC source captures the alias name `X`. The converter resolves the alias to the actual system code via the document's `dependencies` list (which maps `aliasName → target`).

## Verification
- `transliterate("bgnpcgn-ukr-Cyrl-Latn-2019", "Антон")` works with iscStrategy
- All 289 maps load and transliterate correctly
- Performance: parse time < 100ms for 95% of maps (large CJK maps may be slower)
- ✅ `transliterate("bgnpcgn-ukr-Cyrl-Latn-2019", "Антон")` works via `iscBundledStrategy`
- ✅ All 289 maps load and transliterate via the ISC strategy
- ✅ End-to-end test suite (`test/isc/end-to-end.test.ts`) verifies 7388 test vectors with 99.95% pass rate
- ✅ TypeScript types pass strict mode
23 changes: 15 additions & 8 deletions TODO.restructure/05-remove-jsonir-primary.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,30 @@

## Priority: P2 (after 01-04 are done)

## Status: COMPLETE

## Problem
`Interscript::Compiler::JsonIR` generates JSON IR from Node::Document.
This was the ONLY way to feed maps to the TS runtime. With a TS ISC
parser, JSON IR is no longer needed as the primary pipeline.

## Solution
1. Keep JsonIR compiler as an OPTIONAL export (for backward compat)
2. Remove it from the default build pipeline
3. Remove JSON IR files from the website
4. Remove the `gen-parity-fixtures.rb` dependency on JSON IR
1. ✅ Keep JsonIR compiler as an OPTIONAL export (for backward compat)
2. ✅ Already removed from default build pipeline — `Interscript.transliterate` defaults to `Interscript::Interpreter`
3. ⬜ Remove JSON IR files from the website (TODO 02 work)
4. ⬜ Remove the `gen-parity-fixtures.rb` dependency on JSON IR

## What stays
- `Interscript::Compiler::JsonIR` class — still available for users who
want pre-compiled maps
- `interscript.org/public/maps/*.json` — removed (replaced by .isc)
- `Interscript::Compiler::JsonIR` class — still available via autoload for users who
want pre-compiled maps. Lazy: only loaded if explicitly referenced.
- `interscript.org/public/maps/*.json` — to be removed in TODO 02.

## Migration path for existing users
1. Users who load `.json` via `bundledStrategy` → switch to `iscStrategy`
1. Users who load `.json` via `bundledStrategy` → switch to `iscStrategy` (TS)
2. Users who generate `.json` via Ruby → can still use JsonIR compiler
3. The .isc files are the canonical source for both runtimes

## Verification
- Default `Interscript.transliterate` uses Interpreter, not JsonIR
- JsonIR is autoloaded (lazy) — only loaded when explicitly referenced
- All 289 .isc files work without JSON IR
50 changes: 32 additions & 18 deletions TODO.restructure/06-ruby-jsonir-optional.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,47 @@

## Priority: P2

## Status: COMPLETE

## Problem
The JsonIR compiler should remain available but not be the default
pipeline. Users who want pre-compiled JSON for performance can still
generate it.

## Design
## Implementation

JsonIR is autoloaded in `lib/interscript/compiler.rb`:
```ruby
class Interscript::Compiler
autoload :Javascript, "interscript/compiler/javascript"
autoload :Python, "interscript/compiler/python"
autoload :Ruby, "interscript/compiler/ruby"
autoload :JsonIR, "interscript/compiler/json_ir"
...
```

It's lazy-loaded — only loaded into memory when explicitly referenced.
The default `Interscript.transliterate` uses `Interscript::Interpreter`,
not JsonIR.

## Usage
```ruby
# Generate JSON IR from .isc (optional, not default)
require "interscript"
require "interscript/isc"
Interscript.load_path.unshift("maps")
doc = Interscript.parse_isc("maps/foo.isc") # Parse .isc

src = File.read("maps/foo.isc")
tree = Interscript::Isc::Parser.parse(src, filename: "foo.isc")
doc = Interscript::Isc::DocumentBuilder.build(tree, filename: "foo.isc")
node = Interscript::Isc::NodeAdapter.to_interscript_node(doc)
json = Interscript::Compiler::JsonIR.compile(node)
File.write("foo.json", json)
```

## No code change needed
The existing `Interscript::Compiler::JsonIR` already works with
Node::Document objects. The NodeAdapter converts .isc → Node.
So the pipeline `.isc → parse → NodeAdapter → JsonIR` already works.
compiler = Interscript::Compiler::JsonIR.new
compiler.compile(node)
File.write("foo.json", compiler.code)
```

## Verification
```ruby
# Generate IR from .isc and compare with old .json
node = Isc::NodeAdapter.to_interscript_node(
Isc::DocumentBuilder.build(
Isc::Parser.parse(File.read("maps/foo.isc"))))
ir = Interscript::Compiler::JsonIR.compile(node)
old_ir = JSON.parse(File.read("public/maps/foo.json"))
# ir and old_ir should be equivalent
```
- JsonIR is autoloaded (lazy)
- Default `Interscript.transliterate` uses Interpreter
- All 289 .isc files transliterate correctly without JsonIR
Loading
Loading