diff --git a/.github/workflows/docs-validation.yml b/.github/workflows/docs-validation.yml new file mode 100644 index 00000000..210a7dd7 --- /dev/null +++ b/.github/workflows/docs-validation.yml @@ -0,0 +1,123 @@ +name: "Documentation Validation" + +on: + pull_request: + paths: + - 'docs/**' + - 'nodejs/src/**' + - 'python/copilot/**' + - 'go/**/*.go' + - 'dotnet/src/**' + - 'scripts/docs-validation/**' + - '.github/workflows/docs-validation.yml' + workflow_dispatch: + merge_group: + +permissions: + contents: read + +jobs: + validate-typescript: + name: "Validate TypeScript" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: "npm" + cache-dependency-path: "nodejs/package-lock.json" + + - name: Install SDK dependencies + working-directory: nodejs + run: npm ci --ignore-scripts + + - name: Install validation dependencies + working-directory: scripts/docs-validation + run: npm ci + + - name: Extract and validate TypeScript + working-directory: scripts/docs-validation + run: npm run extract && npm run validate:ts + + validate-python: + name: "Validate Python" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install uv + uses: astral-sh/setup-uv@v5 + + - name: Install SDK dependencies + working-directory: python + run: uv sync + + - name: Install mypy + run: pip install mypy + + - name: Install validation dependencies + working-directory: scripts/docs-validation + run: npm ci + + - name: Extract and validate Python + working-directory: scripts/docs-validation + run: npm run extract && npm run validate:py + + validate-go: + name: "Validate Go" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - uses: actions/setup-go@v5 + with: + go-version: "1.23" + cache-dependency-path: "go/go.sum" + + - name: Install validation dependencies + working-directory: scripts/docs-validation + run: npm ci + + - name: Extract and validate Go + working-directory: scripts/docs-validation + run: npm run extract && npm run validate:go + + validate-csharp: + name: "Validate C#" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: "8.0.x" + + - name: Install validation dependencies + working-directory: scripts/docs-validation + run: npm ci + + - name: Restore SDK dependencies + working-directory: dotnet + run: dotnet restore + + - name: Extract and validate C# + working-directory: scripts/docs-validation + run: npm run extract && npm run validate:cs diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..9ec30582 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ + +# Documentation validation output +docs/.validation/ diff --git a/docs/auth/byok.md b/docs/auth/byok.md index 05256e97..6c836743 100644 --- a/docs/auth/byok.md +++ b/docs/auth/byok.md @@ -104,13 +104,14 @@ import ( ) func main() { + ctx := context.Background() client := copilot.NewClient(nil) - if err := client.Start(); err != nil { + if err := client.Start(ctx); err != nil { panic(err) } defer client.Stop() - session, err := client.CreateSession(context.Background(), &copilot.SessionConfig{ + session, err := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "gpt-5.2-codex", // Your deployment name Provider: &copilot.ProviderConfig{ Type: "openai", @@ -123,9 +124,9 @@ func main() { panic(err) } - response, err := session.SendAndWait(copilot.MessageOptions{ + response, err := session.SendAndWait(ctx, copilot.MessageOptions{ Prompt: "What is 2+2?", - }, 0) + }) if err != nil { panic(err) } @@ -325,6 +326,7 @@ const session = await client.createSession({ For Azure OpenAI endpoints (`*.openai.azure.com`), use the correct type: + ```typescript // ❌ Wrong: Using "openai" type with native Azure endpoint provider: { @@ -341,6 +343,7 @@ provider: { However, if your Azure AI Foundry deployment provides an OpenAI-compatible endpoint path (e.g., `/openai/v1/`), use `type: "openai"`: + ```typescript // ✅ Correct: OpenAI-compatible Azure AI Foundry endpoint provider: { diff --git a/docs/auth/index.md b/docs/auth/index.md index ffd47625..9fc65fe2 100644 --- a/docs/auth/index.md +++ b/docs/auth/index.md @@ -50,6 +50,7 @@ await client.start()
Go + ```go import copilot "github.com/github/copilot-sdk/go" @@ -119,6 +120,7 @@ await client.start()
Go + ```go import copilot "github.com/github/copilot-sdk/go" @@ -133,6 +135,7 @@ client := copilot.NewClient(&copilot.ClientOptions{
.NET + ```csharp using GitHub.Copilot.SDK; @@ -251,6 +254,7 @@ const client = new CopilotClient({
Python + ```python client = CopilotClient({ "use_logged_in_user": False, # Only use explicit tokens @@ -262,6 +266,7 @@ client = CopilotClient({
Go + ```go client := copilot.NewClient(&copilot.ClientOptions{ UseLoggedInUser: copilot.Bool(false), // Only use explicit tokens diff --git a/docs/debugging.md b/docs/debugging.md index dffd85a3..6183cccd 100644 --- a/docs/debugging.md +++ b/docs/debugging.md @@ -36,7 +36,7 @@ const client = new CopilotClient({ ```python from copilot import CopilotClient -client = CopilotClient(log_level="debug") +client = CopilotClient({"log_level": "debug"}) ```
@@ -44,6 +44,7 @@ client = CopilotClient(log_level="debug")
Go + ```go import copilot "github.com/github/copilot-sdk/go" @@ -57,6 +58,7 @@ client := copilot.NewClient(&copilot.ClientOptions{
.NET + ```csharp using GitHub.Copilot.SDK; using Microsoft.Extensions.Logging; @@ -108,6 +110,7 @@ const client = new CopilotClient({
Go + ```go // The Go SDK does not currently support passing extra CLI arguments. // For custom log directories, run the CLI manually with --log-dir @@ -161,7 +164,7 @@ var client = new CopilotClient(new CopilotClientOptions Python ```python - client = CopilotClient(cli_path="/usr/local/bin/copilot") + client = CopilotClient({"cli_path": "/usr/local/bin/copilot"}) ```
@@ -214,7 +217,7 @@ var client = new CopilotClient(new CopilotClientOptions ```python import os - client = CopilotClient(github_token=os.environ.get("GITHUB_TOKEN")) + client = CopilotClient({"github_token": os.environ.get("GITHUB_TOKEN")}) ```
diff --git a/docs/getting-started.md b/docs/getting-started.md index 7aea1e06..f615e923 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -162,6 +162,7 @@ Create `main.go`: package main import ( + "context" "fmt" "log" "os" @@ -170,18 +171,19 @@ import ( ) func main() { + ctx := context.Background() client := copilot.NewClient(nil) - if err := client.Start(); err != nil { + if err := client.Start(ctx); err != nil { log.Fatal(err) } defer client.Stop() - session, err := client.CreateSession(&copilot.SessionConfig{Model: "gpt-4.1"}) + session, err := client.CreateSession(ctx, &copilot.SessionConfig{Model: "gpt-4.1"}) if err != nil { log.Fatal(err) } - response, err := session.SendAndWait(copilot.MessageOptions{Prompt: "What is 2 + 2?"}, 0) + response, err := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "What is 2 + 2?"}) if err != nil { log.Fatal(err) } @@ -312,6 +314,7 @@ Update `main.go`: package main import ( + "context" "fmt" "log" "os" @@ -320,13 +323,14 @@ import ( ) func main() { + ctx := context.Background() client := copilot.NewClient(nil) - if err := client.Start(); err != nil { + if err := client.Start(ctx); err != nil { log.Fatal(err) } defer client.Stop() - session, err := client.CreateSession(&copilot.SessionConfig{ + session, err := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "gpt-4.1", Streaming: true, }) @@ -344,7 +348,7 @@ func main() { } }) - _, err = session.SendAndWait(copilot.MessageOptions{Prompt: "Tell me a short joke"}, 0) + _, err = session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "Tell me a short joke"}) if err != nil { log.Fatal(err) } @@ -389,6 +393,112 @@ await session.SendAndWaitAsync(new MessageOptions { Prompt = "Tell me a short jo Run the code again. You'll see the response appear word by word. +### Event Subscription Methods + +The SDK provides methods for subscribing to session events: + +| Method | Description | +|--------|-------------| +| `on(handler)` | Subscribe to all events; returns unsubscribe function | +| `on(eventType, handler)` | Subscribe to specific event type (Node.js/TypeScript only); returns unsubscribe function | + +
+Node.js / TypeScript + +```typescript +// Subscribe to all events +const unsubscribeAll = session.on((event) => { + console.log("Event:", event.type); +}); + +// Subscribe to specific event type +const unsubscribeIdle = session.on("session.idle", (event) => { + console.log("Session is idle"); +}); + +// Later, to unsubscribe: +unsubscribeAll(); +unsubscribeIdle(); +``` + +
+ +
+Python + + +```python +# Subscribe to all events +unsubscribe = session.on(lambda event: print(f"Event: {event.type}")) + +# Filter by event type in your handler +def handle_event(event): + if event.type == SessionEventType.SESSION_IDLE: + print("Session is idle") + elif event.type == SessionEventType.ASSISTANT_MESSAGE: + print(f"Message: {event.data.content}") + +unsubscribe = session.on(handle_event) + +# Later, to unsubscribe: +unsubscribe() +``` + +
+ +
+Go + + +```go +// Subscribe to all events +unsubscribe := session.On(func(event copilot.SessionEvent) { + fmt.Println("Event:", event.Type) +}) + +// Filter by event type in your handler +session.On(func(event copilot.SessionEvent) { + if event.Type == "session.idle" { + fmt.Println("Session is idle") + } else if event.Type == "assistant.message" { + fmt.Println("Message:", *event.Data.Content) + } +}) + +// Later, to unsubscribe: +unsubscribe() +``` + +
+ +
+.NET + + +```csharp +// Subscribe to all events +var unsubscribe = session.On(ev => Console.WriteLine($"Event: {ev.Type}")); + +// Filter by event type using pattern matching +session.On(ev => +{ + switch (ev) + { + case SessionIdleEvent: + Console.WriteLine("Session is idle"); + break; + case AssistantMessageEvent msg: + Console.WriteLine($"Message: {msg.Data.Content}"); + break; + } +}); + +// Later, to unsubscribe: +unsubscribe.Dispose(); +``` + +
+ ## Step 4: Add a Custom Tool Now for the powerful part. Let's give Copilot the ability to call your code by defining a custom tool. We'll create a simple weather lookup tool. @@ -513,6 +623,7 @@ Update `main.go`: package main import ( + "context" "fmt" "log" "math/rand" @@ -534,6 +645,8 @@ type WeatherResult struct { } func main() { + ctx := context.Background() + // Define a tool that Copilot can call getWeather := copilot.DefineTool( "get_weather", @@ -552,12 +665,12 @@ func main() { ) client := copilot.NewClient(nil) - if err := client.Start(); err != nil { + if err := client.Start(ctx); err != nil { log.Fatal(err) } defer client.Stop() - session, err := client.CreateSession(&copilot.SessionConfig{ + session, err := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "gpt-4.1", Streaming: true, Tools: []copilot.Tool{getWeather}, @@ -575,9 +688,9 @@ func main() { } }) - _, err = session.SendAndWait(copilot.MessageOptions{ + _, err = session.SendAndWait(ctx, copilot.MessageOptions{ Prompt: "What's the weather like in Seattle and Tokyo?", - }, 0) + }) if err != nil { log.Fatal(err) } @@ -796,6 +909,7 @@ package main import ( "bufio" + "context" "fmt" "log" "math/rand" @@ -816,6 +930,8 @@ type WeatherResult struct { } func main() { + ctx := context.Background() + getWeather := copilot.DefineTool( "get_weather", "Get the current weather for a city", @@ -832,12 +948,12 @@ func main() { ) client := copilot.NewClient(nil) - if err := client.Start(); err != nil { + if err := client.Start(ctx); err != nil { log.Fatal(err) } defer client.Stop() - session, err := client.CreateSession(&copilot.SessionConfig{ + session, err := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "gpt-4.1", Streaming: true, Tools: []copilot.Tool{getWeather}, @@ -872,7 +988,7 @@ func main() { } fmt.Print("Assistant: ") - _, err = session.SendAndWait(copilot.MessageOptions{Prompt: input}, 0) + _, err = session.SendAndWait(ctx, copilot.MessageOptions{Prompt: input}) if err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) break @@ -1111,6 +1227,7 @@ session = await client.create_session()
Go + ```go import copilot "github.com/github/copilot-sdk/go" @@ -1118,13 +1235,13 @@ client := copilot.NewClient(&copilot.ClientOptions{ CLIUrl: "localhost:4321", }) -if err := client.Start(); err != nil { +if err := client.Start(ctx); err != nil { log.Fatal(err) } defer client.Stop() // Use the client normally -session, err := client.CreateSession() +session, err := client.CreateSession(ctx, nil) // ... ``` diff --git a/docs/guides/session-persistence.md b/docs/guides/session-persistence.md index 4586201e..7c16c53a 100644 --- a/docs/guides/session-persistence.md +++ b/docs/guides/session-persistence.md @@ -49,12 +49,13 @@ await session.sendAndWait({ prompt: "Analyze my codebase" }); from copilot import CopilotClient client = CopilotClient() +await client.start() # Create a session with a meaningful ID -session = await client.create_session( - session_id="user-123-task-456", - model="gpt-5.2-codex", -) +session = await client.create_session({ + "session_id": "user-123-task-456", + "model": "gpt-5.2-codex", +}) # Do some work... await session.send_and_wait({"prompt": "Analyze my codebase"}) @@ -64,17 +65,19 @@ await session.send_and_wait({"prompt": "Analyze my codebase"}) ### Go + ```go +ctx := context.Background() client := copilot.NewClient(nil) // Create a session with a meaningful ID -session, _ := client.CreateSession(context.Background(), &copilot.SessionConfig{ +session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ SessionID: "user-123-task-456", Model: "gpt-5.2-codex", }) // Do some work... -session.SendAndWait(context.Background(), copilot.MessageOptions{Prompt: "Analyze my codebase"}) +session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "Analyze my codebase"}) // Session state is automatically persisted ``` @@ -87,7 +90,7 @@ using GitHub.Copilot.SDK; var client = new CopilotClient(); // Create a session with a meaningful ID -var session = await client.CreateSessionAsync(new CreateSessionOptions +var session = await client.CreateSessionAsync(new SessionConfig { SessionId = "user-123-task-456", Model = "gpt-5.2-codex", @@ -139,16 +142,20 @@ await session.send_and_wait({"prompt": "What did we discuss earlier?"}) ### Go + ```go +ctx := context.Background() + // Resume from a different client instance (or after restart) -session, _ := client.ResumeSession(context.Background(), "user-123-task-456", nil) +session, _ := client.ResumeSession(ctx, "user-123-task-456", nil) // Continue where you left off -session.SendAndWait(context.Background(), copilot.MessageOptions{Prompt: "What did we discuss earlier?"}) +session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "What did we discuss earlier?"}) ``` ### C# (.NET) + ```csharp // Resume from a different client instance (or after restart) var session = await client.ResumeSessionAsync("user-123-task-456"); diff --git a/docs/guides/skills.md b/docs/guides/skills.md new file mode 100644 index 00000000..e17a6209 --- /dev/null +++ b/docs/guides/skills.md @@ -0,0 +1,327 @@ +# Custom Skills + +Skills are reusable collections of prompts, tools, and configuration that extend Copilot's capabilities. Load skills from directories to give Copilot specialized abilities for specific domains or workflows. + +## Overview + +A skill is a directory containing: +- **Prompt files** - Instructions that guide Copilot's behavior +- **Tool definitions** - Custom tools the skill provides +- **Configuration** - Metadata about the skill + +Skills allow you to: +- Package domain expertise into reusable modules +- Share specialized behaviors across projects +- Organize complex agent configurations +- Enable/disable capabilities per session + +## Loading Skills + +Specify directories containing skills when creating a session: + +
+Node.js / TypeScript + +```typescript +import { CopilotClient } from "@github/copilot-sdk"; + +const client = new CopilotClient(); +const session = await client.createSession({ + model: "gpt-4.1", + skillDirectories: [ + "./skills/code-review", + "./skills/documentation", + "~/.copilot/skills", // User-level skills + ], +}); + +// Copilot now has access to skills in those directories +await session.sendAndWait({ prompt: "Review this code for security issues" }); +``` + +
+ +
+Python + +```python +from copilot import CopilotClient + +async def main(): + client = CopilotClient() + await client.start() + + session = await client.create_session({ + "model": "gpt-4.1", + "skill_directories": [ + "./skills/code-review", + "./skills/documentation", + "~/.copilot/skills", # User-level skills + ], + }) + + # Copilot now has access to skills in those directories + await session.send_and_wait({"prompt": "Review this code for security issues"}) + + await client.stop() +``` + +
+ +
+Go + +```go +package main + +import ( + "context" + "log" + copilot "github.com/github/copilot-sdk/go" +) + +func main() { + ctx := context.Background() + client := copilot.NewClient(nil) + if err := client.Start(ctx); err != nil { + log.Fatal(err) + } + defer client.Stop() + + session, err := client.CreateSession(ctx, &copilot.SessionConfig{ + Model: "gpt-4.1", + SkillDirectories: []string{ + "./skills/code-review", + "./skills/documentation", + "~/.copilot/skills", // User-level skills + }, + }) + if err != nil { + log.Fatal(err) + } + + // Copilot now has access to skills in those directories + _, err = session.SendAndWait(ctx, copilot.MessageOptions{ + Prompt: "Review this code for security issues", + }) + if err != nil { + log.Fatal(err) + } +} +``` + +
+ +
+.NET + +```csharp +using GitHub.Copilot.SDK; + +await using var client = new CopilotClient(); +await using var session = await client.CreateSessionAsync(new SessionConfig +{ + Model = "gpt-4.1", + SkillDirectories = new List + { + "./skills/code-review", + "./skills/documentation", + "~/.copilot/skills", // User-level skills + }, +}); + +// Copilot now has access to skills in those directories +await session.SendAndWaitAsync(new MessageOptions +{ + Prompt = "Review this code for security issues" +}); +``` + +
+ +## Disabling Skills + +Disable specific skills while keeping others active: + +
+Node.js / TypeScript + +```typescript +const session = await client.createSession({ + skillDirectories: ["./skills"], + disabledSkills: ["experimental-feature", "deprecated-tool"], +}); +``` + +
+ +
+Python + +```python +session = await client.create_session({ + "skill_directories": ["./skills"], + "disabled_skills": ["experimental-feature", "deprecated-tool"], +}) +``` + +
+ +
+Go + + +```go +session, _ := client.CreateSession(context.Background(), &copilot.SessionConfig{ + SkillDirectories: []string{"./skills"}, + DisabledSkills: []string{"experimental-feature", "deprecated-tool"}, +}) +``` + +
+ +
+.NET + + +```csharp +var session = await client.CreateSessionAsync(new SessionConfig +{ + SkillDirectories = new List { "./skills" }, + DisabledSkills = new List { "experimental-feature", "deprecated-tool" }, +}); +``` + +
+ +## Skill Directory Structure + +A typical skill directory contains: + +``` +skills/ +└── code-review/ + ├── skill.json # Skill metadata and configuration + ├── prompts/ + │ ├── system.md # System prompt additions + │ └── examples.md # Few-shot examples + └── tools/ + └── lint.json # Tool definitions +``` + +### skill.json + +The skill manifest file: + +```json +{ + "name": "code-review", + "displayName": "Code Review Assistant", + "description": "Specialized code review capabilities", + "version": "1.0.0", + "author": "Your Team", + "prompts": ["prompts/system.md"], + "tools": ["tools/lint.json"] +} +``` + +### Prompt Files + +Markdown files that provide context to Copilot: + +```markdown + +# Code Review Guidelines + +When reviewing code, always check for: + +1. **Security vulnerabilities** - SQL injection, XSS, etc. +2. **Performance issues** - N+1 queries, memory leaks +3. **Code style** - Consistent formatting, naming conventions +4. **Test coverage** - Are critical paths tested? + +Provide specific line-number references and suggested fixes. +``` + +## Configuration Options + +### SessionConfig Skill Fields + +| Language | Field | Type | Description | +|----------|-------|------|-------------| +| Node.js | `skillDirectories` | `string[]` | Directories to load skills from | +| Node.js | `disabledSkills` | `string[]` | Skills to disable | +| Python | `skill_directories` | `list[str]` | Directories to load skills from | +| Python | `disabled_skills` | `list[str]` | Skills to disable | +| Go | `SkillDirectories` | `[]string` | Directories to load skills from | +| Go | `DisabledSkills` | `[]string` | Skills to disable | +| .NET | `SkillDirectories` | `List` | Directories to load skills from | +| .NET | `DisabledSkills` | `List` | Skills to disable | + +## Best Practices + +1. **Organize by domain** - Group related skills together (e.g., `skills/security/`, `skills/testing/`) + +2. **Version your skills** - Include version numbers in `skill.json` for compatibility tracking + +3. **Document dependencies** - Note any tools or MCP servers a skill requires + +4. **Test skills in isolation** - Verify skills work before combining them + +5. **Use relative paths** - Keep skills portable across environments + +## Combining with Other Features + +### Skills + Custom Agents + +Skills work alongside custom agents: + +```typescript +const session = await client.createSession({ + skillDirectories: ["./skills/security"], + customAgents: [{ + name: "security-auditor", + description: "Security-focused code reviewer", + prompt: "Focus on OWASP Top 10 vulnerabilities", + }], +}); +``` + +### Skills + MCP Servers + +Skills can complement MCP server capabilities: + +```typescript +const session = await client.createSession({ + skillDirectories: ["./skills/database"], + mcpServers: { + postgres: { + type: "local", + command: "npx", + args: ["-y", "@modelcontextprotocol/server-postgres"], + tools: ["*"], + }, + }, +}); +``` + +## Troubleshooting + +### Skills Not Loading + +1. **Check path exists** - Verify the directory path is correct +2. **Check permissions** - Ensure the SDK can read the directory +3. **Validate skill.json** - Check for JSON syntax errors +4. **Enable debug logging** - Set `logLevel: "debug"` to see skill loading logs + +### Skill Conflicts + +If multiple skills define the same tool: +- Later directories in the array take precedence +- Use `disabledSkills` to exclude conflicting skills + +## See Also + +- [Custom Agents](../getting-started.md#create-custom-agents) - Define specialized AI personas +- [Custom Tools](../getting-started.md#step-4-add-a-custom-tool) - Build your own tools +- [MCP Servers](../mcp/overview.md) - Connect external tool providers diff --git a/docs/hooks/error-handling.md b/docs/hooks/error-handling.md index 572455b1..0f705868 100644 --- a/docs/hooks/error-handling.md +++ b/docs/hooks/error-handling.md @@ -12,6 +12,7 @@ The `onErrorOccurred` hook is called when errors occur during session execution.
Node.js / TypeScript + ```typescript type ErrorOccurredHandler = ( input: ErrorOccurredHookInput, @@ -24,6 +25,7 @@ type ErrorOccurredHandler = (
Python + ```python ErrorOccurredHandler = Callable[ [ErrorOccurredHookInput, HookInvocation], @@ -36,6 +38,7 @@ ErrorOccurredHandler = Callable[
Go + ```go type ErrorOccurredHandler func( input ErrorOccurredHookInput, @@ -48,6 +51,7 @@ type ErrorOccurredHandler func(
.NET + ```csharp public delegate Task ErrorOccurredHandler( ErrorOccurredHookInput input, @@ -105,7 +109,7 @@ const session = await client.createSession({ ```python async def on_error_occurred(input_data, invocation): print(f"[{invocation['session_id']}] Error: {input_data['error']}") - print(f" Context: {input_data['error_context']}") + print(f" Context: {input_data['errorContext']}") print(f" Recoverable: {input_data['recoverable']}") return None @@ -119,6 +123,7 @@ session = await client.create_session({
Go + ```go session, _ := client.CreateSession(context.Background(), &copilot.SessionConfig{ Hooks: &copilot.SessionHooks{ @@ -137,6 +142,7 @@ session, _ := client.CreateSession(context.Background(), &copilot.SessionConfig{
.NET + ```csharp var session = await client.CreateSessionAsync(new SessionConfig { @@ -145,11 +151,8 @@ var session = await client.CreateSessionAsync(new SessionConfig OnErrorOccurred = (input, invocation) => { Console.Error.WriteLine($"[{invocation.SessionId}] Error: {input.Error}"); - Console.Error.WriteLine($" Type: {input.ErrorType}"); - if (!string.IsNullOrEmpty(input.Stack)) - { - Console.Error.WriteLine($" Stack: {input.Stack}"); - } + Console.Error.WriteLine($" Context: {input.ErrorContext}"); + Console.Error.WriteLine($" Recoverable: {input.Recoverable}"); return Task.FromResult(null); }, }, @@ -169,11 +172,11 @@ const session = await client.createSession({ captureException(new Error(input.error), { tags: { sessionId: invocation.sessionId, - errorType: input.errorType, + errorContext: input.errorContext, }, extra: { - stack: input.stack, - context: input.context, + error: input.error, + recoverable: input.recoverable, cwd: input.cwd, }, }); @@ -188,20 +191,20 @@ const session = await client.createSession({ ```typescript const ERROR_MESSAGES: Record = { - "rate_limit": "Too many requests. Please wait a moment and try again.", - "auth_failed": "Authentication failed. Please check your credentials.", - "network_error": "Network connection issue. Please check your internet connection.", - "timeout": "Request timed out. Try breaking your request into smaller parts.", + "model_call": "There was an issue communicating with the AI model. Please try again.", + "tool_execution": "A tool failed to execute. Please check your inputs and try again.", + "system": "A system error occurred. Please try again later.", + "user_input": "There was an issue with your input. Please check and try again.", }; const session = await client.createSession({ hooks: { onErrorOccurred: async (input) => { - const friendlyMessage = ERROR_MESSAGES[input.errorType]; + const friendlyMessage = ERROR_MESSAGES[input.errorContext]; if (friendlyMessage) { return { - modifiedMessage: friendlyMessage, + userNotification: friendlyMessage, }; } @@ -214,17 +217,13 @@ const session = await client.createSession({ ### Suppress Non-Critical Errors ```typescript -const SUPPRESSED_ERRORS = [ - "tool_not_found", - "file_not_found", -]; - const session = await client.createSession({ hooks: { onErrorOccurred: async (input) => { - if (SUPPRESSED_ERRORS.includes(input.errorType)) { - console.log(`Suppressed error: ${input.errorType}`); - return { suppressError: true }; + // Suppress tool execution errors that are recoverable + if (input.errorContext === "tool_execution" && input.recoverable) { + console.log(`Suppressed recoverable error: ${input.error}`); + return { suppressOutput: true }; } return null; }, @@ -238,9 +237,9 @@ const session = await client.createSession({ const session = await client.createSession({ hooks: { onErrorOccurred: async (input) => { - if (input.errorType === "tool_execution_failed") { + if (input.errorContext === "tool_execution") { return { - additionalContext: ` + userNotification: ` The tool failed. Here are some recovery suggestions: - Check if required dependencies are installed - Verify file paths are correct @@ -249,9 +248,11 @@ The tool failed. Here are some recovery suggestions: }; } - if (input.errorType === "rate_limit") { + if (input.errorContext === "model_call" && input.error.includes("rate")) { return { - additionalContext: "Rate limit hit. Waiting before retry.", + errorHandling: "retry", + retryCount: 3, + userNotification: "Rate limit hit. Retrying...", }; } @@ -275,7 +276,7 @@ const errorStats = new Map(); const session = await client.createSession({ hooks: { onErrorOccurred: async (input, invocation) => { - const key = `${input.errorType}:${input.error.substring(0, 50)}`; + const key = `${input.errorContext}:${input.error.substring(0, 50)}`; const existing = errorStats.get(key) || { count: 0, @@ -303,17 +304,17 @@ const session = await client.createSession({ ### Alert on Critical Errors ```typescript -const CRITICAL_ERRORS = ["auth_failed", "api_error", "system_error"]; +const CRITICAL_CONTEXTS = ["system", "model_call"]; const session = await client.createSession({ hooks: { onErrorOccurred: async (input, invocation) => { - if (CRITICAL_ERRORS.includes(input.errorType)) { + if (CRITICAL_CONTEXTS.includes(input.errorContext) && !input.recoverable) { await sendAlert({ level: "critical", message: `Critical error in session ${invocation.sessionId}`, error: input.error, - type: input.errorType, + context: input.errorContext, timestamp: new Date(input.timestamp).toISOString(), }); } @@ -350,7 +351,7 @@ const session = await client.createSession({ console.error(`Error in session ${invocation.sessionId}:`); console.error(` Error: ${input.error}`); - console.error(` Type: ${input.errorType}`); + console.error(` Context: ${input.errorContext}`); if (ctx?.lastTool) { console.error(` Last tool: ${ctx.lastTool}`); } diff --git a/docs/hooks/overview.md b/docs/hooks/overview.md index be04b417..a51ef046 100644 --- a/docs/hooks/overview.md +++ b/docs/hooks/overview.md @@ -57,6 +57,7 @@ from copilot import CopilotClient async def main(): client = CopilotClient() + await client.start() async def on_pre_tool_use(input_data, invocation): print(f"Tool called: {input_data['toolName']}") @@ -231,5 +232,5 @@ const session = await client.createSession({ ## See Also - [Getting Started Guide](../getting-started.md) -- [Custom Tools](../getting-started.md#step-4-add-custom-tools) +- [Custom Tools](../getting-started.md#step-4-add-a-custom-tool) - [Debugging Guide](../debugging.md) diff --git a/docs/hooks/post-tool-use.md b/docs/hooks/post-tool-use.md index ecaf41ae..0021e20a 100644 --- a/docs/hooks/post-tool-use.md +++ b/docs/hooks/post-tool-use.md @@ -12,6 +12,7 @@ The `onPostToolUse` hook is called **after** a tool executes. Use it to:
Node.js / TypeScript + ```typescript type PostToolUseHandler = ( input: PostToolUseHookInput, @@ -24,6 +25,7 @@ type PostToolUseHandler = (
Python + ```python PostToolUseHandler = Callable[ [PostToolUseHookInput, HookInvocation], @@ -36,6 +38,7 @@ PostToolUseHandler = Callable[
Go + ```go type PostToolUseHandler func( input PostToolUseHookInput, @@ -48,6 +51,7 @@ type PostToolUseHandler func(
.NET + ```csharp public delegate Task PostToolUseHandler( PostToolUseHookInput input, @@ -118,6 +122,7 @@ session = await client.create_session({
Go + ```go session, _ := client.CreateSession(context.Background(), &copilot.SessionConfig{ Hooks: &copilot.SessionHooks{ @@ -136,6 +141,7 @@ session, _ := client.CreateSession(context.Background(), &copilot.SessionConfig{
.NET + ```csharp var session = await client.CreateSessionAsync(new SessionConfig { diff --git a/docs/hooks/pre-tool-use.md b/docs/hooks/pre-tool-use.md index 43bb63be..ac12df4f 100644 --- a/docs/hooks/pre-tool-use.md +++ b/docs/hooks/pre-tool-use.md @@ -12,6 +12,7 @@ The `onPreToolUse` hook is called **before** a tool executes. Use it to:
Node.js / TypeScript + ```typescript type PreToolUseHandler = ( input: PreToolUseHookInput, @@ -24,6 +25,7 @@ type PreToolUseHandler = (
Python + ```python PreToolUseHandler = Callable[ [PreToolUseHookInput, HookInvocation], @@ -36,6 +38,7 @@ PreToolUseHandler = Callable[
Go + ```go type PreToolUseHandler func( input PreToolUseHookInput, @@ -48,6 +51,7 @@ type PreToolUseHandler func(
.NET + ```csharp public delegate Task PreToolUseHandler( PreToolUseHookInput input, @@ -125,6 +129,7 @@ session = await client.create_session({
Go + ```go session, _ := client.CreateSession(context.Background(), &copilot.SessionConfig{ Hooks: &copilot.SessionHooks{ @@ -144,6 +149,7 @@ session, _ := client.CreateSession(context.Background(), &copilot.SessionConfig{
.NET + ```csharp var session = await client.CreateSessionAsync(new SessionConfig { diff --git a/docs/hooks/session-lifecycle.md b/docs/hooks/session-lifecycle.md index a1187cba..74f4666f 100644 --- a/docs/hooks/session-lifecycle.md +++ b/docs/hooks/session-lifecycle.md @@ -16,6 +16,7 @@ The `onSessionStart` hook is called when a session begins (new or resumed).
Node.js / TypeScript + ```typescript type SessionStartHandler = ( input: SessionStartHookInput, @@ -28,6 +29,7 @@ type SessionStartHandler = (
Python + ```python SessionStartHandler = Callable[ [SessionStartHookInput, HookInvocation], @@ -40,6 +42,7 @@ SessionStartHandler = Callable[
Go + ```go type SessionStartHandler func( input SessionStartHookInput, @@ -52,6 +55,7 @@ type SessionStartHandler func(
.NET + ```csharp public delegate Task SessionStartHandler( SessionStartHookInput input, @@ -204,6 +208,7 @@ type SessionEndHandler = (
Python + ```python SessionEndHandler = Callable[ [SessionEndHookInput, HookInvocation], @@ -216,6 +221,7 @@ SessionEndHandler = Callable[
Go + ```go type SessionEndHandler func( input SessionEndHookInput, diff --git a/docs/hooks/user-prompt-submitted.md b/docs/hooks/user-prompt-submitted.md index af013022..3205b95c 100644 --- a/docs/hooks/user-prompt-submitted.md +++ b/docs/hooks/user-prompt-submitted.md @@ -12,6 +12,7 @@ The `onUserPromptSubmitted` hook is called when a user submits a message. Use it
Node.js / TypeScript + ```typescript type UserPromptSubmittedHandler = ( input: UserPromptSubmittedHookInput, @@ -24,6 +25,7 @@ type UserPromptSubmittedHandler = (
Python + ```python UserPromptSubmittedHandler = Callable[ [UserPromptSubmittedHookInput, HookInvocation], @@ -36,6 +38,7 @@ UserPromptSubmittedHandler = Callable[
Go + ```go type UserPromptSubmittedHandler func( input UserPromptSubmittedHookInput, @@ -48,6 +51,7 @@ type UserPromptSubmittedHandler func(
.NET + ```csharp public delegate Task UserPromptSubmittedHandler( UserPromptSubmittedHookInput input, @@ -112,6 +116,7 @@ session = await client.create_session({
Go + ```go session, _ := client.CreateSession(context.Background(), &copilot.SessionConfig{ Hooks: &copilot.SessionHooks{ @@ -128,6 +133,7 @@ session, _ := client.CreateSession(context.Background(), &copilot.SessionConfig{
.NET + ```csharp var session = await client.CreateSessionAsync(new SessionConfig { @@ -205,9 +211,10 @@ const session = await client.createSession({ onUserPromptSubmitted: async (input) => { for (const pattern of BLOCKED_PATTERNS) { if (pattern.test(input.prompt)) { + // Replace the prompt with a warning message return { - reject: true, - rejectReason: "Please don't include sensitive credentials in your prompts. Use environment variables instead.", + modifiedPrompt: "[Content blocked: Please don't include sensitive credentials in your prompts. Use environment variables instead.]", + suppressOutput: true, }; } } @@ -226,9 +233,10 @@ const session = await client.createSession({ hooks: { onUserPromptSubmitted: async (input) => { if (input.prompt.length > MAX_PROMPT_LENGTH) { + // Truncate the prompt and add context return { - reject: true, - rejectReason: `Prompt too long (${input.prompt.length} chars). Maximum allowed: ${MAX_PROMPT_LENGTH} chars. Please shorten your request.`, + modifiedPrompt: input.prompt.substring(0, MAX_PROMPT_LENGTH), + additionalContext: `Note: The original prompt was ${input.prompt.length} characters and was truncated to ${MAX_PROMPT_LENGTH} characters.`, }; } return null; diff --git a/docs/mcp/debugging.md b/docs/mcp/debugging.md index 34e22a19..5ca51d1e 100644 --- a/docs/mcp/debugging.md +++ b/docs/mcp/debugging.md @@ -242,6 +242,7 @@ cd /expected/working/dir #### .NET Console Apps / Tools + ```csharp // Correct configuration for .NET exe ["my-dotnet-server"] = new McpLocalServerConfig @@ -266,6 +267,7 @@ cd /expected/working/dir #### NPX Commands + ```csharp // Windows needs cmd /c for npx ["filesystem"] = new McpLocalServerConfig @@ -302,6 +304,7 @@ xattr -d com.apple.quarantine /path/to/mcp-server #### Homebrew Paths + ```typescript // GUI apps may not have /opt/homebrew in PATH mcpServers: { diff --git a/docs/mcp/overview.md b/docs/mcp/overview.md index ed8eaa5e..aa2fba66 100644 --- a/docs/mcp/overview.md +++ b/docs/mcp/overview.md @@ -104,31 +104,35 @@ asyncio.run(main()) package main import ( + "context" "log" copilot "github.com/github/copilot-sdk/go" ) func main() { + ctx := context.Background() client := copilot.NewClient(nil) - if err := client.Start(); err != nil { + if err := client.Start(ctx); err != nil { log.Fatal(err) } defer client.Stop() - session, err := client.CreateSession(&copilot.SessionConfig{ + // MCPServerConfig is map[string]any for flexibility + session, err := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "gpt-5", MCPServers: map[string]copilot.MCPServerConfig{ "my-local-server": { - Type: "local", - Command: "node", - Args: []string{"./mcp-server.js"}, - Tools: []string{"*"}, + "type": "local", + "command": "node", + "args": []string{"./mcp-server.js"}, + "tools": []string{"*"}, }, }, }) if err != nil { log.Fatal(err) } + defer session.Destroy() // Use the session... } @@ -149,8 +153,8 @@ await using var session = await client.CreateSessionAsync(new SessionConfig { Type = "local", Command = "node", - Args = new[] { "./mcp-server.js" }, - Tools = new[] { "*" }, + Args = new List { "./mcp-server.js" }, + Tools = new List { "*" }, }, }, }); @@ -165,7 +169,6 @@ import { CopilotClient } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient(); - await client.start(); // Create session with filesystem MCP server const session = await client.createSession({ diff --git a/justfile b/justfile index 8b1af30c..8cf72c73 100644 --- a/justfile +++ b/justfile @@ -84,3 +84,36 @@ install: playground: @echo "=== Starting SDK Playground ===" @cd demos/playground && npm install && npm start + +# Validate documentation code examples +validate-docs: validate-docs-extract validate-docs-check + +# Extract code blocks from documentation +validate-docs-extract: + @echo "=== Extracting documentation code blocks ===" + @cd scripts/docs-validation && npm ci --silent && npm run extract + +# Validate all extracted code blocks +validate-docs-check: + @echo "=== Validating documentation code blocks ===" + @cd scripts/docs-validation && npm run validate + +# Validate only TypeScript documentation examples +validate-docs-ts: + @echo "=== Validating TypeScript documentation ===" + @cd scripts/docs-validation && npm run validate:ts + +# Validate only Python documentation examples +validate-docs-py: + @echo "=== Validating Python documentation ===" + @cd scripts/docs-validation && npm run validate:py + +# Validate only Go documentation examples +validate-docs-go: + @echo "=== Validating Go documentation ===" + @cd scripts/docs-validation && npm run validate:go + +# Validate only C# documentation examples +validate-docs-cs: + @echo "=== Validating C# documentation ===" + @cd scripts/docs-validation && npm run validate:cs diff --git a/scripts/docs-validation/.gitignore b/scripts/docs-validation/.gitignore new file mode 100644 index 00000000..c2658d7d --- /dev/null +++ b/scripts/docs-validation/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/scripts/docs-validation/extract.ts b/scripts/docs-validation/extract.ts new file mode 100644 index 00000000..2fc48d82 --- /dev/null +++ b/scripts/docs-validation/extract.ts @@ -0,0 +1,453 @@ +/** + * Extracts code blocks from markdown documentation files. + * Outputs individual files for validation by language-specific tools. + */ + +import * as fs from "fs"; +import * as path from "path"; +import { glob } from "glob"; + +const DOCS_DIR = path.resolve(import.meta.dirname, "../../docs"); +const OUTPUT_DIR = path.resolve(import.meta.dirname, "../../docs/.validation"); + +// Map markdown language tags to our canonical names +const LANGUAGE_MAP: Record = { + typescript: "typescript", + ts: "typescript", + javascript: "typescript", // Treat JS as TS for validation + js: "typescript", + python: "python", + py: "python", + go: "go", + golang: "go", + csharp: "csharp", + "c#": "csharp", + cs: "csharp", +}; + +interface CodeBlock { + language: string; + code: string; + file: string; + line: number; + skip: boolean; + wrapAsync: boolean; +} + +interface ExtractionManifest { + extractedAt: string; + blocks: { + id: string; + sourceFile: string; + sourceLine: number; + language: string; + outputFile: string; + }[]; +} + +function parseMarkdownCodeBlocks( + content: string, + filePath: string +): CodeBlock[] { + const blocks: CodeBlock[] = []; + const lines = content.split("\n"); + + let inCodeBlock = false; + let currentLang = ""; + let currentCode: string[] = []; + let blockStartLine = 0; + let skipNext = false; + let wrapAsync = false; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + // Check for validation directives + if (line.includes("")) { + skipNext = true; + continue; + } + if (line.includes("")) { + wrapAsync = true; + continue; + } + + // Start of code block + if (!inCodeBlock && line.startsWith("```")) { + const lang = line.slice(3).trim().toLowerCase(); + if (lang && LANGUAGE_MAP[lang]) { + inCodeBlock = true; + currentLang = LANGUAGE_MAP[lang]; + currentCode = []; + blockStartLine = i + 1; // 1-indexed line number + } + continue; + } + + // End of code block + if (inCodeBlock && line.startsWith("```")) { + blocks.push({ + language: currentLang, + code: currentCode.join("\n"), + file: filePath, + line: blockStartLine, + skip: skipNext, + wrapAsync: wrapAsync, + }); + inCodeBlock = false; + currentLang = ""; + currentCode = []; + skipNext = false; + wrapAsync = false; + continue; + } + + // Inside code block + if (inCodeBlock) { + currentCode.push(line); + } + } + + return blocks; +} + +function generateFileName( + block: CodeBlock, + index: number, + langCounts: Map +): string { + const count = langCounts.get(block.language) || 0; + langCounts.set(block.language, count + 1); + + const sourceBasename = path.basename(block.file, ".md"); + const ext = getExtension(block.language); + + return `${sourceBasename}_${count}${ext}`; +} + +function getExtension(language: string): string { + switch (language) { + case "typescript": + return ".ts"; + case "python": + return ".py"; + case "go": + return ".go"; + case "csharp": + return ".cs"; + default: + return ".txt"; + } +} + +/** + * Detect code fragments that can't be validated as standalone files. + * These are typically partial snippets showing configuration options + * or code that's meant to be part of a larger context. + */ +function shouldSkipFragment(block: CodeBlock): boolean { + const code = block.code.trim(); + + // TypeScript/JavaScript: Skip bare object literals (config snippets) + if (block.language === "typescript") { + // Starts with property: value pattern (e.g., "provider: {") + if (/^[a-zA-Z_]+\s*:\s*[\{\[]/.test(code)) { + return true; + } + // Starts with just an object/array that's not assigned + if (/^\{[\s\S]*\}$/.test(code) && !code.includes("import ") && !code.includes("export ")) { + return true; + } + } + + // Go: Skip fragments that are just type definitions without package + if (block.language === "go") { + // Function signatures without bodies (interface definitions shown in docs) + if (/^func\s+\w+\([^)]*\)\s*\([^)]*\)\s*$/.test(code)) { + return true; + } + } + + return false; +} + +function wrapCodeForValidation(block: CodeBlock): string { + let code = block.code; + + // Python: auto-detect async code and wrap if needed + if (block.language === "python") { + const hasAwait = /\bawait\b/.test(code); + const hasAsyncDef = /\basync\s+def\b/.test(code); + + // Check if await is used outside of any async def + // Simple heuristic: if await appears at column 0 or after assignment at column 0 + const lines = code.split("\n"); + let awaitOutsideFunction = false; + let inAsyncFunction = false; + let indentLevel = 0; + + for (const line of lines) { + const trimmed = line.trimStart(); + const leadingSpaces = line.length - trimmed.length; + + // Track if we're in an async function + if (trimmed.startsWith("async def ")) { + inAsyncFunction = true; + indentLevel = leadingSpaces; + } else if (inAsyncFunction && leadingSpaces <= indentLevel && trimmed && !trimmed.startsWith("#")) { + // Dedented back, we're out of the function + inAsyncFunction = false; + } + + // Check for await outside function + if (trimmed.includes("await ") && !inAsyncFunction) { + awaitOutsideFunction = true; + break; + } + } + + const needsWrap = block.wrapAsync || awaitOutsideFunction || (hasAwait && !hasAsyncDef); + + if (needsWrap) { + const indented = code + .split("\n") + .map((l) => " " + l) + .join("\n"); + code = `import asyncio\n\nasync def main():\n${indented}\n\nasyncio.run(main())`; + } + } + + // Go: ensure package declaration + if (block.language === "go" && !code.includes("package ")) { + code = `package main\n\n${code}`; + } + + // Go: add main function if missing and has statements outside functions + if (block.language === "go" && !code.includes("func main()")) { + // Check if code has statements that need to be in main + const hasStatements = /^[a-z]/.test(code.trim().split("\n").pop() || ""); + if (hasStatements) { + // This is a snippet, wrap it + const lines = code.split("\n"); + const packageLine = lines.find((l) => l.startsWith("package ")) || ""; + const imports = lines.filter( + (l) => l.startsWith("import ") || l.startsWith('import (') + ); + const rest = lines.filter( + (l) => + !l.startsWith("package ") && + !l.startsWith("import ") && + !l.startsWith("import (") && + !l.startsWith(")") && + !l.startsWith("\t") // import block lines + ); + + // Only wrap if there are loose statements (not type/func definitions) + const hasLooseStatements = rest.some( + (l) => + l.trim() && + !l.startsWith("type ") && + !l.startsWith("func ") && + !l.startsWith("//") && + !l.startsWith("var ") && + !l.startsWith("const ") + ); + + if (!hasLooseStatements) { + // Code has proper structure, just ensure it has a main + code = code + "\n\nfunc main() {}"; + } + } + } + + // C#: wrap in a class to avoid top-level statements conflicts + // (C# only allows one file with top-level statements per project) + if (block.language === "csharp") { + // Check if it's a complete file (has namespace or class) + const hasStructure = + code.includes("namespace ") || + code.includes("class ") || + code.includes("record ") || + code.includes("public delegate "); + + if (!hasStructure) { + // Extract any existing using statements + const lines = code.split("\n"); + const usings: string[] = []; + const rest: string[] = []; + + for (const line of lines) { + if (line.trim().startsWith("using ") && line.trim().endsWith(";")) { + usings.push(line); + } else { + rest.push(line); + } + } + + // Always ensure SDK using is present + if (!usings.some(u => u.includes("GitHub.Copilot.SDK"))) { + usings.push("using GitHub.Copilot.SDK;"); + } + + // Generate a unique class name based on block location + const className = `ValidationClass_${block.file.replace(/[^a-zA-Z0-9]/g, "_")}_${block.line}`; + + // Wrap in async method to support await + const hasAwait = code.includes("await "); + const indentedCode = rest.map(l => " " + l).join("\n"); + + if (hasAwait) { + code = `${usings.join("\n")} + +public static class ${className} +{ + public static async Task Main() + { +${indentedCode} + } +}`; + } else { + code = `${usings.join("\n")} + +public static class ${className} +{ + public static void Main() + { +${indentedCode} + } +}`; + } + } else { + // Has structure, but may still need using directive + if (!code.includes("using GitHub.Copilot.SDK;")) { + code = "using GitHub.Copilot.SDK;\n" + code; + } + } + } + + return code; +} + +async function main() { + console.log("📖 Extracting code blocks from documentation...\n"); + + // Clean output directory + if (fs.existsSync(OUTPUT_DIR)) { + fs.rmSync(OUTPUT_DIR, { recursive: true }); + } + fs.mkdirSync(OUTPUT_DIR, { recursive: true }); + + // Create language subdirectories + for (const lang of ["typescript", "python", "go", "csharp"]) { + fs.mkdirSync(path.join(OUTPUT_DIR, lang), { recursive: true }); + } + + // Find all markdown files + const mdFiles = await glob("**/*.md", { + cwd: DOCS_DIR, + ignore: [".validation/**", "node_modules/**", "IMPROVEMENT_PLAN.md"], + }); + + console.log(`Found ${mdFiles.length} markdown files\n`); + + const manifest: ExtractionManifest = { + extractedAt: new Date().toISOString(), + blocks: [], + }; + + const langCounts = new Map(); + let totalBlocks = 0; + let skippedBlocks = 0; + + for (const mdFile of mdFiles) { + const fullPath = path.join(DOCS_DIR, mdFile); + const content = fs.readFileSync(fullPath, "utf-8"); + const blocks = parseMarkdownCodeBlocks(content, mdFile); + + for (const block of blocks) { + if (block.skip) { + skippedBlocks++; + continue; + } + + // Skip empty or trivial blocks + if (block.code.trim().length < 10) { + continue; + } + + // Skip incomplete code fragments that can't be validated standalone + if (shouldSkipFragment(block)) { + skippedBlocks++; + continue; + } + + const fileName = generateFileName(block, totalBlocks, langCounts); + const outputPath = path.join(OUTPUT_DIR, block.language, fileName); + + const wrappedCode = wrapCodeForValidation(block); + + // Add source location comment + const sourceComment = getSourceComment( + block.language, + block.file, + block.line + ); + const finalCode = sourceComment + "\n" + wrappedCode; + + fs.writeFileSync(outputPath, finalCode); + + manifest.blocks.push({ + id: `${block.language}/${fileName}`, + sourceFile: block.file, + sourceLine: block.line, + language: block.language, + outputFile: `${block.language}/${fileName}`, + }); + + totalBlocks++; + } + } + + // Write manifest + fs.writeFileSync( + path.join(OUTPUT_DIR, "manifest.json"), + JSON.stringify(manifest, null, 2) + ); + + // Summary + console.log("Extraction complete!\n"); + console.log(" Language Count"); + console.log(" ─────────────────────"); + for (const [lang, count] of langCounts) { + console.log(` ${lang.padEnd(14)} ${count}`); + } + console.log(" ─────────────────────"); + console.log(` Total ${totalBlocks}`); + if (skippedBlocks > 0) { + console.log(` Skipped ${skippedBlocks}`); + } + console.log(`\nOutput: ${OUTPUT_DIR}`); +} + +function getSourceComment( + language: string, + file: string, + line: number +): string { + const location = `Source: ${file}:${line}`; + switch (language) { + case "typescript": + case "go": + case "csharp": + return `// ${location}`; + case "python": + return `# ${location}`; + default: + return `// ${location}`; + } +} + +main().catch((err) => { + console.error("Extraction failed:", err); + process.exit(1); +}); diff --git a/scripts/docs-validation/package-lock.json b/scripts/docs-validation/package-lock.json new file mode 100644 index 00000000..aa37d5df --- /dev/null +++ b/scripts/docs-validation/package-lock.json @@ -0,0 +1,1016 @@ +{ + "name": "docs-validation", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "docs-validation", + "version": "1.0.0", + "dependencies": { + "glob": "^11.0.0", + "tsx": "^4.19.0", + "typescript": "^5.7.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz", + "integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==", + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.1.tgz", + "integrity": "sha512-EoY1N2xCn44xU6750Sx7OjOIT59FkmstNc3X6y5xpz7D5cBtZRe/3pSlTkDJgqsOk3WwZPkWfonhhUJfttQo3w==", + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/lru-cache": { + "version": "11.2.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", + "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/minimatch": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", + "integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", + "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/scripts/docs-validation/package.json b/scripts/docs-validation/package.json new file mode 100644 index 00000000..976df1de --- /dev/null +++ b/scripts/docs-validation/package.json @@ -0,0 +1,19 @@ +{ + "name": "docs-validation", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "extract": "tsx extract.ts", + "validate": "tsx validate.ts", + "validate:ts": "tsx validate.ts --lang typescript", + "validate:py": "tsx validate.ts --lang python", + "validate:go": "tsx validate.ts --lang go", + "validate:cs": "tsx validate.ts --lang csharp" + }, + "dependencies": { + "glob": "^11.0.0", + "tsx": "^4.19.0", + "typescript": "^5.7.0" + } +} diff --git a/scripts/docs-validation/validate.ts b/scripts/docs-validation/validate.ts new file mode 100644 index 00000000..109430c8 --- /dev/null +++ b/scripts/docs-validation/validate.ts @@ -0,0 +1,478 @@ +/** + * Validates extracted documentation code blocks. + * Runs language-specific type/compile checks. + */ + +import * as fs from "fs"; +import * as path from "path"; +import { execSync, spawn } from "child_process"; +import { glob } from "glob"; + +const ROOT_DIR = path.resolve(import.meta.dirname, "../.."); +const VALIDATION_DIR = path.join(ROOT_DIR, "docs/.validation"); + +interface ValidationResult { + file: string; + sourceFile: string; + sourceLine: number; + success: boolean; + errors: string[]; +} + +interface Manifest { + blocks: { + id: string; + sourceFile: string; + sourceLine: number; + language: string; + outputFile: string; + }[]; +} + +function loadManifest(): Manifest { + const manifestPath = path.join(VALIDATION_DIR, "manifest.json"); + if (!fs.existsSync(manifestPath)) { + console.error( + "❌ No manifest found. Run extraction first: npm run extract" + ); + process.exit(1); + } + return JSON.parse(fs.readFileSync(manifestPath, "utf-8")); +} + +async function validateTypeScript(): Promise { + const results: ValidationResult[] = []; + const tsDir = path.join(VALIDATION_DIR, "typescript"); + const manifest = loadManifest(); + + if (!fs.existsSync(tsDir)) { + console.log(" No TypeScript files to validate"); + return results; + } + + // Create a temporary tsconfig for validation + const tsconfig = { + compilerOptions: { + target: "ES2022", + module: "NodeNext", + moduleResolution: "NodeNext", + strict: true, + skipLibCheck: true, + noEmit: true, + esModuleInterop: true, + allowSyntheticDefaultImports: true, + resolveJsonModule: true, + types: ["node"], + paths: { + "@github/copilot-sdk": [path.join(ROOT_DIR, "nodejs/src/index.ts")], + }, + }, + include: ["./**/*.ts"], + }; + + const tsconfigPath = path.join(tsDir, "tsconfig.json"); + fs.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2)); + + try { + // Run tsc + const tscPath = path.join(ROOT_DIR, "nodejs/node_modules/.bin/tsc"); + execSync(`${tscPath} --project ${tsconfigPath} 2>&1`, { + encoding: "utf-8", + cwd: tsDir, + }); + + // All files passed + const files = await glob("*.ts", { cwd: tsDir }); + for (const file of files) { + if (file === "tsconfig.json") continue; + const block = manifest.blocks.find( + (b) => b.outputFile === `typescript/${file}` + ); + results.push({ + file: `typescript/${file}`, + sourceFile: block?.sourceFile || "unknown", + sourceLine: block?.sourceLine || 0, + success: true, + errors: [], + }); + } + } catch (err: any) { + // Parse tsc output for errors + const output = err.stdout || err.message || ""; + const errorLines = output.split("\n"); + + // Group errors by file + const fileErrors = new Map(); + let currentFile = ""; + + for (const line of errorLines) { + const match = line.match(/^(.+\.ts)\((\d+),(\d+)\): error/); + if (match) { + currentFile = match[1]; + if (!fileErrors.has(currentFile)) { + fileErrors.set(currentFile, []); + } + fileErrors.get(currentFile)!.push(line); + } else if (currentFile && line.trim()) { + fileErrors.get(currentFile)?.push(line); + } + } + + // Create results + const files = await glob("*.ts", { cwd: tsDir }); + for (const file of files) { + if (file === "tsconfig.json") continue; + const fullPath = path.join(tsDir, file); + const block = manifest.blocks.find( + (b) => b.outputFile === `typescript/${file}` + ); + const errors = fileErrors.get(fullPath) || fileErrors.get(file) || []; + + results.push({ + file: `typescript/${file}`, + sourceFile: block?.sourceFile || "unknown", + sourceLine: block?.sourceLine || 0, + success: errors.length === 0, + errors, + }); + } + } + + return results; +} + +async function validatePython(): Promise { + const results: ValidationResult[] = []; + const pyDir = path.join(VALIDATION_DIR, "python"); + const manifest = loadManifest(); + + if (!fs.existsSync(pyDir)) { + console.log(" No Python files to validate"); + return results; + } + + const files = await glob("*.py", { cwd: pyDir }); + + for (const file of files) { + const fullPath = path.join(pyDir, file); + const block = manifest.blocks.find( + (b) => b.outputFile === `python/${file}` + ); + const errors: string[] = []; + + // Syntax check with py_compile + try { + execSync(`python3 -m py_compile "${fullPath}" 2>&1`, { + encoding: "utf-8", + }); + } catch (err: any) { + errors.push(err.stdout || err.message || "Syntax error"); + } + + // Type check with mypy (if available) + if (errors.length === 0) { + try { + execSync( + `python3 -m mypy "${fullPath}" --ignore-missing-imports --no-error-summary 2>&1`, + { encoding: "utf-8" } + ); + } catch (err: any) { + const output = err.stdout || err.message || ""; + // Filter out "Success" messages and notes + const typeErrors = output + .split("\n") + .filter( + (l: string) => + l.includes(": error:") && + !l.includes("Cannot find implementation") + ); + if (typeErrors.length > 0) { + errors.push(...typeErrors); + } + } + } + + results.push({ + file: `python/${file}`, + sourceFile: block?.sourceFile || "unknown", + sourceLine: block?.sourceLine || 0, + success: errors.length === 0, + errors, + }); + } + + return results; +} + +async function validateGo(): Promise { + const results: ValidationResult[] = []; + const goDir = path.join(VALIDATION_DIR, "go"); + const manifest = loadManifest(); + + if (!fs.existsSync(goDir)) { + console.log(" No Go files to validate"); + return results; + } + + // Create a go.mod for the validation directory + const goMod = `module docs-validation + +go 1.21 + +require github.com/github/copilot-sdk/go v0.0.0 + +replace github.com/github/copilot-sdk/go => ${path.join(ROOT_DIR, "go")} +`; + fs.writeFileSync(path.join(goDir, "go.mod"), goMod); + + // Run go mod tidy to fetch dependencies + try { + execSync(`go mod tidy 2>&1`, { + encoding: "utf-8", + cwd: goDir, + env: { ...process.env, GO111MODULE: "on" }, + }); + } catch (err: any) { + // go mod tidy might fail if there are syntax errors, continue anyway + } + + const files = await glob("*.go", { cwd: goDir }); + + // Try to compile each file individually + for (const file of files) { + const fullPath = path.join(goDir, file); + const block = manifest.blocks.find((b) => b.outputFile === `go/${file}`); + const errors: string[] = []; + + try { + // Use go vet for syntax and basic checks + execSync(`go build -o /dev/null "${fullPath}" 2>&1`, { + encoding: "utf-8", + cwd: goDir, + env: { ...process.env, GO111MODULE: "on" }, + }); + } catch (err: any) { + const output = err.stdout || err.stderr || err.message || ""; + errors.push( + ...output.split("\n").filter((l: string) => l.trim() && !l.startsWith("#")) + ); + } + + results.push({ + file: `go/${file}`, + sourceFile: block?.sourceFile || "unknown", + sourceLine: block?.sourceLine || 0, + success: errors.length === 0, + errors, + }); + } + + return results; +} + +async function validateCSharp(): Promise { + const results: ValidationResult[] = []; + const csDir = path.join(VALIDATION_DIR, "csharp"); + const manifest = loadManifest(); + + if (!fs.existsSync(csDir)) { + console.log(" No C# files to validate"); + return results; + } + + // Create a minimal csproj for validation + const csproj = ` + + Library + net8.0 + enable + enable + CS8019;CS0168;CS0219 + + + + +`; + + fs.writeFileSync(path.join(csDir, "DocsValidation.csproj"), csproj); + + const files = await glob("*.cs", { cwd: csDir }); + + // Compile all files together + try { + execSync(`dotnet build "${path.join(csDir, "DocsValidation.csproj")}" 2>&1`, { + encoding: "utf-8", + cwd: csDir, + }); + + // All files passed + for (const file of files) { + const block = manifest.blocks.find( + (b) => b.outputFile === `csharp/${file}` + ); + results.push({ + file: `csharp/${file}`, + sourceFile: block?.sourceFile || "unknown", + sourceLine: block?.sourceLine || 0, + success: true, + errors: [], + }); + } + } catch (err: any) { + const output = err.stdout || err.stderr || err.message || ""; + + // Parse errors by file + const fileErrors = new Map(); + + for (const line of output.split("\n")) { + const match = line.match(/([^/\\]+\.cs)\((\d+),(\d+)\): error/); + if (match) { + const fileName = match[1]; + if (!fileErrors.has(fileName)) { + fileErrors.set(fileName, []); + } + fileErrors.get(fileName)!.push(line); + } + } + + for (const file of files) { + const block = manifest.blocks.find( + (b) => b.outputFile === `csharp/${file}` + ); + const errors = fileErrors.get(file) || []; + + results.push({ + file: `csharp/${file}`, + sourceFile: block?.sourceFile || "unknown", + sourceLine: block?.sourceLine || 0, + success: errors.length === 0, + errors, + }); + } + } + + return results; +} + +function printResults(results: ValidationResult[], language: string): { failed: number; passed: number; failures: ValidationResult[] } { + const failed = results.filter((r) => !r.success); + const passed = results.filter((r) => r.success); + + if (failed.length === 0) { + console.log(` ✅ ${passed.length} files passed`); + return { failed: 0, passed: passed.length, failures: [] }; + } + + console.log(` ❌ ${failed.length} failed, ${passed.length} passed\n`); + + for (const result of failed) { + console.log(` ┌─ ${result.sourceFile}:${result.sourceLine}`); + console.log(` │ Extracted to: ${result.file}`); + for (const error of result.errors.slice(0, 5)) { + console.log(` │ ${error}`); + } + if (result.errors.length > 5) { + console.log(` │ ... and ${result.errors.length - 5} more errors`); + } + console.log(` └─`); + } + + return { failed: failed.length, passed: passed.length, failures: failed }; +} + +function writeGitHubSummary(summaryData: { language: string; passed: number; failed: number; failures: ValidationResult[] }[]) { + const summaryFile = process.env.GITHUB_STEP_SUMMARY; + if (!summaryFile) return; + + const totalPassed = summaryData.reduce((sum, d) => sum + d.passed, 0); + const totalFailed = summaryData.reduce((sum, d) => sum + d.failed, 0); + const allPassed = totalFailed === 0; + + let summary = `## 📖 Documentation Validation Results\n\n`; + + if (allPassed) { + summary += `✅ **All ${totalPassed} code blocks passed validation**\n\n`; + } else { + summary += `❌ **${totalFailed} failures** out of ${totalPassed + totalFailed} code blocks\n\n`; + } + + summary += `| Language | Status | Passed | Failed |\n`; + summary += `|----------|--------|--------|--------|\n`; + + for (const { language, passed, failed } of summaryData) { + const status = failed === 0 ? "✅" : "❌"; + summary += `| ${language} | ${status} | ${passed} | ${failed} |\n`; + } + + if (totalFailed > 0) { + summary += `\n### Failures\n\n`; + for (const { language, failures } of summaryData) { + if (failures.length === 0) continue; + summary += `#### ${language}\n\n`; + for (const f of failures) { + summary += `- **${f.sourceFile}:${f.sourceLine}**\n`; + summary += ` \`\`\`\n ${f.errors.slice(0, 3).join("\n ")}\n \`\`\`\n`; + } + } + } + + fs.appendFileSync(summaryFile, summary); +} + +async function main() { + const args = process.argv.slice(2); + const langArg = args.find((a) => a.startsWith("--lang=")); + const targetLang = langArg?.split("=")[1]; + + console.log("🔍 Validating documentation code blocks...\n"); + + if (!fs.existsSync(VALIDATION_DIR)) { + console.error("❌ No extracted code found. Run extraction first:"); + console.error(" npm run extract"); + process.exit(1); + } + + let totalFailed = 0; + const summaryData: { language: string; passed: number; failed: number; failures: ValidationResult[] }[] = []; + + const validators: [string, () => Promise][] = [ + ["TypeScript", validateTypeScript], + ["Python", validatePython], + ["Go", validateGo], + ["C#", validateCSharp], + ]; + + for (const [name, validator] of validators) { + const langKey = name.toLowerCase().replace("#", "sharp"); + if (targetLang && langKey !== targetLang) continue; + + console.log(`\n${name}:`); + const results = await validator(); + const { failed, passed, failures } = printResults(results, name); + totalFailed += failed; + summaryData.push({ language: name, passed, failed, failures }); + } + + // Write GitHub Actions summary + writeGitHubSummary(summaryData); + + console.log("\n" + "─".repeat(40)); + + if (totalFailed > 0) { + console.log(`\n❌ Validation failed: ${totalFailed} file(s) have errors`); + console.log("\nTo fix:"); + console.log(" 1. Check the error messages above"); + console.log(" 2. Update the code blocks in the markdown files"); + console.log(" 3. Re-run: npm run validate"); + console.log("\nTo skip a code block, add before it:"); + console.log(" "); + process.exit(1); + } + + console.log("\n✅ All documentation code blocks are valid!"); +} + +main().catch((err) => { + console.error("Validation failed:", err); + process.exit(1); +});