diff --git a/.claude/skills/translate-examples-to-swift/SKILL.md b/.claude/skills/translate-examples-to-swift/SKILL.md new file mode 100644 index 0000000000..2e00202d85 --- /dev/null +++ b/.claude/skills/translate-examples-to-swift/SKILL.md @@ -0,0 +1,315 @@ +--- +name: translate-examples-to-swift +description: Translates inline JavaScript example code to Swift +--- + +## Usage + +Invoke this skill with `/translate-examples-to-swift` followed by a description of what to translate. + +### Examples + +``` +/translate-examples-to-swift translate the JavaScript examples in src/pages/docs/ai-transport/streaming.mdx +``` + +``` +/translate-examples-to-swift translate all JavaScript code blocks in the src/pages/docs/messages/ directory +``` + +``` +/translate-examples-to-swift translate the code block at line 45 of src/pages/docs/channels/index.mdx +``` + +### What to specify + +- **File or directory**: Which documentation file(s) contain the examples to translate +- **Scope** (optional): Specific code blocks if you don't want to translate all examples in a file + +--- + +## Architecture Overview + +This skill uses a **three-phase architecture** with independent translation, verification, and assembly: + +1. **Translation phase**: Spawn one sub-agent per MDX file. Each translates examples, self-checks compilation (for iteration), inserts into MDX, and writes translation metadata JSON. + +2. **Verification phase**: Spawn one sub-agent per MDX file. Each reads Swift code from the MDX (source of truth), compiles in a fresh harness, assesses faithfulness, and writes verification results JSON. + +3. **Assembly phase**: Run `consolidate.sh` to merge JSONs and generate review HTML. + +**Key principle**: Verification reads from MDX, not from translation output. This ensures verification tests what actually ships. + +**Verify-only mode**: When translations already exist in MDX but no translation JSONs are available, you can skip the translation phase and run only verification + assembly. See the "Verify-only workflow" section below. + +**Always delegate**: Spawn a sub-agent for each file, even for single-file tasks. This keeps behavior consistent and context isolated. + +--- + +## Orchestrator Constraints + +The orchestrator (you, when running this skill) coordinates subagents but does NOT directly modify: +- MDX documentation files +- Translation JSON files (`swift-translations/translations/*.json`) +- Verification JSON files (`swift-translations/verifications/*.json`) + +All modifications to these files must go through the appropriate subagent. The orchestrator may: +- Read files to understand state +- Run validation commands +- Run the consolidation script +- Spawn and coordinate subagents +- Report results to the user + +This separation ensures that all code changes are tested before being written, and all verification results reflect actual verification. + +--- + +## Output Directory Structure + +All intermediate files go in `swift-translations/` at the repo root: + +``` +swift-translations/ + harness-{filename}/ # Test harness per translation subagent + verify-{filename}/ # Test harness per verification subagent + translations/ + {filename}.json # One per MDX file + verifications/ + {filename}.json # One per MDX file + consolidated.json # Merged data for review app (generated by script) + review.html # Human review interface (generated by script) +``` + +The `{filename}` is derived from the MDX filename without path or extension: +- `src/pages/docs/ai-transport/messaging/citations.mdx` → `citations` +- `src/pages/docs/ai-transport/token-streaming/message-per-token.mdx` → `message-per-token` + +--- + +## Workflow + +### Step 1: Spawn translation subagents + +For each MDX file to translate, spawn a translation subagent. + +**Get the prompt from the file** `.claude/skills/translate-examples-to-swift/prompts/translation-subagent.md`: + +1. Read the file +2. Replace `{FILEPATH}` with the full path to the MDX file being translated +3. Replace `{FILENAME}` with the MDX filename without path or extension +4. Pass the result as the subagent prompt + +**Do not paraphrase or rewrite the prompt.** Use the file contents with only the placeholders replaced. + +``` +Tool: Task +subagent_type: "general-purpose" +prompt: +``` + +#### Validate translation output + +After each translation subagent completes, validate its JSON output: + +```bash +npx ajv-cli validate \ + -s .claude/skills/translate-examples-to-swift/schemas/translation.schema.json \ + -d swift-translations/translations/{filename}.json +``` + +If validation fails, report the error. + +### Step 2: Spawn verification subagents + +After translation subagents complete, spawn verification subagents for each file that was translated. + +**Important**: Spawn a verification subagent for EVERY file that had a translation subagent, even if that file had no examples. This ensures 1:1 matching between translation and verification outputs, avoiding special-case handling in the consolidation phase. + +**Get the prompt from the file** `.claude/skills/translate-examples-to-swift/prompts/verification-subagent.md`: + +1. Read the file +2. Replace `{FILEPATH}` with the full path to the MDX file being verified +3. Replace `{FILENAME}` with the MDX filename without path or extension +4. Pass the result as the subagent prompt + +**Do not paraphrase or rewrite the prompt.** Use the file contents with only the placeholders replaced. + +``` +Tool: Task +subagent_type: "general-purpose" +prompt: +``` + +#### Why independent verification? + +- **Fresh perspective**: The verifier has no memory of translation decisions, so it approaches the code without bias +- **Catches copy-paste errors**: Ensures the code in documentation matches what was actually tested +- **Validates harness comments**: Confirms the test harness comments are complete and usable +- **Faithfulness check**: Compares translations against originals with fresh eyes + +#### Validate verification output + +After the verification subagent completes, validate its JSON output: + +```bash +npx ajv-cli validate \ + -s .claude/skills/translate-examples-to-swift/schemas/verification.schema.json \ + -d swift-translations/verifications/{filename}.json +``` + +If validation fails, report the error. + +#### Handling verification results + +When the verification subagent returns: + +1. **All passed**: Proceed to Step 3 (generate review file) +2. **Compilation failures or faithfulness concerns**: + - Report the failures to the user with details + - Ask if they want you to attempt a fix + - If yes, spawn a **new translation subagent** (or resume the original one) to fix the issue + - After the fix subagent completes, spawn a **new verification subagent** to re-verify + - Repeat until verification passes or the user decides to skip + +**Important**: The orchestrator must NEVER directly edit MDX files or verification JSON files. All changes to translations must go through a translation subagent. All verification results must come from a verification subagent. + +### Step 3: Generate review file + +Run the consolidation script to merge JSONs and generate the review HTML: + +```bash +.claude/skills/translate-examples-to-swift/scripts/consolidate.sh +``` + +This reads from `swift-translations/translations/` and `swift-translations/verifications/`, then produces: +- `swift-translations/consolidated.json` - merged data +- `swift-translations/review.html` - human review interface + +The review HTML provides: +- Side-by-side comparison of JavaScript and Swift code +- Syntax highlighting +- Translation notes and decisions (collapsible) +- Test harness context (collapsible) +- Verification results (compilation status, faithfulness rating) +- Interactive review controls (approve/flag/skip with comments) +- Export functionality for review summary + +### Step 4: Report back to the user + +Report back to the user, explaining: + +- any important decisions that were made, such as deviations from the JavaScript code +- the verification results summary +- any issues that require human attention +- the location of the review file for human review + +Example: + +``` +Translation complete. + +## Summary +- Files processed: 2 +- Examples translated: 5 +- Compilation: 4 passed, 1 failed + +## Review file +Open the review file to examine translations: + swift-translations/review.html + +## Issues requiring attention +- src/pages/docs/messages/updates-deletes.mdx:78 - Compilation failed: `updateSerial` property not found +``` + +--- + +## Handling user feedback + +When the user reviews the generated review file and provides feedback: + +1. Spawn a **translation subagent** (or resume the original one) to make the requested changes + - The subagent will update the test harness, verify compilation, and update the MDX +2. Spawn a **verification subagent** to independently verify the changes +3. Regenerate the review file with updated data +4. Report back to the user + +**Important**: The orchestrator must NEVER directly edit MDX files, translation JSON, or verification JSON. All file modifications must go through the appropriate subagent. This ensures: +- Every change is tested before being written +- The verification JSON always reflects actual verification results +- There's a clear audit trail of what each agent did + +**This is not optional.** Any change to a translation—whether from user feedback, verification comments, or your own corrections—must go through the full verify-and-review cycle before being considered complete. + +--- + +## Verify-only workflow + +Use this workflow when Swift translations already exist in the MDX files but no translation JSON files are available. This happens when: +- Re-verifying translations from a previous session +- Manual edits were made directly to MDX files +- Translation JSON output was discarded or lost + +### Step 1: Spawn verification subagents + +Same as the normal workflow Step 2 — spawn a verification subagent for each MDX file. Use the prompt from `.claude/skills/translate-examples-to-swift/prompts/verification-subagent.md` with placeholders replaced. + +### Step 2: Generate translation stubs + +After verification subagents complete, generate stub translation JSONs from the verification data: + +```bash +.claude/skills/translate-examples-to-swift/scripts/generate-translation-stubs.sh +``` + +This reads each verification JSON and creates a matching translation JSON with a "verify-only" info note. It skips files that already have a translation JSON, so it's safe to run even if some real translations exist. + +### Step 3: Run consolidation + +Run `consolidate.sh` as normal: + +```bash +.claude/skills/translate-examples-to-swift/scripts/consolidate.sh +``` + +### Step 4: Report back + +Report as normal, but note that translation notes are unavailable since the translation phase was skipped. The review HTML will show a "verify-only" info note in place of translation decision notes. + +--- + +### The verification invariant + +**Never output code that hasn't been verified.** This is the core principle of the skill: + +- Every Swift code block inserted into documentation must have passed `swift build` in a test harness +- Every change to existing Swift code must be re-verified before the task is complete +- The review file must always reflect the current state of the translations + +If you find yourself about to report completion without having verified recent changes, stop and run verification first. + +--- + +## Scripts + +Scripts are in `.claude/skills/translate-examples-to-swift/scripts/`: + +- `consolidate.sh` - Merges translation and verification JSONs, validates, generates review HTML +- `generate-translation-stubs.sh` - Generates stub translation JSONs from verification data (for verify-only mode) + +Scripts are in `.claude/skills/translate-examples-to-swift/review-app/`: + +- `generate-review.sh` - Generates review HTML from consolidated JSON (called by consolidate.sh) + +## JSON Schemas + +Schemas are in `.claude/skills/translate-examples-to-swift/schemas/`: + +- `translation.schema.json` - Translation sub-agent output (notes and metadata) +- `verification.schema.json` - Verification sub-agent output (code, harness, results) +- `consolidated.schema.json` - Final merged data for review app + +Validate with: + +```bash +npx ajv-cli validate -s {schema} -d {data} +``` diff --git a/.claude/skills/translate-examples-to-swift/prompts/translation-subagent.md b/.claude/skills/translate-examples-to-swift/prompts/translation-subagent.md new file mode 100644 index 0000000000..a20805a3b7 --- /dev/null +++ b/.claude/skills/translate-examples-to-swift/prompts/translation-subagent.md @@ -0,0 +1,674 @@ +You are a translation sub-agent. Your job is to translate JavaScript examples to Swift in a single MDX file. + +## File to translate + +{FILEPATH} + +## Output + +Write your translation metadata to: + swift-translations/translations/{FILENAME}.json + +The JSON must conform to the schema at `.claude/skills/translate-examples-to-swift/schemas/translation.schema.json`. Read that schema file to understand the required structure. Validate your output with `npx ajv-cli validate` — do not use python or other tools for JSON validation. + +--- + +## Translation Steps + +Translate every JavaScript code block in the documentation unless explicitly told otherwise. For each one, you will: + +1. Generate a Swift test harness +2. Translate the JavaScript code +3. Insert the translated code into the test harness +4. Use the test harness to verify the translated code +5. Insert the translated code into the documentation + +As you work through these steps, collect the data needed for the translation JSON. For each example, you'll need: the example ID, line number, and any translation notes/decisions. + +### Example IDs + +Generate a unique ID for each translated example. IDs are random 6-character alphanumeric strings (mixed case), e.g. `Kx9mQ3`, `tR4wBn`, `Hj7nPq`. Just pick a random-looking string — the only requirement is uniqueness. + +IDs are written into the MDX harness comment (so the verification agent can read them) and used in your JSON output. They are also used in the harness function name (e.g., `func example_Kx9mQ3(...)`) during compilation. + +Generate an ID for each translated example. + +--- + +## 1. Generate a Swift test harness + +Generate a Swift _test harness_. This is a Swift program into which we will subsequently insert the translated code in order to verify that the translated code compiles. This is necessary because a given block may not be self-contained; it may assume that there are variables or other types that already exist. The test harness provides these types. + +**Important**: The harness provides ONLY the context that is NOT present in the original JavaScript example. If the JavaScript example creates something (a client, a variable, a function), the Swift translation should also create it—that code belongs in the translated example, not hidden in the harness. The harness is for things the JS example _assumes_ exist but doesn't show. + +1. Read the JavaScript code example and the context surrounding this example in the documentation. + +2. Decide what context needs to exist in the test harness—i.e., what does the JS code assume exists but not create? + +For example, given this JavaScript: + +```javascript +// Publish initial message and capture the serial for appending tokens +const { serials: [msgSerial] } = await channel.publish({ name: 'response', data: '' }); + +// Example: stream returns events like { type: 'token', text: 'Hello' } +for await (const event of stream) { + // Append each token as it arrives + if (event.type === 'token') { + channel.appendMessage({ serial: msgSerial, data: event.text }); + } +} +``` + +We can see that the following must exist: + +- a realtime channel (the `channel` variable) + - from the surrounding context, you can infer that this is a `RealtimeChannel` + - the equivalent in ably-cocoa is an `ARTRealtimeChannel` +- a stream of events (the `stream` variable) + - from the surrounding context, you can infer that this is an `AsyncIterable` whose elements have a user-provided type that has shape `{ type: string, text: string }` + - the equivalent in Swift could be an `any AsyncSequence<(type: String, text: String), Never>` using a tuple with labeled elements + +### Setting up the Swift test harness + +Create a Swift package with ably-cocoa as a dependency: + +1. Create the package (use a unique directory per file being translated): + ```bash + mkdir -p swift-translations/harness-{FILENAME} && cd swift-translations/harness-{FILENAME} + swift package init --type executable + ``` + +2. Update `Package.swift`: + ```swift + // swift-tools-version: 6.2 + import PackageDescription + + let package = Package( + name: "SwiftTestHarness", + platforms: [ + .macOS(.v26) // Required for Task.immediate (Swift 6.2+) + ], + dependencies: [ + .package(url: "https://github.com/ably/ably-cocoa", from: "1.2.0") + ], + targets: [ + .executableTarget( + name: "SwiftTestHarness", + dependencies: [ + .product(name: "Ably", package: "ably-cocoa") + ] + ), + ] + ) + ``` + +3. Put your test harness code in `Sources/SwiftTestHarness/SwiftTestHarness.swift` with `import Ably` at the top. + +4. Build with `swift build` to verify compilation. + +### Providing context via parameters vs stub type declarations + +The goal is to make everything the original JavaScript code assumes exists available in scope. The easiest way to do this is to pass things as parameters to the `example()` function, spelling their types using function types, tuples, and existentials. + +**Use parameters** (the default) when the type only needs to exist in the function signature—i.e., the translated code uses values of that type but doesn't need to spell the type name itself. + +For example, if the JavaScript code calls `loadResponsesFromDatabase()` which returns an object with a `latest()` method and a `has()` method, you can spell this as a parameter: + +```swift +func example( + loadResponsesFromDatabase: () -> ( + latest: () -> (timestamp: Date, Void), + has: (String) -> Bool + ), + channel: ARTRealtimeChannel +) { + let completedResponses = loadResponsesFromDatabase() + let latestTimestamp = completedResponses.latest().timestamp + if completedResponses.has(responseId) { ... } +} +``` + +**Use stub type declarations** (in the enclosing scope) when the translated code itself needs to reference the type name—for type annotations, instantiation, or type inference hints. For example: + +```swift +// Stub type declaration needed because the translated code references `ResponseData` by name +struct ResponseData { + var timestamp: Date +} + +func example(...) { + // The type name `ResponseData` appears in the translated code + let responses: [String: ResponseData] = [:] + ... +} +``` + +The key question is: does the type name appear *inside* the translated code, or only in the harness's function signature? + +When stub type declarations are needed, wrap them in an enclosing function to provide scope. Use the example ID in the function name: + +```swift +func exampleContext_Kx9mQ3() { + // Stub type declaration needed because translated code references `ResponseData` by name + struct ResponseData { + var timestamp: Date + } + + func example(...) { + let responses: [String: ResponseData] = [:] // type name appears in translated code + ... + } +} +``` + +### Putting it together + +For the running example (which doesn't need stub type declarations), create a simple function. **Use the example ID in the function name** to enable correlation: + +```swift +// The body of this function is the translation of the example. +// Function name includes the example ID +func example_Kx9mQ3(channel: ARTRealtimeChannel, stream: any AsyncSequence<(type: String, text: String), Never>) async throws { + // TODO: fill in with translation of example (to come in next step) +} +``` + +Now **confirm that the test harness builds cleanly**: `swift build`. + +--- + +## 2. Translate the JavaScript code + +Now translate the JavaScript code to Swift, using your knowledge of ably-js and ably-cocoa. + +### Looking up API details + +When you need to look up specific API details (method signatures, parameter types, return types), consult the auto-generated SDK documentation: + +- **JavaScript SDK**: https://ably.com/docs/sdk/js/v2.0 (to understand what you're translating FROM) +- **Swift/Cocoa SDK**: https://ably.com/docs/sdk/cocoa/v1.2/ (to understand what you're translating TO) + +Note that some items may not appear in the auto-generated docs. If you can't find something: + +1. **Check existing Swift examples** in this documentation repository for reference +2. **Look at the ably-cocoa header files** in the test harness's SPM checkout. After running `swift build`, the source is available at `.build/checkouts/ably-cocoa/Source/include/Ably/`. Use `find` and `grep` to locate the header file you need, then read it directly. The headers contain authoritative type definitions, method signatures, and documentation comments. + +**Do not fetch files from GitHub** to look up API details. The SPM checkout already contains the exact version of the SDK you're compiling against, so it's both faster and more reliable to read the headers locally. + +### Looking up translation patterns + +For examples of how JavaScript code is typically translated to Swift (e.g., how callbacks are structured, how async/await becomes completion handlers), look at existing Swift examples in this documentation repository. For example, `src/pages/docs/messages/updates-deletes.mdx` contains Swift examples alongside JavaScript that demonstrate common patterns. + +### Guidance + +- Keep the translated code as close to the original JavaScript as possible; don't make material changes without good reason + +#### Bridging ably-cocoa callbacks with `async` / `await` + +The ably-cocoa SDK uses Objective-C-style callbacks, but all translated Swift code should use `async` / `await` to match the JavaScript examples' structure. Bridge one-shot SDK callbacks with `withCheckedThrowingContinuation`. + +There are three categories of SDK call, each translated differently: + +##### One-shot calls where JS uses `await` + +When JavaScript `await`s a one-shot SDK call (publish, history, annotations.publish, etc.), bridge it with `withCheckedThrowingContinuation`. Always `await` when JS does, even if the result isn't used — this preserves the JS semantics and keeps translations consistent. + +##### One-shot calls where JS does NOT `await` (fire-and-forget) + +When JavaScript intentionally does not `await` a one-shot SDK call (e.g. `channel.appendMessage(...)` with no `await`), simply call the ably-cocoa method without a callback (or with a no-op callback `{ _, _ in }` if the API requires one). Do NOT wrap it in a `Task` — this preserves call ordering (important when appending tokens in a loop) and is the most concise translation. + +For example, given this JavaScript: + +```javascript +const { serials: [msgSerial] } = await channel.publish({ name: 'response', data: '' }); + +for await (const event of stream) { + // No await — fire-and-forget + channel.appendMessage({ serial: msgSerial, data: event.text }); +} +``` + +The `publish` is awaited, so it gets a continuation. The `appendMessage` is not awaited, so it's called directly without a callback: + +```swift +let publishResult = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + channel.publish("response", data: "") { result, error in + if let error { + continuation.resume(throwing: error) + } else { + continuation.resume(returning: result!) + } + } +} + +guard let msgSerial = publishResult.serials.first?.value else { + print("No serial returned") + return +} + +for await event in stream { + let messageToAppend = ARTMessage() + messageToAppend.serial = msgSerial + messageToAppend.data = event.text + + channel.append(messageToAppend, operation: nil, params: nil) +} +``` + +##### Deferred error checking (`Promise.allSettled` pattern) + +When the JS code fires off multiple operations and then _later_ checks for failures (e.g. collecting promises and checking with `Promise.allSettled`), use `Task.immediate` with a continuation inside. `Task.immediate` (Swift 6.2+) starts executing immediately on the current executor before yielding, which preserves the ordering of SDK calls — unlike `Task { }`, which provides no ordering guarantee. Collect the `Task` handles and await them after the loop. + +Include a comment in the translated code explaining why `Task.immediate` is used, along the lines of: + +``` +// Task.immediate starts on the calling context and continues until it suspends, +// preserving the order of append calls. Users targeting OS versions prior to +// macOS 26 / iOS 26 may need to use a callback-based approach instead. +``` + +Example: + +```swift +var appendTasks: [Task] = [] + +for await event in stream { + let messageToAppend = ARTMessage() + messageToAppend.serial = msgSerial + messageToAppend.data = event.text + + // Task.immediate starts on the calling context and continues until it suspends, + // preserving the order of append calls. Users targeting OS versions prior to + // macOS 26 / iOS 26 may need to use a callback-based approach instead. + let task = Task.immediate { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + channel.append(messageToAppend, operation: nil, params: nil) { _, error in + if let error { + continuation.resume(throwing: error) + } else { + continuation.resume() + } + } + } + } + appendTasks.append(task) +} + +// Check for any failures after the stream completes +var failed = false +for task in appendTasks { + do { + try await task.value + } catch { + failed = true + } +} +``` + +##### Persistent listeners (subscribe) + +`channel.subscribe` callbacks fire multiple times, so they cannot be bridged to a single `await`. These remain as callbacks in the translated code. + +However, when the JS `await`s the subscribe call itself (e.g. `await channel.subscribe('prompt', callback)`), the `await` is waiting for the implicit channel attach. Bridge this with `subscribe(_:onAttach:callback:)`: + +```swift +try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + channel.subscribe("prompt", onAttach: { error in + if let error { + continuation.resume(throwing: error) + } else { + continuation.resume() + } + }, callback: { message in + // This callback fires for each message — it stays as a callback + processMessage(message) + }) +} +``` + +If the JS does NOT `await` the subscribe (just `channel.subscribe('prompt', callback)` with no `await`), use the simple form without `onAttach:`. + +#### The `(result, error)` callback convention + +ably-cocoa callbacks pass `(Result?, Error?)` where the convention is exactly one is non-nil. + +**Exception — pagination callbacks**: `ARTPaginatedResult.next` calls back with `(nil, nil)` when there are no more pages (per spec point TG4). Use an optional continuation type for pagination and check for nil after the `await`. + +**When you need the result**, force-unwrap `result!` inside the continuation — this enforces the convention and is the only acceptable use of force-unwrap: + +```swift +let publishResult = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + channel.publish("response", data: data) { result, error in + if let error { + continuation.resume(throwing: error) + } else { + continuation.resume(returning: result!) // Force-unwrap: if no error, result is non-nil + } + } +} +``` + +After the continuation, use optional chaining + `guard` for everything else — never force-unwrap beyond the callback convention itself: + +```swift +guard let msgSerial = publishResult.serials.first?.value else { + print("No serial returned") + return +} +``` + +The callback body should do nothing beyond resuming the continuation. Do not extract values, perform logic, or branch inside the callback — the whole point of the continuation is to get the result out so you can work with it sequentially after the `await`. + +**When you only care about success or failure** (you don't need the result value), just check the error — do not force-unwrap a result you're going to discard: + +```swift +try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + channel.publish([message]) { _, error in + if let error { + continuation.resume(throwing: error) + } else { + continuation.resume() + } + } +} +``` + +#### The `channel.history` special case + +`ARTRealtimeChannel.history(_:callback:)` is unusual: the ObjC method signature is `- (BOOL)history:callback:error:`, which Swift bridges as `throws`. The continuation closure passed to `withCheckedThrowingContinuation` is non-throwing (`(CheckedContinuation) -> Void`), so you need a `do`/`catch` _inside_ the continuation to handle the synchronous `throws`: + +```swift +var page = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation?, Error>) in + do { + try channel.history(query) { page, error in + if let error { + continuation.resume(throwing: error) + } else { + continuation.resume(returning: page) + } + } + } catch { + continuation.resume(throwing: error) + } +} +``` + +#### Handling mutable state with @MainActor + +This section applies when **subscribe callbacks mutate local state**. Since subscribe callbacks remain as callbacks (they can't be bridged to `await`), and they need to access mutable state declared outside the callback, you need `@MainActor` isolation. + +**Do NOT create custom actor types** (e.g., `actor PendingPrompts { ... }` or `actor ActiveRequestsStore { ... }`). This adds unnecessary complexity and diverges from the JavaScript's simple approach. + +Instead, mark the harness function with `@MainActor` and use plain local variables. Since ably-cocoa executes callbacks on the main thread by default, use `MainActor.assumeIsolated { }` inside subscribe callbacks to access main-actor-isolated state: + +```swift +@MainActor +func example(channel: ARTRealtimeChannel) { + // Mutable state as simple local variables, just like in JavaScript + var pendingPrompts: [String: String] = [:] + + // ably-cocoa callbacks run on main thread, so use MainActor.assumeIsolated + // to tell the compiler it's safe to access @MainActor state + channel.subscribe { message in + MainActor.assumeIsolated { + // This compiles because we're asserting we're on the main actor + pendingPrompts[message.id] = message.data as? String + } + } +} +``` + +If a subscribe callback needs to do async work (e.g. calling a nested `async` function) that also touches the mutable state, use `Task { }` inside `MainActor.assumeIsolated { }`. The `Task` inherits `@MainActor` isolation from the `assumeIsolated` closure, so an explicit `@MainActor` annotation on the `Task` is not needed: + +```swift +@MainActor +func example(channel: ARTRealtimeChannel) { + var activeRequests: [String: String] = [:] + + channel.subscribe("user-input") { message in + MainActor.assumeIsolated { + let promptID = (message.data as? [String: Any])?["promptId"] as? String ?? "" + activeRequests[promptID] = "processing" + + Task { + defer { activeRequests.removeValue(forKey: promptID) } + await processRequest(promptID) + } + } + } +} +``` + +**When `@MainActor` is NOT needed**: If no subscribe callback mutates local state — i.e. callbacks just pass data to functions or print output — then `@MainActor` should not be used on the harness function. If a subscribe callback needs to launch async work but doesn't touch shared mutable state, a plain `Task { }` (without `@MainActor` or `MainActor.assumeIsolated`) suffices. + +#### Nested functions + +If the JavaScript example defines a function (like `async function processAndRespond(...)`), translate it as a nested function inside the harness function body. The nested function becomes part of the translated example code, not a harness parameter: + +```swift +@MainActor +func example(channel: ARTRealtimeChannel) { + // Nested async function - part of the translated example + func processAndRespond(prompt: String, promptId: String) async { + // ... + } + + // Rest of translated code that calls processAndRespond +} +``` + +#### Conventions + +Follow these conventions in all Swift translations: + +**C1. Template variables**: Use the same template variables as the JavaScript code (`'{{RANDOM_CHANNEL_NAME}}'`, `{{API_KEY}}`, `{{APP_ID}}`). Never hardcode channel names like `"test-channel"` or `"my-channel"` when the JS uses a template variable. + +**C2. Setting `message.extras`**: Use `as ARTJsonCompatible` (not `as NSDictionary`) when assigning dictionary literals to `message.extras`, since Swift cannot implicitly bridge `[String: Any]` to `(any ARTJsonCompatible)?`. Use the SDK protocol type rather than the Foundation type. Example: +```swift +message.extras = ["ai.ably.chat": ["think": true]] as ARTJsonCompatible +``` + +**C3. `authCallback` token values**: Use `as ARTTokenDetailsCompatible` when passing a `String` token to the `authCallback` callback, since `String` doesn't implicitly conform. Example: +```swift +options.authCallback = { tokenParams, callback in + Task { + do { + let url = URL(string: "/api/auth/token")! + let (data, _) = try await URLSession.shared.data(from: url) + let token = String(data: data, encoding: .utf8)! + callback(token as ARTTokenDetailsCompatible, nil) + } catch { + callback(nil, error) + } + } +} +``` + +**C4. Reading `message.extras`**: Always use `toJSON()` for reading extras: +```swift +guard let extras = try? message.extras?.toJSON() as? [String: Any], + let aiExtras = extras["ai.ably.chat"] as? [String: Any] else { return } +``` +Do NOT use `as? NSDictionary`, `as? ARTJsonCompatible`, or `as? [String: Any]` directly on `message.extras`. + +**C5. Swift naming — `ID` not `Id`**: Use `ID` in Swift variable names per Swift API Design Guidelines: `promptID`, `responseID`, `toolCallID`, `userID`, `clientID`. Keep dictionary *keys* unchanged (`"promptId"`, `"responseId"` etc.) since those are cross-platform wire format. + +**C6. No empty-string fallback for `clientId`**: Instead of `message.clientId ?? ""`, use a guard: +```swift +guard let userID = message.clientId else { return } +``` +Exception: `?? ""` is acceptable inside string interpolation purely for display (e.g., `print("User: \(member.clientId ?? "")")`). + +**C7. Avoid `as Any` casts**: `as Any` is a code smell. Fix depending on cause: +- **Optional in dictionary literal**: Guard/unwrap the optional first, then put the unwrapped value in the dictionary. +- **Discriminated data**: Use an enum with associated data so each case carries exactly the fields it needs. +- **Optional in non-optional context**: Guard/unwrap. + +**C8. No `(value: T, Void)` tuples in user-visible code**: Do NOT use tuples like `(text: String, Void)` to mimic JS objects with one property in the **translated example code** that readers see. Use `T` directly — e.g., `[String: String]` instead of `[String: (text: String, Void)]`. However, `(value: T, Void)` tuples are acceptable in **harness parameters** (the function signature in the harness comment) where they provide labelled property access matching the JS original (e.g., `latest: () -> (timestamp: Date, Void)` so the visible code can write `.latest().timestamp`). + +**C9. No `nonisolated(unsafe)`**: Never use `nonisolated(unsafe)` for mutable state. Instead, mark the harness function with `@MainActor` and use `MainActor.assumeIsolated { }` inside ably-cocoa subscribe callbacks to access main-actor-isolated state. See the [Handling mutable state with @MainActor](#handling-mutable-state-with-mainactor) section above. + +--- + +For example, a candidate translation of the running example would be: + +```swift +// Publish initial message and capture the serial for appending tokens +let publishResult = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + channel.publish("response", data: "") { result, error in + if let error { + continuation.resume(throwing: error) + } else { + continuation.resume(returning: result!) + } + } +} + +guard let msgSerial = publishResult.serials.first?.value else { + print("No serial returned") + return +} + +// Example: stream returns events like { type: 'token', text: 'Hello' } +for await event in stream { + // Append each token as it arrives + if event.type == "token" { + let messageToAppend = ARTMessage() + messageToAppend.serial = msgSerial + messageToAppend.data = event.text + + channel.append(messageToAppend, operation: nil, params: nil) + } +} +``` + +Note how the JS `await channel.publish(...)` becomes a `try await withCheckedThrowingContinuation`, while the JS `channel.appendMessage(...)` (no `await`) is called directly without a callback. + +--- + +## 3. Insert the translated code into the test harness + +Insert the translated code from step 2 into the test harness from step 1: + +```swift +func example_Kx9mQ3(channel: ARTRealtimeChannel, stream: any AsyncSequence<(type: String, text: String), Never> & Sendable) async throws { + // Publish initial message and capture the serial for appending tokens + let publishResult = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + channel.publish("response", data: "") { result, error in + if let error { + continuation.resume(throwing: error) + } else { + continuation.resume(returning: result!) + } + } + } + + guard let msgSerial = publishResult.serials.first?.value else { + print("No serial returned") + return + } + + // Example: stream returns events like { type: 'token', text: 'Hello' } + for await event in stream { + // Append each token as it arrives + if event.type == "token" { + let messageToAppend = ARTMessage() + messageToAppend.serial = msgSerial + messageToAppend.data = event.text + + channel.append(messageToAppend, operation: nil, params: nil) + } + } +} +``` + +--- + +## 4. Use the test harness to verify the translated code + +1. Inside the test harness directory, run `swift build`. +2. If the compilation succeeds, the translated code can be considered correct; proceed to step 5. +3. If the compilation fails, analyse the compilation failures. There are two possible causes: + - **Missing context in the test harness**: The original JavaScript code assumed something exists (a type, a function, a variable) that the test harness doesn't provide. In this case, add the missing context to the test harness and try again. Do NOT modify the translated code to work around missing context. + - **Mistranslation**: The translated Swift code is incorrect (wrong method names, wrong syntax, incorrect API usage). In this case, fix the translation and try again. +4. If you cannot determine the cause of the compilation failure or do not know how to fix it, report the issue. Provide: + - the original JavaScript code + - the location of the original JavaScript code + - the translated code and the test harness code into which it was inserted (make it clear which is which) + - the compilation failure and any ideas you have about what's going on + +**Important**: The code that ends up in the documentation must be exactly the code inside the `example()` function body that was verified to compile. Do not insert different code into the documentation than what was tested. + +--- + +## 5. Insert the translated code into the documentation + +Insert the verified Swift code into the documentation file, within the same `` block as the JavaScript example. Include a test harness comment immediately before the Swift code block, showing the compilable wrapper: + +```mdx + +```javascript +// original JavaScript code +``` + +{/* +Swift test harness (id: Kx9mQ3): +func example(channel: ARTRealtimeChannel, stream: any AsyncSequence<(type: String, text: String), Never>) async throws { + // insert example code here +} +*/} +```swift +// translated Swift code goes here +``` + +``` + +When **stub types are needed** (the translated code references a type by name), include them in the harness comment: + +```mdx + +```javascript +// original JavaScript code +``` + +{/* +Swift test harness (id: tR4wBn): +struct ResponseData { + var timestamp: Date + var content: String +} + +func example(channel: ARTRealtimeChannel) { + // insert example code here +} +*/} +```swift +// The type name `ResponseData` appears in the translated code +let responses: [String: ResponseData] = [:] +// ... +``` + +``` + +The test harness comment must include **everything needed to compile the example**: +- **Stub types**: Any actors, structs, classes, or type aliases that the example code references by name +- **Function signature**: The function that wraps the example code, with all parameters + +**Critical**: The example code in the MDX must be **byte-for-byte identical** to the code inside the `example()` function body that was tested. If you need to add a wrapper, actor, or any other code to make compilation work, that code must either: +1. Go in the harness comment (if it's context the example assumes exists), OR +2. Be part of the example code itself (if it's something the reader should see) + +Never test code with workarounds (like `@unchecked Sendable`) that aren't included in either the harness comment or the example code. + +**Exception — `import` statements**: When the original JavaScript example includes an `import` statement (e.g. `import * as Ably from "ably"`), the displayed Swift code block should include `import Ably` at the top. Since Swift imports are file-level and cannot appear inside a function body, this line is exempt from the byte-for-byte rule — the displayed code includes the import, but the compiled function body does not. The test harness already has `import Ably` at file scope. + +This enables: +- **Reviewers** to verify the translation compiles correctly +- **Future editors** to modify the Swift code and test compilation without having to reverse-engineer what context was originally used + +--- + +## Report back + +Report what you translated and any issues encountered. diff --git a/.claude/skills/translate-examples-to-swift/prompts/verification-subagent.md b/.claude/skills/translate-examples-to-swift/prompts/verification-subagent.md new file mode 100644 index 0000000000..3888783c1e --- /dev/null +++ b/.claude/skills/translate-examples-to-swift/prompts/verification-subagent.md @@ -0,0 +1,183 @@ +You are a verification agent for Swift translations. Your job is to independently verify that Swift example code in documentation is correct and faithful to the original JavaScript. + +**Important**: You must verify what's actually in the MDX file. Do NOT read any translation JSON files - that would defeat the purpose of independent verification. + +## File to verify + +{FILEPATH} + +## Output + +Write your verification results to: + swift-translations/verifications/{FILENAME}.json + +The JSON must conform to the schema at `.claude/skills/translate-examples-to-swift/schemas/verification.schema.json`. Read that schema file to understand the required structure. + +--- + +## Your tasks + +For each Swift code block that has an accompanying test harness comment: + +### 1. Extract the code from the MDX + +For each `` block containing both JavaScript and Swift: +- **Read the ID** from the harness comment. The format is `Swift test harness (id: Kx9mQ3):` — extract the 6-character alphanumeric ID. Do NOT invent or assign your own IDs. +- Extract the JavaScript code (this goes in `original.code`) +- Extract the Swift code (this goes in `translation.code`) +- Extract the function signature from the harness comment (this goes in `harness.functionSignature`) +- Build the full compilable context (this goes in `harness.fullContext`) + +The harness comment format is: + +``` +{/* +Swift test harness (id: Kx9mQ3): +func example(...) { + // insert example code here +} +*/} +``` + +It may also include stub type declarations before the function. + +### 2. Create a fresh Swift package + +Create a new Swift package in swift-translations/verify-{FILENAME}/ (do NOT reuse any package created by the translation agent): + +```bash +mkdir -p swift-translations/verify-{FILENAME} && cd swift-translations/verify-{FILENAME} +swift package init --type executable +``` + +Update `Package.swift`: + +```swift +// swift-tools-version: 6.2 +import PackageDescription + +let package = Package( + name: "SwiftVerification", + platforms: [ + .macOS(.v26) + ], + dependencies: [ + .package(url: "https://github.com/ably/ably-cocoa", from: "1.2.0") + ], + targets: [ + .executableTarget( + name: "SwiftVerification", + dependencies: [ + .product(name: "Ably", package: "ably-cocoa") + ] + ), + ] +) +``` + +### 3. Assemble the harness strictly from the MDX harness comment + +Populate `Sources/SwiftVerification/main.swift` using **only** what the MDX harness comment provides. Do NOT add any types, functions, variables, or other declarations that are not present in the harness comment. + +The harness file must be assembled mechanically as follows: + +1. Start with `import Ably` +2. For each translated example, wrap its harness comment contents in a scoping function to isolate stub types: + - Create an outer function `func scope_{id}()` using the ID from the harness comment + - Inside the outer function, copy the harness comment contents verbatim: any stub type declarations, then the `func example(...)` with the example code inserted into its body + - This prevents type name conflicts between examples (e.g., two examples both defining `struct ToolCall` with different fields) +3. End with the `@main` struct + +```swift +import Ably + +// MARK: - Example Kx9mQ3 +func scope_Kx9mQ3() { + // (stub types from harness comment, if any, go here) + + func example(channel: ARTRealtimeChannel) { + // Example code from MDX inserted here + } +} + +// MARK: - Example tR4wBn +func scope_tR4wBn() { + func example(channel: ARTRealtimeChannel, stream: any AsyncSequence & Sendable) async { + // Example code from MDX inserted here + } +} + +// ... include ALL translated examples ... + +@main +struct SwiftVerification { + static func main() { + print("Verification harness") + } +} +``` + +**Critical rule**: You must NOT invent, infer, or supplement any context beyond what the harness comment provides. If the example code references a type like `ToolCall` or a function like `displayApprovalUI` that does not appear in the harness comment, do NOT create a stub for it yourself. Instead, let compilation fail — this is a legitimate failure indicating the harness comment is incomplete. + +This ensures: +1. All translated examples are verified in a single compilation +2. The harness comment's completeness is itself verified — if it's missing context, that's a bug +3. IDs match between the MDX, the JSON output, and the harness function names + +### 4. Verify compilation + +- Run `swift build` +- Record the result: + - `"pass"` if it compiles + - `"fail"` with `errorMessage` if it does not. In the error message, distinguish between: + - **Incomplete harness comment**: The code references types or functions that aren't provided by the harness comment (e.g., "use of undeclared type 'ToolCall'" when ToolCall is not in the comment). Note this explicitly — it means the translation agent failed to include necessary context in the harness comment. + - **Mistranslation**: Other compilation errors (wrong method names, type mismatches, syntax errors, etc.) + +### 5. Check faithfulness to original JavaScript + +Compare the Swift translation to the original JavaScript code block in the same section: + +- Does it preserve the same logical flow? +- Does it handle the same cases? +- Are comments preserved and accurate? +- Are there any material additions or omissions? + +Rate faithfulness as: faithful, minor_differences (list them), or significant_deviation (explain) + +### 6. Check convention compliance + +Read the "Guidance" and "Conventions" sections of `.claude/skills/translate-examples-to-swift/prompts/translation-subagent.md`. Check each Swift example for violations. In particular, look for: + +- `nonisolated(unsafe)` (forbidden by C9) +- Force-unwraps beyond the `(result, error)` callback convention (i.e. force-unwrapping a result that isn't being used) +- Logic inside continuation callbacks beyond resuming the continuation (values should be extracted after the `await`, not inside the callback) +- Fire-and-forget SDK calls using bare callbacks instead of `Task { }` with a continuation inside +- `@MainActor` on functions where no subscribe callback mutates local state +- Missing `onAttach:` / `attachCallback:` on subscribe calls where the JS `await`s the subscribe + +Record any violations in the faithfulness notes. Rate as minor_differences if there are convention violations even if the code is otherwise faithful to the JS. + +### 7. Write verification JSON + +Write the results to swift-translations/verifications/{FILENAME}.json conforming to the schema, then validate with `npx ajv-cli validate` — do not use python or other tools for JSON validation. Each example in the `examples` array must include: + +- `id`: The ID read from the harness comment (e.g., "Kx9mQ3", "tR4wBn") +- `lineNumber`: Line number of the JavaScript code block in the MDX (for human reference) +- `original`: `{ "language": "javascript", "code": "..." }` - the extracted JavaScript code +- `translation`: `{ "language": "swift", "code": "..." }` - the extracted Swift code +- `harness`: `{ "functionSignature": "...", "stubTypes": null, "fullContext": "..." }` - the harness details +- `compilation`: `{ "status": "pass" }` or `{ "status": "fail", "errorMessage": "..." }` +- `faithfulness`: `{ "rating": "faithful" }` or `{ "rating": "minor_differences", "notes": "..." }` + +### 8. Report findings + +Provide a summary of what you verified and any issues found. + +--- + +## Important + +- Do NOT modify any documentation files — you are only verifying +- Do NOT invent stub types, functions, or any other declarations that are not in the MDX harness comment. The harness comment must be self-contained. If it isn't, that is a compilation failure with cause "incomplete harness comment" — not something for you to fix. +- Be objective and thorough +- You MUST include the actual extracted code in your JSON output diff --git a/.claude/skills/translate-examples-to-swift/review-app/example-data.json b/.claude/skills/translate-examples-to-swift/review-app/example-data.json new file mode 100644 index 0000000000..e25ca6a719 --- /dev/null +++ b/.claude/skills/translate-examples-to-swift/review-app/example-data.json @@ -0,0 +1,118 @@ +{ + "version": "1.0", + "generatedAt": "2026-01-30T10:30:00Z", + "summary": { + "filesProcessed": 2, + "examplesTranslated": 3, + "compilationPassed": 2, + "compilationFailed": 1 + }, + "files": [ + { + "path": "src/pages/docs/ai-transport/streaming.mdx", + "examples": [ + { + "id": "streaming-1", + "lineNumber": 45, + "original": { + "language": "javascript", + "code": "// Publish initial message and capture the serial for appending tokens\nconst { serials: [msgSerial] } = await channel.publish({ name: 'response', data: '' });\n\n// Example: stream returns events like { type: 'token', text: 'Hello' }\nfor await (const event of stream) {\n // Append each token as it arrives\n if (event.type === 'token') {\n channel.appendMessage({ serial: msgSerial, data: event.text });\n }\n}" + }, + "translation": { + "language": "swift", + "code": "// Publish initial message and capture the serial for appending tokens\nchannel.publish(\"response\", data: \"\") { publishResult, error in\n if let error {\n print(\"Error publishing message: \\(error)\")\n return\n }\n\n let msgSerial = publishResult!.serials[0].value!\n\n Task {\n // Example: stream returns events like { type: 'token', text: 'Hello' }\n for await event in stream {\n // Append each token as it arrives\n if (event.type == \"token\") {\n let messageToAppend = ARTMessage()\n messageToAppend.serial = msgSerial\n messageToAppend.data = event.text\n\n channel.append(messageToAppend, operation: nil, params: nil) { _, error in\n if let error {\n print(\"Error appending to message: \\(error)\")\n }\n }\n }\n }\n }\n}" + }, + "harness": { + "functionSignature": "func example(channel: ARTRealtimeChannel, stream: any AsyncSequence<(type: String, text: String), Never> & Sendable) async throws", + "stubTypes": null, + "fullContext": "import Ably\n\nfunc example(channel: ARTRealtimeChannel, stream: any AsyncSequence<(type: String, text: String), Never> & Sendable) async throws {\n // example code inserted here\n}" + }, + "translationNotes": [ + { + "type": "decision", + "message": "Used callback pattern instead of async/await as ably-cocoa doesn't have async publish API" + }, + { + "type": "deviation", + "message": "Added explicit error handling in callback (not present in JS original)" + } + ], + "verification": { + "compilation": { + "status": "pass" + }, + "faithfulness": { + "rating": "minor_differences", + "notes": "Error handling added in Swift version; logical flow preserved" + } + } + }, + { + "id": "streaming-2", + "lineNumber": 112, + "original": { + "language": "javascript", + "code": "const client = new Ably.Realtime({ key: 'your-api-key' });\nconst channel = client.channels.get('my-channel');" + }, + "translation": { + "language": "swift", + "code": "let client = ARTRealtime(key: \"your-api-key\")\nlet channel = client.channels.get(\"my-channel\")" + }, + "harness": { + "functionSignature": "func example()", + "stubTypes": null, + "fullContext": "import Ably\n\nfunc example() {\n // example code inserted here\n}" + }, + "translationNotes": [], + "verification": { + "compilation": { + "status": "pass" + }, + "faithfulness": { + "rating": "faithful", + "notes": null + } + } + } + ] + }, + { + "path": "src/pages/docs/messages/updates-deletes.mdx", + "examples": [ + { + "id": "updates-deletes-1", + "lineNumber": 78, + "original": { + "language": "javascript", + "code": "// Subscribe to message updates\nchannel.subscribe('update', (message) => {\n console.log('Message updated:', message.data);\n console.log('Original serial:', message.updateSerial);\n});" + }, + "translation": { + "language": "swift", + "code": "// Subscribe to message updates\nchannel.subscribe(\"update\") { message in\n print(\"Message updated: \\(message.data)\")\n print(\"Original serial: \\(message.updateSerial)\")\n}" + }, + "harness": { + "functionSignature": "func example(channel: ARTRealtimeChannel)", + "stubTypes": null, + "fullContext": "import Ably\n\nfunc example(channel: ARTRealtimeChannel) {\n // example code inserted here\n}" + }, + "translationNotes": [ + { + "type": "warning", + "message": "updateSerial property may not exist on ARTMessage - needs verification against latest SDK" + } + ], + "verification": { + "compilation": { + "status": "fail", + "errorMessage": "error: value of type 'ARTMessage' has no member 'updateSerial'\n print(\"Original serial: \\(message.updateSerial)\")\n ^~~~~~~~~~~~" + }, + "faithfulness": { + "rating": "not_assessed", + "notes": "Could not assess faithfulness due to compilation failure" + } + } + } + ] + } + ] +} diff --git a/.claude/skills/translate-examples-to-swift/review-app/generate-review.sh b/.claude/skills/translate-examples-to-swift/review-app/generate-review.sh new file mode 100755 index 0000000000..087000228e --- /dev/null +++ b/.claude/skills/translate-examples-to-swift/review-app/generate-review.sh @@ -0,0 +1,118 @@ +#!/bin/bash +# +# generate-review.sh - Generates a human-reviewable HTML file from translation data +# +# This script combines the review-app.html template with a JSON data file containing +# translation results to produce a standalone HTML file that can be opened in a browser +# for human review of Swift translations. +# +# USAGE +# ./generate-review.sh [DATA_FILE] [OUTPUT_FILE] +# +# ARGUMENTS +# DATA_FILE Path to JSON file containing translation data (default: example-data.json) +# OUTPUT_FILE Path for generated HTML file (default: stdout) +# +# EXAMPLES +# # Generate review file using example data, output to stdout +# ./generate-review.sh +# +# # Generate review file from specific data, output to stdout +# ./generate-review.sh /path/to/translation-data.json +# +# # Generate review file and save to a specific location +# ./generate-review.sh /path/to/translation-data.json /tmp/review.html +# +# # Generate and immediately open in browser +# ./generate-review.sh data.json /tmp/review.html && open /tmp/review.html +# +# JSON DATA FORMAT +# The data file must conform to the translation data schema. See example-data.json +# for the expected structure. Key fields: +# +# { +# "version": "1.0", +# "generatedAt": "2026-01-30T10:30:00Z", +# "summary": { +# "filesProcessed": 2, +# "examplesTranslated": 3, +# "compilationPassed": 2, +# "compilationFailed": 1 +# }, +# "files": [ +# { +# "path": "src/pages/docs/example.mdx", +# "examples": [ +# { +# "id": "unique-id", +# "lineNumber": 45, +# "original": { "language": "javascript", "code": "..." }, +# "translation": { "language": "swift", "code": "..." }, +# "harness": { "functionSignature": "...", "fullContext": "..." }, +# "translationNotes": [ { "type": "decision|deviation|info|warning", "message": "..." } ], +# "verification": { +# "compilation": { "status": "pass|fail", "errorMessage": "..." }, +# "faithfulness": { "rating": "faithful|minor_differences|significant_deviation", "notes": "..." } +# } +# } +# ] +# } +# ] +# } +# +# OUTPUT +# A standalone HTML file that displays: +# - Side-by-side JavaScript/Swift code comparison +# - Translation notes and decisions +# - Verification results (compilation status, faithfulness assessment) +# - Interactive review controls (approve/flag/skip with comments) +# - Export functionality for review summary +# +# FILES +# review-app.html - HTML template (source of truth) +# example-data.json - Sample data file demonstrating expected format +# +# EXIT CODES +# 0 Success +# 1 Error (template or data file not found) + +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +TEMPLATE="$SCRIPT_DIR/review-app.html" +DATA_FILE="${1:-$SCRIPT_DIR/example-data.json}" +OUTPUT="${2:-/dev/stdout}" + +if [[ ! -f "$TEMPLATE" ]]; then + echo "Error: Template not found at $TEMPLATE" >&2 + exit 1 +fi + +if [[ ! -f "$DATA_FILE" ]]; then + echo "Error: Data file not found at $DATA_FILE" >&2 + exit 1 +fi + +# Validate JSON against schema +SCHEMA="$SCRIPT_DIR/../schemas/consolidated.schema.json" +if ! npx ajv-cli validate -s "$SCHEMA" -d "$DATA_FILE" >/dev/null 2>&1; then + echo "Error: Data file does not conform to schema" >&2 + echo "Run: npx ajv-cli validate -s $SCHEMA -d $DATA_FILE" >&2 + exit 1 +fi + +# Use awk to handle multi-line JSON replacement +awk -v data_file="$DATA_FILE" ' +// { + print "" + next +} +{ print } +' "$TEMPLATE" > "$OUTPUT" + +if [[ "$OUTPUT" != "/dev/stdout" ]]; then + echo "Generated: $OUTPUT" >&2 +fi diff --git a/.claude/skills/translate-examples-to-swift/review-app/review-app.html b/.claude/skills/translate-examples-to-swift/review-app/review-app.html new file mode 100644 index 0000000000..86f9eb4557 --- /dev/null +++ b/.claude/skills/translate-examples-to-swift/review-app/review-app.html @@ -0,0 +1,1266 @@ + + + + + + Swift Translation Review + + + + + + +
+
Loading translation data...
+
+ + + + + + + + + + + + diff --git a/.claude/skills/translate-examples-to-swift/schemas/consolidated.schema.json b/.claude/skills/translate-examples-to-swift/schemas/consolidated.schema.json new file mode 100644 index 0000000000..41714f9e8d --- /dev/null +++ b/.claude/skills/translate-examples-to-swift/schemas/consolidated.schema.json @@ -0,0 +1,179 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "consolidated.schema.json", + "title": "Consolidated Translation Data", + "description": "Merged translation and verification data for the review app", + "type": "object", + "required": ["version", "generatedAt", "summary", "files"], + "examples": [ + { + "version": "1.0", + "generatedAt": "2026-01-30T12:00:00Z", + "summary": { + "filesProcessed": 1, + "examplesTranslated": 1, + "compilationPassed": 1, + "compilationFailed": 0 + }, + "files": [ + { + "path": "src/pages/docs/example.mdx", + "examples": [ + { + "id": "Kx9mQ3", + "lineNumber": 45, + "original": { "language": "javascript", "code": "await channel.publish('greeting', 'hello');" }, + "translation": { "language": "swift", "code": "channel.publish(\"greeting\", data: \"hello\")" }, + "harness": { + "functionSignature": "func example(channel: ARTRealtimeChannel)", + "stubTypes": null, + "fullContext": "import Ably\n\nfunc example(channel: ARTRealtimeChannel) {\n // ...\n}" + }, + "translationNotes": [ + { "type": "decision", "message": "Used callback pattern" } + ], + "verification": { + "compilation": { "status": "pass" }, + "faithfulness": { "rating": "faithful", "notes": null } + } + } + ] + } + ] + } + ], + "properties": { + "version": { + "type": "string", + "const": "1.0", + "description": "Schema version" + }, + "generatedAt": { + "type": "string", + "description": "ISO 8601 timestamp when this file was generated" + }, + "summary": { + "type": "object", + "required": ["filesProcessed", "examplesTranslated", "compilationPassed", "compilationFailed"], + "properties": { + "filesProcessed": { + "type": "integer", + "minimum": 0, + "description": "Number of MDX files processed" + }, + "examplesTranslated": { + "type": "integer", + "minimum": 0, + "description": "Total number of examples translated" + }, + "compilationPassed": { + "type": "integer", + "minimum": 0, + "description": "Number of examples that compiled successfully" + }, + "compilationFailed": { + "type": "integer", + "minimum": 0, + "description": "Number of examples that failed to compile" + } + } + }, + "files": { + "type": "array", + "description": "Translation data grouped by file", + "items": { + "type": "object", + "required": ["path", "examples"], + "properties": { + "path": { + "type": "string", + "description": "Path to the MDX file" + }, + "examples": { + "type": "array", + "items": { + "type": "object", + "required": ["id", "lineNumber", "original", "translation", "harness", "translationNotes", "verification"], + "properties": { + "id": { + "type": "string" + }, + "lineNumber": { + "type": "integer", + "minimum": 1 + }, + "original": { + "type": "object", + "required": ["language", "code"], + "properties": { + "language": { "type": "string" }, + "code": { "type": "string" } + } + }, + "translation": { + "type": "object", + "required": ["language", "code"], + "properties": { + "language": { "type": "string" }, + "code": { "type": "string" } + } + }, + "harness": { + "type": "object", + "required": ["functionSignature", "fullContext"], + "properties": { + "functionSignature": { "type": "string" }, + "stubTypes": { "type": ["string", "null"] }, + "fullContext": { "type": "string" } + } + }, + "translationNotes": { + "type": "array", + "items": { + "type": "object", + "required": ["type", "message"], + "properties": { + "type": { + "type": "string", + "enum": ["decision", "deviation", "info", "warning"] + }, + "message": { "type": "string" } + } + } + }, + "verification": { + "type": "object", + "required": ["compilation", "faithfulness"], + "properties": { + "compilation": { + "type": "object", + "required": ["status"], + "properties": { + "status": { + "type": "string", + "enum": ["pass", "fail"] + }, + "errorMessage": { "type": "string" } + } + }, + "faithfulness": { + "type": "object", + "required": ["rating"], + "properties": { + "rating": { + "type": "string", + "enum": ["faithful", "minor_differences", "significant_deviation", "not_assessed"] + }, + "notes": { "type": ["string", "null"] } + } + } + } + } + } + } + } + } + } + } + } +} diff --git a/.claude/skills/translate-examples-to-swift/schemas/translation.schema.json b/.claude/skills/translate-examples-to-swift/schemas/translation.schema.json new file mode 100644 index 0000000000..91eb94e453 --- /dev/null +++ b/.claude/skills/translate-examples-to-swift/schemas/translation.schema.json @@ -0,0 +1,77 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "translation.schema.json", + "title": "Translation Output", + "description": "Output from a translation sub-agent for a single MDX file", + "type": "object", + "required": ["file", "translatedAt", "examples"], + "examples": [ + { + "file": "src/pages/docs/example.mdx", + "translatedAt": "2026-01-30T10:30:00Z", + "examples": [ + { + "id": "Kx9mQ3", + "lineNumber": 45, + "notes": [ + { "type": "decision", "message": "Used callback pattern instead of async/await" }, + { "type": "deviation", "message": "Added explicit error handling" } + ] + } + ] + } + ], + "properties": { + "file": { + "type": "string", + "description": "Path to the MDX file that was translated", + "examples": ["src/pages/docs/messages/streaming.mdx"] + }, + "translatedAt": { + "type": "string", + "description": "ISO 8601 timestamp when translation was completed", + "examples": ["2026-01-30T10:30:00Z"] + }, + "examples": { + "type": "array", + "description": "Metadata for each translated example", + "items": { + "type": "object", + "required": ["id", "lineNumber"], + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the example, a 6-character alphanumeric string read from the harness comment", + "examples": ["Kx9mQ3", "tR4wBn"] + }, + "lineNumber": { + "type": "integer", + "minimum": 1, + "description": "Line number of the original JavaScript code block in the MDX file", + "examples": [45, 105] + }, + "notes": { + "type": "array", + "description": "Notes about translation decisions made", + "items": { + "type": "object", + "required": ["type", "message"], + "properties": { + "type": { + "type": "string", + "enum": ["decision", "deviation", "info", "warning"], + "description": "Type of note: decision (intentional choice), deviation (necessary difference from original), info (helpful context), warning (potential issue)" + }, + "message": { + "type": "string", + "description": "Description of the decision or note", + "examples": ["Used callback pattern instead of async/await", "Added explicit error handling"] + } + } + } + } + } + } + } + } +} diff --git a/.claude/skills/translate-examples-to-swift/schemas/verification.schema.json b/.claude/skills/translate-examples-to-swift/schemas/verification.schema.json new file mode 100644 index 0000000000..8a6a2e2769 --- /dev/null +++ b/.claude/skills/translate-examples-to-swift/schemas/verification.schema.json @@ -0,0 +1,138 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "verification.schema.json", + "title": "Verification Output", + "description": "Output from a verification sub-agent for a single MDX file", + "type": "object", + "required": ["file", "verifiedAt", "examples"], + "examples": [ + { + "file": "src/pages/docs/example.mdx", + "verifiedAt": "2026-01-30T11:00:00Z", + "examples": [ + { + "id": "Kx9mQ3", + "lineNumber": 45, + "original": { "language": "javascript", "code": "const result = await channel.publish('greeting', 'hello');" }, + "translation": { "language": "swift", "code": "channel.publish(\"greeting\", data: \"hello\") { error in\n // ...\n}" }, + "harness": { + "functionSignature": "func example(channel: ARTRealtimeChannel) async throws", + "stubTypes": null, + "fullContext": "import Ably\n\nfunc example(channel: ARTRealtimeChannel) async throws {\n // example code here\n}" + }, + "compilation": { "status": "pass" }, + "faithfulness": { "rating": "faithful", "notes": null } + } + ] + } + ], + "properties": { + "file": { + "type": "string", + "description": "Path to the MDX file that was verified", + "examples": ["src/pages/docs/messages/streaming.mdx"] + }, + "verifiedAt": { + "type": "string", + "description": "ISO 8601 timestamp when verification was completed", + "examples": ["2026-01-30T11:00:00Z"] + }, + "examples": { + "type": "array", + "description": "Verification results for each example", + "items": { + "type": "object", + "required": ["id", "lineNumber", "original", "translation", "harness", "compilation", "faithfulness"], + "properties": { + "id": { + "type": "string", + "description": "Unique identifier extracted from the harness comment in the MDX, a 6-character alphanumeric string", + "examples": ["Kx9mQ3", "tR4wBn"] + }, + "lineNumber": { + "type": "integer", + "minimum": 1, + "description": "Line number of the code block in the MDX file", + "examples": [45, 105] + }, + "original": { + "type": "object", + "required": ["language", "code"], + "properties": { + "language": { + "type": "string", + "const": "javascript" + }, + "code": { + "type": "string", + "description": "Original JavaScript code extracted from MDX" + } + } + }, + "translation": { + "type": "object", + "required": ["language", "code"], + "properties": { + "language": { + "type": "string", + "const": "swift" + }, + "code": { + "type": "string", + "description": "Swift translation extracted from MDX" + } + } + }, + "harness": { + "type": "object", + "required": ["functionSignature", "fullContext"], + "properties": { + "functionSignature": { + "type": "string", + "description": "Function signature extracted from MDX harness comment" + }, + "stubTypes": { + "type": ["string", "null"], + "description": "Stub type declarations if any" + }, + "fullContext": { + "type": "string", + "description": "Full compilable context including imports and function wrapper" + } + } + }, + "compilation": { + "type": "object", + "required": ["status"], + "properties": { + "status": { + "type": "string", + "enum": ["pass", "fail"], + "description": "Whether the code compiled successfully" + }, + "errorMessage": { + "type": "string", + "description": "Compiler error message if compilation failed" + } + } + }, + "faithfulness": { + "type": "object", + "required": ["rating"], + "properties": { + "rating": { + "type": "string", + "enum": ["faithful", "minor_differences", "significant_deviation", "not_assessed"], + "description": "Assessment of how faithful the translation is to the original" + }, + "notes": { + "type": ["string", "null"], + "description": "Explanation of differences if any" + } + } + } + } + } + } + } +} diff --git a/.claude/skills/translate-examples-to-swift/scripts/consolidate.sh b/.claude/skills/translate-examples-to-swift/scripts/consolidate.sh new file mode 100755 index 0000000000..08b48d4cf0 --- /dev/null +++ b/.claude/skills/translate-examples-to-swift/scripts/consolidate.sh @@ -0,0 +1,190 @@ +#!/bin/bash +# +# consolidate.sh - Merges translation and verification JSONs into consolidated.json +# +# This script reads all translation and verification JSON files from swift-translations/, +# merges them by file and example ID, validates against the schema, and generates +# the review HTML file. +# +# USAGE +# ./consolidate.sh [OUTPUT_DIR] +# +# ARGUMENTS +# OUTPUT_DIR Directory containing translations/ and verifications/ (default: swift-translations) +# +# OUTPUT +# {OUTPUT_DIR}/consolidated.json - Merged data +# {OUTPUT_DIR}/review.html - Human review interface +# +# EXIT CODES +# 0 Success +# 1 Error (missing files, validation failure, etc.) + +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +SKILL_DIR="$(dirname "$SCRIPT_DIR")" +OUTPUT_DIR="${1:-swift-translations}" + +TRANSLATIONS_DIR="$OUTPUT_DIR/translations" +VERIFICATIONS_DIR="$OUTPUT_DIR/verifications" +CONSOLIDATED="$OUTPUT_DIR/consolidated.json" +REVIEW_HTML="$OUTPUT_DIR/review.html" + +SCHEMA="$SKILL_DIR/schemas/consolidated.schema.json" + +# Check directories exist +if [[ ! -d "$TRANSLATIONS_DIR" ]]; then + echo "Error: Translations directory not found: $TRANSLATIONS_DIR" >&2 + exit 1 +fi + +if [[ ! -d "$VERIFICATIONS_DIR" ]]; then + echo "Error: Verifications directory not found: $VERIFICATIONS_DIR" >&2 + exit 1 +fi + +# Count files +TRANSLATION_COUNT=$(find "$TRANSLATIONS_DIR" -name '*.json' 2>/dev/null | wc -l | tr -d ' ') +VERIFICATION_COUNT=$(find "$VERIFICATIONS_DIR" -name '*.json' 2>/dev/null | wc -l | tr -d ' ') + +if [[ "$TRANSLATION_COUNT" -eq 0 ]]; then + echo "Error: No translation JSON files found in $TRANSLATIONS_DIR" >&2 + exit 1 +fi + +if [[ "$VERIFICATION_COUNT" -eq 0 ]]; then + echo "Error: No verification JSON files found in $VERIFICATIONS_DIR" >&2 + exit 1 +fi + +echo "Found $TRANSLATION_COUNT translation(s) and $VERIFICATION_COUNT verification(s)" >&2 + +# Use node to merge the JSONs (more reliable than jq for complex merges) +node -e ' +const fs = require("fs"); +const path = require("path"); + +const translationsDir = process.argv[1]; +const verificationsDir = process.argv[2]; +const outputFile = process.argv[3]; + +// Read all JSON files from a directory +function readJsonFiles(dir) { + const files = fs.readdirSync(dir).filter(f => f.endsWith(".json")); + return files.map(f => ({ + filename: f.replace(".json", ""), + data: JSON.parse(fs.readFileSync(path.join(dir, f), "utf8")) + })); +} + +const translations = readJsonFiles(translationsDir); +const verifications = readJsonFiles(verificationsDir); + +// Build lookup map of verifications by file, then by example ID +const verificationsByFile = new Map(); +verifications.forEach(v => { + const byId = new Map(); + v.data.examples.forEach(ex => { + byId.set(ex.id, ex); + }); + verificationsByFile.set(v.data.file, byId); +}); + +// Merge by iterating over translations (ensures every translation is accounted for) +let validationErrors = []; +let totalExamples = 0; +let compilationPassed = 0; +let compilationFailed = 0; + +const files = translations.map(t => { + const verificationMap = verificationsByFile.get(t.data.file); + if (!verificationMap) { + validationErrors.push(`Translation for ${t.data.file} has no matching verification file`); + return null; + } + + const examples = t.data.examples.map(translationEx => { + const verificationEx = verificationMap.get(translationEx.id); + if (!verificationEx) { + validationErrors.push(`Translation ID "${translationEx.id}" in ${t.data.file} was not verified`); + return null; + } + + totalExamples++; + if (verificationEx.compilation.status === "pass") { + compilationPassed++; + } else { + compilationFailed++; + } + + return { + id: translationEx.id, + lineNumber: verificationEx.lineNumber, + original: verificationEx.original, + translation: verificationEx.translation, + harness: verificationEx.harness, + translationNotes: translationEx.notes || [], + verification: { + compilation: verificationEx.compilation, + faithfulness: verificationEx.faithfulness + } + }; + }).filter(Boolean); + + // Check for extra verifications not in translation + verificationMap.forEach((_, id) => { + const inTranslation = t.data.examples.some(ex => ex.id === id); + if (!inTranslation) { + validationErrors.push(`Verification ID "${id}" in ${t.data.file} has no matching translation`); + } + }); + + // Sort by line number so review order matches document order + examples.sort((a, b) => a.lineNumber - b.lineNumber); + + return { + path: t.data.file, + examples + }; +}).filter(Boolean); + +if (validationErrors.length > 0) { + console.error("Validation errors:"); + validationErrors.forEach(e => console.error(" - " + e)); + process.exit(1); +} + +const consolidated = { + version: "1.0", + generatedAt: new Date().toISOString(), + summary: { + filesProcessed: files.length, + examplesTranslated: totalExamples, + compilationPassed, + compilationFailed + }, + files +}; + +fs.writeFileSync(outputFile, JSON.stringify(consolidated, null, 2)); +console.error(`Wrote ${outputFile}`); +console.error(` Files: ${files.length}`); +console.error(` Examples: ${totalExamples} (${compilationPassed} passed, ${compilationFailed} failed)`); +' "$TRANSLATIONS_DIR" "$VERIFICATIONS_DIR" "$CONSOLIDATED" + +# Validate against schema +echo "Validating against schema..." >&2 +if ! npx ajv-cli validate -s "$SCHEMA" -d "$CONSOLIDATED" >/dev/null 2>&1; then + echo "Error: Consolidated JSON does not conform to schema" >&2 + echo "Run: npx ajv-cli validate -s $SCHEMA -d $CONSOLIDATED" >&2 + exit 1 +fi +echo "Schema validation passed" >&2 + +# Generate review HTML +echo "Generating review HTML..." >&2 +"$SKILL_DIR/review-app/generate-review.sh" "$CONSOLIDATED" "$REVIEW_HTML" + +echo "" >&2 +echo "Done! Open $REVIEW_HTML to review translations." >&2 diff --git a/.claude/skills/translate-examples-to-swift/scripts/generate-translation-stubs.sh b/.claude/skills/translate-examples-to-swift/scripts/generate-translation-stubs.sh new file mode 100755 index 0000000000..833a132381 --- /dev/null +++ b/.claude/skills/translate-examples-to-swift/scripts/generate-translation-stubs.sh @@ -0,0 +1,123 @@ +#!/bin/bash +# +# generate-translation-stubs.sh - Generate stub translation JSONs from verification data +# +# For verify-only mode: when verification JSONs exist but translation JSONs don't +# (e.g. re-verification of existing translations, manual edits, previous sessions). +# +# This reads each file in swift-translations/verifications/ and generates a +# corresponding file in swift-translations/translations/ with stub metadata. +# Existing translation files are skipped (safe to run when some translations exist). +# +# USAGE +# ./generate-translation-stubs.sh [OUTPUT_DIR] +# +# ARGUMENTS +# OUTPUT_DIR Directory containing verifications/ (default: swift-translations) +# +# OUTPUT +# {OUTPUT_DIR}/translations/{filename}.json - One stub per verification file +# +# EXIT CODES +# 0 Success +# 1 Error (missing files, validation failure, etc.) + +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +SKILL_DIR="$(dirname "$SCRIPT_DIR")" +OUTPUT_DIR="${1:-swift-translations}" + +VERIFICATIONS_DIR="$OUTPUT_DIR/verifications" +TRANSLATIONS_DIR="$OUTPUT_DIR/translations" + +TRANSLATION_SCHEMA="$SKILL_DIR/schemas/translation.schema.json" + +# Check verifications directory exists +if [[ ! -d "$VERIFICATIONS_DIR" ]]; then + echo "Error: Verifications directory not found: $VERIFICATIONS_DIR" >&2 + exit 1 +fi + +VERIFICATION_COUNT=$(find "$VERIFICATIONS_DIR" -name '*.json' 2>/dev/null | wc -l | tr -d ' ') +if [[ "$VERIFICATION_COUNT" -eq 0 ]]; then + echo "Error: No verification JSON files found in $VERIFICATIONS_DIR" >&2 + exit 1 +fi + +# Ensure translations directory exists +mkdir -p "$TRANSLATIONS_DIR" + +echo "Generating translation stubs from $VERIFICATION_COUNT verification file(s)..." >&2 + +# Use node to read verification JSONs and generate translation stubs +node -e ' +const fs = require("fs"); +const path = require("path"); + +const verificationsDir = process.argv[1]; +const translationsDir = process.argv[2]; + +const verificationFiles = fs.readdirSync(verificationsDir).filter(f => f.endsWith(".json")); + +let generated = 0; +let skipped = 0; + +for (const file of verificationFiles) { + const translationPath = path.join(translationsDir, file); + + // Skip if translation already exists + if (fs.existsSync(translationPath)) { + console.error(` Skip (exists): ${file}`); + skipped++; + continue; + } + + const verification = JSON.parse(fs.readFileSync(path.join(verificationsDir, file), "utf8")); + + const stub = { + file: verification.file, + translatedAt: new Date().toISOString(), + examples: verification.examples.map(ex => ({ + id: ex.id, + lineNumber: ex.lineNumber, + notes: [ + { + type: "info", + message: "Verify-only mode — no translation was performed. This stub was generated from verification data." + } + ] + })) + }; + + fs.writeFileSync(translationPath, JSON.stringify(stub, null, 2) + "\n"); + console.error(` Generated: ${file}`); + generated++; +} + +console.error(""); +console.error(`Done: ${generated} generated, ${skipped} skipped (already existed)`); + +if (generated === 0 && skipped > 0) { + console.error("All verification files already have matching translations."); +} +' "$VERIFICATIONS_DIR" "$TRANSLATIONS_DIR" + +# Validate generated stubs against schema +echo "" >&2 +echo "Validating generated stubs against translation schema..." >&2 + +VALIDATION_FAILED=0 +for f in "$TRANSLATIONS_DIR"/*.json; do + if ! npx ajv-cli validate -s "$TRANSLATION_SCHEMA" -d "$f" >/dev/null 2>&1; then + echo " FAIL: $(basename "$f")" >&2 + VALIDATION_FAILED=1 + fi +done + +if [[ "$VALIDATION_FAILED" -eq 1 ]]; then + echo "Error: Some generated stubs failed schema validation" >&2 + exit 1 +fi + +echo "All translation files pass schema validation." >&2 diff --git a/.gitignore b/.gitignore index 3d5158aed7..7bea006c96 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,5 @@ config/nginx-redirects.conf src/gatsby-types.d.ts .idea/* **/*.swp -.claude/* -!.claude/commands/ +.claude/*.local.* /.junio/ diff --git a/src/pages/docs/ai-transport/guides/anthropic/anthropic-message-per-response.mdx b/src/pages/docs/ai-transport/guides/anthropic/anthropic-message-per-response.mdx index 3f9bc5fd22..6461ef167c 100644 --- a/src/pages/docs/ai-transport/guides/anthropic/anthropic-message-per-response.mdx +++ b/src/pages/docs/ai-transport/guides/anthropic/anthropic-message-per-response.mdx @@ -634,13 +634,12 @@ await channel.subscribe((message) => { console.log('Subscriber ready, waiting for tokens...'); ``` -{/* Swift example test harness -ID: anthropic-message-per-response-1 -To verify: copy this comment into a Swift file, paste the example code into the function body, run `swift build` - +{/* +Swift test harness: @MainActor -func example_anthropic_message_per_response_1() async throws { - // --- example code starts here --- +func example() async throws { + // insert example code here +} */} ```client_swift import Ably @@ -695,7 +694,6 @@ try await withCheckedThrowingContinuation { (continuation: CheckedContinuation { }); ``` -{/* Swift example test harness -ID: anthropic-message-per-response-2 -To verify: copy this comment into a Swift file, paste the example code into the function body, run `swift build` - +{/* +Swift test harness: @MainActor -func example_anthropic_message_per_response_2(realtime: ARTRealtime) async throws { - // --- example code starts here --- +func example(realtime: ARTRealtime) async throws { + // insert example code here +} */} ```client_swift // Use rewind to receive recent historical messages @@ -991,7 +988,6 @@ try await withCheckedThrowingContinuation { (continuation: CheckedContinuation { console.log('Subscriber ready, waiting for tokens...'); ``` -{/* Swift example test harness -ID: anthropic-message-per-token-1 -To verify: copy this comment into a Swift file, paste the example code into the function body, run `swift build` - +{/* +Swift test harness: @MainActor -func example_anthropic_message_per_token_1() async throws { - // --- example code starts here --- +func example() async throws { + // insert example code here +} */} ```client_swift import Ably @@ -680,7 +679,6 @@ try await withCheckedThrowingContinuation { (continuation: CheckedContinuation Response { + // Assign a mock user ID for demonstration + request.storage[UserIDKey.self] = "user123" + return try await next.respond(to: request) + } +} + +// Return the claims payload to embed in the signed JWT. +func getJWTClaims(userID: String) -> AblyJWTPayload { + // Returns a payload with only the standard exp and iat claims, so the + // token inherits the capabilities of the signing key. + let now = Date() + return AblyJWTPayload( + expiration: .init(value: now.addingTimeInterval(3600)), + issuedAt: .init(value: now) + ) +} + +let app = try await Application.make() + +let keyParts = "{{API_KEY}}".split(separator: ":") +let keyName = String(keyParts[0]) +let keySecret = String(keyParts[1]) + +// Register the HMAC-SHA256 signing key. +await app.jwt.keys.add( + hmac: HMACKey(from: keySecret), + digestAlgorithm: .sha256, + kid: JWKIdentifier(string: keyName) +) + +// Define an auth endpoint used by the client to obtain a signed JWT +// which it can use to authenticate with the Ably service. +app.grouped(AuthenticateUser()).get("api", "auth", "token") { req async throws -> Response in + let userID = req.storage[UserIDKey.self]! + + // Sign a JWT using the secret part of the Ably API key. + let token = try await req.jwt.sign( + getJWTClaims(userID: userID), + kid: JWKIdentifier(string: keyName) + ) + + let response = Response(status: .ok, body: .init(string: token)) + response.headers.contentType = .init(type: "application", subType: "jwt") + return response +} + +app.http.server.configuration.port = 3001 + +try await app.execute() +```