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..076b3b5296 --- /dev/null +++ b/.claude/skills/translate-examples-to-swift/prompts/translation-subagent.md @@ -0,0 +1,494 @@ +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 + +For each JavaScript code block in the documentation, 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 example using the format `{filename}-{sequential}`, numbering ALL JavaScript examples in the file sequentially: +- `streaming-1` for the first JavaScript example in `streaming.mdx` +- `streaming-2` for the second JavaScript example +- etc. + +**Important**: Number all JavaScript examples, even those you don't translate (like data structure literals). This keeps IDs stable and predictable. If you skip translating an example, just skip that ID - don't renumber. + +This ID must be consistent across: +1. The harness function name (e.g., `func example_streaming_1(...)`) +2. The MDX harness comment (e.g., `ID: streaming-1`) +3. Your JSON output + +All three must match exactly to enable correlation between harness, MDX, and verification results. + +--- + +## 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.0 + import PackageDescription + + let package = Package( + name: "SwiftTestHarness", + platforms: [ + .macOS(.v15) // Required for modern Swift features like typed AsyncSequence + ], + 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 a unique identifier in the function name in case you later want to use the same test harness for multiple examples (for efficiency): + +```swift +func exampleContext_7EEA145D_060F_4DAD_BFBF_1A4CC28856E8() { + // 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 (streaming-1 -> example_streaming_1) +func example_streaming_1(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 + +#### Handling mutable state with @MainActor + +**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, when the JavaScript example uses mutable local variables (like `Map`, arrays, or objects that get mutated), use `@MainActor` isolation with plain local variables. This keeps the Swift code close to the JavaScript structure. + +Mark the harness function with `@MainActor`. Since ably-cocoa executes callbacks on the main thread by default, use `MainActor.assumeIsolated { }` inside 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 + } + } +} +``` + +This pattern: +- Mirrors JavaScript's straightforward mutable variable approach +- Avoids custom actor types, which are rarely used in typical Swift code and would be unfamiliar to most readers +- Is simpler for readers to understand + +#### 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. `async throws` with continuations**: When the JavaScript has `async function`, the Swift equivalent should be `async throws` using `withCheckedThrowingContinuation` to bridge callback-based SDK calls: +```swift +func sendPrompt(text: String) async throws { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + channel.publish("prompt", data: text) { error in + if let error { + continuation.resume(throwing: error) + } else { + continuation.resume() + } + } + } +} +``` +Use this when the JavaScript function awaits SDK calls. Use plain callback chaining when only demonstrating SDK call sequences. + +**C8. 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. + +**C9. No `(value: T, Void)` tuples for single-property types**: Do NOT use tuples like `(text: String, Void)` to mimic JS objects with one property. Use `T` directly — e.g., `[String: String]` instead of `[String: (text: String, Void)]`. + +**C10. No `nonisolated(unsafe)`**: Never use `nonisolated(unsafe)` for mutable state. Instead, mark the harness function with `@MainActor` and use `MainActor.assumeIsolated { }` inside ably-cocoa 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 +channel.publish("response", data: "") { publishResult, error in + if let error { + print("Error publishing message: \(error)") + return + } + + let msgSerial = publishResult!.serials[0].value! + + Task { + // 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) { _, error in + if let error { + print("Error appending to message: \(error)") + } + } + } + } + } +} +``` + +--- + +## 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(channel: ARTRealtimeChannel, stream: any AsyncSequence<(type: String, text: String), Never> & Sendable) async throws { + // Publish initial message and capture the serial for appending tokens + channel.publish("response", data: "") { publishResult, error in + if let error { + print("Error publishing message: \(error)") + return + } + + let msgSerial = publishResult!.serials[0].value! + + Task { + // 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) { _, error in + if let error { + print("Error appending to message: \(error)") + } + } + } + } + } + } +} +``` + +--- + +## 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 the test harness context as a JSX comment with the example ID: + +```mdx + +```javascript +// original JavaScript code +``` + +{/* Swift example test harness +ID: streaming-1 +To verify: copy this comment into a Swift file, paste the example code into the function body, run `swift build` + +func example(channel: ARTRealtimeChannel, stream: any AsyncSequence<(type: String, text: String), Never>) async throws { + // --- example code starts here --- +*/} +```swift +// translated Swift code goes here +``` +{/* --- end example code --- */} + +``` + +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 example test harness +ID: streaming-5 +To verify: copy this comment into a Swift file, paste the example code into the function body, run `swift build` + +struct ResponseData { + var timestamp: Date + var content: String +} + +func example(channel: ARTRealtimeChannel) { + // --- example code starts here --- +*/} +```swift +// The type name `ResponseData` appears in the translated code +let responses: [String: ResponseData] = [:] +// ... +``` +{/* --- end example code --- */} + +``` + +The test harness comment must include **everything needed to compile the example**: +- **ID**: Unique identifier for this example (used by verification to match translations) +- **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: +- **Verification agent** to match this translation with its verification results +- **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..625a8243eb --- /dev/null +++ b/.claude/skills/translate-examples-to-swift/prompts/verification-subagent.md @@ -0,0 +1,140 @@ +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: +- Extract the **ID** from the JSX harness comment (line starting with `ID:`) - this goes in `id` +- Extract the JavaScript code (this goes in `original.code`) +- Extract the Swift code (this goes in `translation.code`) +- Extract the function signature from the JSX harness comment (this goes in `harness.functionSignature`) +- Build the full compilable context (this goes in `harness.fullContext`) + +**Important**: The ID must be extracted from the harness comment, not generated. If no ID is found in the comment, report this as an error - the translation is incomplete. + +### 2. Recreate the test harness from scratch + +Create a new Swift package in swift-translations/verify-{FILENAME}/: + +```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.0 +import PackageDescription + +let package = Package( + name: "SwiftVerification", + platforms: [ + .macOS(.v15) + ], + dependencies: [ + .package(url: "https://github.com/ably/ably-cocoa", from: "1.2.0") + ], + targets: [ + .executableTarget( + name: "SwiftVerification", + dependencies: [ + .product(name: "Ably", package: "ably-cocoa") + ] + ), + ] +) +``` + +Create the harness in `Sources/SwiftVerification/main.swift`. **Important**: Include ALL Swift examples from the MDX file in a single harness file. Use the example ID from the MDX harness comment in each function name: + +```swift +import Ably + +// MARK: - Example streaming-1 +func example_streaming_1(channel: ARTRealtimeChannel) { + // Example code from MDX inserted here +} + +// MARK: - Example streaming-3 +// (streaming-2 was not translated, so there's a gap in numbering) +func example_streaming_3(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") + } +} +``` + +This ensures: +1. All translated examples are verified in a single compilation +2. The harness file can be compared with the translation harness for discrepancies +3. Function names match the IDs in the MDX for easy correlation +4. Gaps in numbering are expected (some JS examples may not have Swift translations) + +### 3. Insert the example code and verify compilation + +- Copy the Swift example code from the documentation (the code between the ``` markers, NOT the harness comment) +- Insert it into the function body in your recreated harness +- Run `swift build` +- Record: "pass" if it compiles, "fail" with error message if not + +### 4. 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) + +### 5. 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 extracted from the harness comment (e.g., "streaming-1", "streaming-2") +- `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": "..." }` + +### 6. Report findings + +Provide a summary of what you verified and any issues found. + +--- + +## Important + +- Do NOT modify any documentation files - you are only verifying +- If you cannot recreate a harness from the comment alone, note this as an issue (the comment is incomplete) +- 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..c001367935 --- /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": "example-1", + "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..4d0dee3f23 --- /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": "example-1", + "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, format: {filename}-{sequential}", + "examples": ["streaming-1", "citations-2"] + }, + "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..57bbf95217 --- /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": "example-1", + "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", + "examples": ["streaming-1", "citations-2"] + }, + "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..434fca1d46 --- /dev/null +++ b/.claude/skills/translate-examples-to-swift/scripts/consolidate.sh @@ -0,0 +1,187 @@ +#!/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`); + } + }); + + 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 11c7f8ca85..e4dbb6ce24 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,4 @@ config/nginx-redirects.conf src/gatsby-types.d.ts .idea/* **/*.swp -.claude +.claude/*.local.* diff --git a/src/data/languages/languageData.ts b/src/data/languages/languageData.ts index db68469c71..7c2e6aa8de 100644 --- a/src/data/languages/languageData.ts +++ b/src/data/languages/languageData.ts @@ -46,6 +46,7 @@ export default { javascript: '2.11', java: '1.6', python: '3.0', + swift: '1.2', }, spaces: { javascript: '0.4', diff --git a/src/pages/docs/ai-transport/messaging/accepting-user-input.mdx b/src/pages/docs/ai-transport/messaging/accepting-user-input.mdx index 9713595eb8..b662c38a3a 100644 --- a/src/pages/docs/ai-transport/messaging/accepting-user-input.mdx +++ b/src/pages/docs/ai-transport/messaging/accepting-user-input.mdx @@ -52,6 +52,20 @@ claims = { Map claims = new HashMap<>(); claims.put("x-ably-clientId", "user-123"); ``` + +{/* Swift example test harness +ID: accepting-user-input-1 +To verify: copy this comment into a Swift file, paste the example code into the function body, run `swift build` + +func example() { + // --- example code starts here --- +*/} +```swift +let claims = [ + "x-ably-clientId": "user-123" +] +``` +{/* --- end example code --- */}
The `clientId` is automatically attached to every message the user publishes, so agents can trust this identity. @@ -91,6 +105,32 @@ channel.subscribe("user-input", message -> { processAndRespond(channel, text, promptId, userId); }); ``` + +{/* Swift example test harness +ID: accepting-user-input-2 +To verify: copy this comment into a Swift file, paste the example code into the function body, run `swift build` + +func example_accepting_user_input_2( + channel: ARTRealtimeChannel, + processAndRespond: @escaping (_ channel: ARTRealtimeChannel, _ text: String, _ promptID: String, _ userID: String) -> Void +) { + // --- example code starts here --- +*/} +```swift +channel.subscribe("user-input") { message in + guard let userID = message.clientId else { return } + // promptId is a user-generated UUID for correlating responses + guard let data = message.data as? [String: Any], + let promptID = data["promptId"] as? String, + let text = data["text"] as? String else { + return + } + + print("Received prompt from user \(userID)") + processAndRespond(channel, text, promptID, userID) +} +``` +{/* --- end example code --- */}
### Verify by role @@ -114,6 +154,20 @@ claims = { Map claims = new HashMap<>(); claims.put("ably.channel.*", "user"); ``` + +{/* Swift example test harness +ID: accepting-user-input-3 +To verify: copy this comment into a Swift file, paste the example code into the function body, run `swift build` + +func example() { + // --- example code starts here --- +*/} +```swift +let claims = [ + "ably.channel.*": "user" +] +``` +{/* --- end example code --- */}
The user claim is automatically attached to every message the user publishes, so agents can trust this role information. @@ -164,6 +218,36 @@ channel.subscribe("user-input", message -> { processAndRespond(channel, text, promptId); }); ``` + +{/* Swift example test harness +ID: accepting-user-input-4 +To verify: copy this comment into a Swift file, paste the example code into the function body, run `swift build` + +func example_accepting_user_input_4( + channel: ARTRealtimeChannel, + processAndRespond: @escaping (_ channel: ARTRealtimeChannel, _ text: String, _ promptID: String) -> Void +) { + // --- example code starts here --- +*/} +```swift +channel.subscribe("user-input") { message in + let role = (try? message.extras?.toJSON())?["userClaim"] as? String + // promptId is a user-generated UUID for correlating responses + guard let data = message.data as? [String: Any], + let promptID = data["promptId"] as? String, + let text = data["text"] as? String else { + return + } + + guard role == "user" else { + print("Ignoring message from non-user") + return + } + + processAndRespond(channel, text, promptID) +} +``` +{/* --- end example code --- */} ## Publish user input @@ -204,6 +288,24 @@ data.addProperty("promptId", promptId); data.addProperty("text", "What is the weather like today?"); channel.publish("user-input", data); ``` + +{/* Swift example test harness +ID: accepting-user-input-5 +To verify: copy this comment into a Swift file, paste the example code into the function body, run `swift build` + +func example_accepting_user_input_5(ably: ARTRealtime) { + // --- example code starts here --- +*/} +```swift +let channel = ably.channels.get("{{RANDOM_CHANNEL_NAME}}") + +let promptID = UUID().uuidString +channel.publish("user-input", data: [ + "promptId": promptID, + "text": "What is the weather like today?" +]) +``` +{/* --- end example code --- */}