diff --git a/.github/workflows/generate-aidocs.yml b/.github/workflows/generate-aidocs.yml
new file mode 100644
index 0000000..f50ddba
--- /dev/null
+++ b/.github/workflows/generate-aidocs.yml
@@ -0,0 +1,190 @@
+name: Generate AI Docs
+
+# ──────────────────────────────────────────────────────────────────────────────
+# Triggers
+# ──────────────────────────────────────────────────────────────────────────────
+on:
+ workflow_dispatch:
+ inputs:
+ base_branch:
+ description: 'Base branch for the AI docs review PR (default: main)'
+ required: false
+ default: main
+ type: string
+ release_name:
+ description: 'Optional release label used in the review branch name (example: 2026.09.R1.PublicAPIService.docs)'
+ required: false
+ type: string
+ force_regenerate:
+ description: 'Force a full regeneration of the AI docs'
+ required: false
+ type: boolean
+ default: false
+
+# ──────────────────────────────────────────────────────────────────────────────
+# Jobs
+# ──────────────────────────────────────────────────────────────────────────────
+jobs:
+ generate-aidocs:
+ name: Generate AI Docs review branch
+ runs-on: windows-latest
+ permissions:
+ contents: write # push the review branch only; this workflow never creates PRs or merges
+
+ steps:
+ # ── 1. Checkout the release/base branch ─────────────────────────────────
+ - name: Checkout target branch
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ ref: ${{ inputs.base_branch || 'main' }}
+ token: ${{ secrets.GITHUB_TOKEN }}
+
+ # ── 2. Resolve branch metadata ───────────────────────────────────────────
+ - name: Resolve review metadata
+ id: meta
+ shell: pwsh
+ env:
+ INPUT_BASE: ${{ inputs.base_branch }}
+ INPUT_LABEL: ${{ inputs.release_name }}
+ run: |
+ $base = $env:INPUT_BASE
+ if ([string]::IsNullOrWhiteSpace($base)) { $base = 'main' }
+
+ $label = $env:INPUT_LABEL
+ if ([string]::IsNullOrWhiteSpace($label)) {
+ $label = "auto-$([DateTime]::UtcNow.ToString('yyyyMMdd-HHmmss'))"
+ }
+
+ # Must start alphanumeric so the value can't be parsed as a git option.
+ if ($base -notmatch '^[A-Za-z0-9][A-Za-z0-9._/-]*$') { throw "Invalid base_branch: '$base'" }
+ if ($label -notmatch '^[A-Za-z0-9][A-Za-z0-9._-]*$') { throw "Invalid release_name: '$label'" }
+
+ $branch = "ai-docs/$label"
+
+ "base=$base" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
+ "branch=$branch" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
+ "label=$label" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
+
+ Write-Host "Base branch : $base"
+ Write-Host "Review branch: $branch"
+
+ # ── 3. Create the AI docs review branch from the chosen base ────────────
+ - name: Create AI docs review branch
+ shell: pwsh
+ env:
+ BASE: ${{ steps.meta.outputs.base }}
+ BRANCH: ${{ steps.meta.outputs.branch }}
+ run: |
+ git checkout $env:BASE
+ git checkout -B $env:BRANCH
+
+ # ── 4. Download required tooling and SDK artifacts ───────────────────────
+ - name: Download C2M4AI
+ shell: pwsh
+ run: |
+ New-Item -ItemType Directory -Force -Path tools | Out-Null
+ .\scripts\ai-docs-scripts\Get-C2M4AI.ps1 -OutputPath tools\C2M4AI.exe
+
+ - name: Download .NET SDK XML documentation
+ shell: pwsh
+ run: .\scripts\ai-docs-scripts\Get-DotNetSdkXml.ps1 -OutputPath tools\Rws.LanguageCloud.Sdk.xml
+
+ - name: Download Java SDK sources
+ shell: pwsh
+ run: .\scripts\ai-docs-scripts\Get-JavaSdkSources.ps1 -OutputDir tools\java-api
+
+ # ── 5. Run the AI Docs Pipeline on the review branch ────────────────────
+ - name: Run AI Docs Pipeline
+ shell: pwsh
+ env:
+ FORCE: ${{ inputs.force_regenerate }}
+ run: |
+ $params = @{ RootDir = '.'; All = $true }
+ if ($env:FORCE -eq 'true') { $params.Force = $true }
+
+ .\scripts\ai-docs-scripts\Invoke-AiDocsPipeline.ps1 @params
+
+ # ── 6. Commit generated AI docs to the review branch ────────────────────
+ - name: Commit AI docs changes
+ id: commit
+ shell: pwsh
+ env:
+ LABEL: ${{ steps.meta.outputs.label }}
+ run: |
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+ git config user.name "github-actions[bot]"
+
+ git add articles/LCPublicAPI/aidocs/
+ $staged = git diff --cached --stat
+
+ if ($staged) {
+ git commit -m "docs: regenerate AI docs $($env:LABEL) [skip ci]"
+ "changed=true" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
+ Write-Host "AI docs branch updated with generated changes." -ForegroundColor Green
+ } else {
+ "changed=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
+ Write-Host "No AI docs changes were generated. Nothing to push." -ForegroundColor DarkGray
+ }
+
+ # ── 7. Push the review branch (no PR, no merge — a human does both) ────────
+ - name: Push review branch
+ if: steps.commit.outputs.changed == 'true'
+ shell: pwsh
+ env:
+ BRANCH: ${{ steps.meta.outputs.branch }}
+ run: git push origin $env:BRANCH
+
+ # ── 8. Tell the human what to do next ──────────────────────────────────────
+ - name: Write next-steps summary
+ shell: pwsh
+ env:
+ CHANGED: ${{ steps.commit.outputs.changed }}
+ BRANCH: ${{ steps.meta.outputs.branch }}
+ BASE: ${{ steps.meta.outputs.base }}
+ SERVER: ${{ github.server_url }}
+ REPO: ${{ github.repository }}
+ run: |
+ if ($env:CHANGED -ne 'true') {
+ "## AI docs: no changes`nNothing was generated, so no branch was pushed." | Out-File $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8
+ return
+ }
+
+ $compare = "$($env:SERVER)/$($env:REPO)/compare/$($env:BASE)...$($env:BRANCH)?expand=1"
+ $doc = "$($env:SERVER)/$($env:REPO)/blob/$($env:BRANCH)/scripts/ai-docs-scripts/README.md"
+
+ # Single-quoted here-string: backticks and code fences stay literal.
+ # __PLACEHOLDERS__ are replaced below.
+ $text = @'
+ ## AI docs branch is ready: `__BRANCH__`
+
+ The reference docs are done. The guides still need a person. **Do the steps in order and do not skip any.**
+
+ ### Steps
+
+ 1. Open this branch in **Open VS Code**.
+ 2. **Open the powershell terminal.** and run this command:
+ ```powershell
+ .\scripts\ai-docs-scripts\New-GuidesPrompt.ps1 -All
+ ```
+ You should see `Prompt written to: ...\_update-prompt.md` in green.
+
+ 3. **Run the AI agent.**
+ 1. Open Copilot Chat in Agent mode.
+ 2. Type `#_update-prompt.md` and press Enter.
+ 3. Wait until Copilot says it is finished (several minutes), then click **Keep**.
+
+ 4. **Rebuild the indexes.** Paste these two lines into the terminal, one at a time, pressing Enter after each:
+ ```powershell
+ .\scripts\ai-docs-scripts\Update-GuidesIndex.ps1
+ .\scripts\ai-docs-scripts\Update-AiDocsIndex.ps1
+ ```
+ Green `Updated` or grey `Unchanged` is fine. **Red means stop and ask the docs team.**
+
+ 5. **Final** - Review the changes, commit, push and open a pull request when ready.
+
+ More detail: [Reviewer guide in the README](__DOC__)
+ '@
+
+ $text = $text.Replace('__BRANCH__', $env:BRANCH).Replace('__BASE__', $env:BASE).Replace('__COMPARE__', $compare).Replace('__DOC__', $doc)
+ $text | Out-File $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8
diff --git a/.gitignore b/.gitignore
index 19ef2f3..9a01ceb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,3 +12,6 @@ _site
*.testlog
.vscode
.vs
+tools/
+_update-prompt.md
+.api-spec-hash
\ No newline at end of file
diff --git a/articles/LCPublicAPI/docs/AI-Development.md b/articles/LCPublicAPI/docs/AI-Development.md
new file mode 100644
index 0000000..00da0ac
--- /dev/null
+++ b/articles/LCPublicAPI/docs/AI-Development.md
@@ -0,0 +1,15 @@
+# AI Agent Development
+
+The Trados Cloud Platform API provides a machine-readable documentation set designed specifically for AI-assisted integrations.This section gives you and your tooling everything needed to understand and call the API correctly.
+
+## What are AI Docs?
+
+AI Docs are a structured documentation set derived from the live OpenAPI specification. They are designed to be consumed by AI agents.
+
+## Starting point for AI agents
+
+If you are wiring up an AI agent or a tool that needs to discover and call Trados Cloud Platform API endpoints, point it at the master index:
+
+**AI Docs - Master Index**
+
+The index lists every available operation and links directly to its detail page. From there, your agent can navigate to any endpoint it needs.
diff --git a/docfx.json b/docfx.json
index ccef19e..54f39ca 100644
--- a/docfx.json
+++ b/docfx.json
@@ -32,7 +32,8 @@
"*.md"
],
"exclude": [
- "_site/**"
+ "_site/**",
+ "articles/LCPublicAPI/aidocs/**"
]
}
],
@@ -47,6 +48,14 @@
"exclude": [
"_site/**"
]
+ },
+ {
+ "files": [
+ "articles/LCPublicAPI/aidocs/**"
+ ],
+ "exclude": [
+ "_site/**"
+ ]
}
],
"overwrite": [
diff --git a/scripts/ai-docs-scripts/Get-ApiSpecDiff.ps1 b/scripts/ai-docs-scripts/Get-ApiSpecDiff.ps1
new file mode 100644
index 0000000..e79f608
--- /dev/null
+++ b/scripts/ai-docs-scripts/Get-ApiSpecDiff.ps1
@@ -0,0 +1,104 @@
+<#
+.SYNOPSIS
+ Diffs two OpenAPI spec files and returns which operations were added,
+ removed, or changed (by content hash).
+
+.DESCRIPTION
+ Used by the pipeline orchestrator to decide which reference .md files
+ need to be regenerated or have their SDK sections re-injected.
+
+ Each operation is keyed by its operationId. The "changed" set contains
+ operations present in both specs whose full operation object differs.
+
+ Output is a JSON object with three arrays:
+ { "Added": [...], "Removed": [...], "Changed": [...] }
+
+.PARAMETER OldSpec
+ Path to the previous version of the OpenAPI JSON file.
+
+.PARAMETER NewSpec
+ Path to the current version of the OpenAPI JSON file.
+
+.PARAMETER OutputJson
+ Optional path to write the JSON diff result. If omitted, result is
+ written to stdout only.
+
+.EXAMPLE
+ .\scripts\ai-docs-scripts\Get-ApiSpecDiff.ps1 -OldSpec .\prev\api.json -NewSpec .\LCPublicAPI\api.json
+ .\scripts\ai-docs-scripts\Get-ApiSpecDiff.ps1 -OldSpec old.json -NewSpec new.json -OutputJson diff.json
+#>
+param(
+ [Parameter(Mandatory)][string] $OldSpec,
+ [Parameter(Mandatory)][string] $NewSpec,
+ [string] $OutputJson
+)
+
+Set-StrictMode -Version Latest
+$ErrorActionPreference = "Stop"
+
+function Get-OperationMap ([string]$specPath) {
+ $spec = Get-Content $specPath -Raw | ConvertFrom-Json
+ $ops = @{}
+
+ foreach ($pathProp in $spec.paths.PSObject.Properties) {
+ $pathStr = $pathProp.Name
+ foreach ($methodProp in $pathProp.Value.PSObject.Properties) {
+ # Skip non-HTTP fields (e.g. "parameters" at path level)
+ $httpMethods = 'get','post','put','patch','delete','head','options','trace'
+ if ($methodProp.Name -notin $httpMethods) { continue }
+
+ $operation = $methodProp.Value
+ $operationId = $operation.operationId
+ if (-not $operationId) { continue }
+
+ # Hash the full operation object for change detection
+ $json = $operation | ConvertTo-Json -Depth 20 -Compress
+ $bytes = [System.Text.Encoding]::UTF8.GetBytes($json)
+ $hash = [System.Security.Cryptography.SHA256]::Create().ComputeHash($bytes)
+ $hashStr = [System.BitConverter]::ToString($hash) -replace '-', ''
+
+ $ops[$operationId] = [PSCustomObject]@{
+ Path = $pathStr
+ Method = $methodProp.Name.ToUpper()
+ Hash = $hashStr
+ }
+ }
+ }
+ return $ops
+}
+
+Write-Host "=== Get-ApiSpecDiff ===" -ForegroundColor Cyan
+Write-Host "Old : $OldSpec"
+Write-Host "New : $NewSpec"
+Write-Host ""
+
+$old = Get-OperationMap $OldSpec
+$new = Get-OperationMap $NewSpec
+
+$added = @($new.Keys | Where-Object { -not $old.ContainsKey($_) } | Sort-Object)
+$removed = @($old.Keys | Where-Object { -not $new.ContainsKey($_) } | Sort-Object)
+$changed = @($new.Keys | Where-Object {
+ $old.ContainsKey($_) -and $old[$_].Hash -ne $new[$_].Hash
+} | Sort-Object)
+
+$result = [PSCustomObject]@{
+ Added = $added
+ Removed = $removed
+ Changed = $changed
+}
+
+# Report
+Write-Host "Added ($($added.Count)) : $($added -join ', ')"
+Write-Host "Removed ($($removed.Count)) : $($removed -join ', ')"
+Write-Host "Changed ($($changed.Count)) : $($changed -join ', ')"
+
+$json = $result | ConvertTo-Json -Depth 3
+
+if ($OutputJson) {
+ $json | Set-Content -Path $OutputJson -Encoding UTF8
+ Write-Host ""
+ Write-Host "Diff written to: $OutputJson" -ForegroundColor Cyan
+}
+
+# Always write to stdout so callers can capture via pipe or $( )
+Write-Output $json
diff --git a/scripts/ai-docs-scripts/Get-C2M4AI.ps1 b/scripts/ai-docs-scripts/Get-C2M4AI.ps1
new file mode 100644
index 0000000..4fc1a55
--- /dev/null
+++ b/scripts/ai-docs-scripts/Get-C2M4AI.ps1
@@ -0,0 +1,82 @@
+<#
+.SYNOPSIS
+ Downloads and extracts the Contract2Markdown4AI (C2M4AI) executable
+ from a GitHub release ZIP.
+
+.PARAMETER OutputPath
+ Destination path for the extracted executable.
+ Default: .\tools\C2M4AI.exe
+
+.PARAMETER DownloadUrl
+ Full URL of the release ZIP.
+ Default: v0.3.0 win-x64 release from github.com/haiduc32/Contract2Markdown4AI
+
+.PARAMETER Force
+ Re-download and overwrite even when the executable already exists.
+
+.EXAMPLE
+ .\scripts\ai-docs-scripts\Get-C2M4AI.ps1
+ .\scripts\ai-docs-scripts\Get-C2M4AI.ps1 -OutputPath .\tools\C2M4AI.exe
+ .\scripts\ai-docs-scripts\Get-C2M4AI.ps1 -DownloadUrl https://github.com/.../v0.4.0/...zip -Force
+#>
+param(
+ [string] $OutputPath = ".\tools\C2M4AI.exe",
+ [string] $DownloadUrl = "https://github.com/haiduc32/Contract2Markdown4AI/releases/download/v0.3.0/Contract2Markdown4AI-win-x64-0.3.0.zip",
+ [switch] $Force
+)
+
+Set-StrictMode -Version Latest
+$ErrorActionPreference = "Stop"
+
+# Skip if already present and not forced
+if (-not $Force -and (Test-Path $OutputPath)) {
+ Write-Host "C2M4AI.exe already present at: $OutputPath" -ForegroundColor DarkGray
+ Write-Output (Resolve-Path $OutputPath).Path
+ exit 0
+}
+
+# Ensure output directory exists
+$outDir = Split-Path $OutputPath -Parent
+if ($outDir -and -not (Test-Path $outDir)) {
+ New-Item -ItemType Directory -Path $outDir -Force | Out-Null
+}
+
+Write-Host "Downloading C2M4AI from:" -ForegroundColor Cyan
+Write-Host " $DownloadUrl"
+
+$tmpZip = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), [System.IO.Path]::GetRandomFileName() + ".zip")
+try {
+ Invoke-WebRequest -Uri $DownloadUrl -OutFile $tmpZip -UseBasicParsing
+
+ Add-Type -AssemblyName System.IO.Compression.FileSystem
+ $zip = [System.IO.Compression.ZipFile]::OpenRead($tmpZip)
+ try {
+ # Find the first .exe in the archive
+ $exeEntry = $zip.Entries | Where-Object { $_.Name -match '\.exe$' } | Select-Object -First 1
+ if (-not $exeEntry) {
+ throw "No .exe found inside the downloaded archive: $DownloadUrl"
+ }
+
+ Write-Host " Extracting: $($exeEntry.FullName) -> $OutputPath"
+ $outputDir = Split-Path $OutputPath -Parent
+ if (-not (Test-Path $outputDir)) {
+ New-Item -ItemType Directory -Path $outputDir | Out-Null
+ Write-Host " Created directory: $outputDir"
+ }
+ $entryStream = $exeEntry.Open()
+ $fileStream = [System.IO.File]::Create($OutputPath)
+ try {
+ $entryStream.CopyTo($fileStream)
+ } finally {
+ $fileStream.Close()
+ $entryStream.Close()
+ }
+ } finally {
+ $zip.Dispose()
+ }
+} finally {
+ if (Test-Path $tmpZip) { Remove-Item $tmpZip -Force }
+}
+
+Write-Host "C2M4AI.exe extracted to: $OutputPath" -ForegroundColor Green
+Write-Output (Resolve-Path $OutputPath).Path
diff --git a/scripts/ai-docs-scripts/Get-DotNetSdkXml.ps1 b/scripts/ai-docs-scripts/Get-DotNetSdkXml.ps1
new file mode 100644
index 0000000..28260a1
--- /dev/null
+++ b/scripts/ai-docs-scripts/Get-DotNetSdkXml.ps1
@@ -0,0 +1,115 @@
+<#
+.SYNOPSIS
+ Downloads the latest Rws.LanguageCloud.Sdk NuGet package and extracts
+ the XML documentation file needed by Update-SdkSections.ps1.
+
+.DESCRIPTION
+ Queries the NuGet flat-container API to discover the latest stable version,
+ downloads the .nupkg (which is a ZIP), and extracts the XML doc file from
+ lib/netstandard2.0/ (with fallback to lib/net*/). No reflection required.
+
+.PARAMETER PackageId
+ NuGet package identifier (lowercase). Default: rws.languagecloud.sdk
+
+.PARAMETER OutputPath
+ Destination path for the extracted XML documentation file.
+ Default: .\tools\Rws.LanguageCloud.Sdk.xml
+
+.PARAMETER Version
+ Specific package version to download. If empty, the latest stable version
+ is auto-discovered from the NuGet flat-container index.
+
+.PARAMETER Force
+ Re-download even when the XML file is already present at the correct version.
+
+.EXAMPLE
+ .\scripts\ai-docs-scripts\Get-DotNetSdkXml.ps1
+ .\scripts\ai-docs-scripts\Get-DotNetSdkXml.ps1 -OutputPath .\tools\sdk.xml
+ .\scripts\ai-docs-scripts\Get-DotNetSdkXml.ps1 -Version 2.5.0
+#>
+param(
+ [string] $PackageId = "rws.languagecloud.sdk",
+ [string] $OutputPath = ".\tools\Rws.LanguageCloud.Sdk.xml",
+ [string] $Version = "",
+ [switch] $Force
+)
+
+Set-StrictMode -Version Latest
+$ErrorActionPreference = "Stop"
+
+# ── Discover latest stable version ────────────────────────────────────────────
+if (-not $Version) {
+ $indexUrl = "https://api.nuget.org/v3-flatcontainer/$PackageId/index.json"
+ Write-Host "Querying NuGet for latest version of $PackageId ..." -ForegroundColor Cyan
+ $index = Invoke-RestMethod -Uri $indexUrl -UseBasicParsing
+ # Filter out pre-release versions (those containing a hyphen)
+ $stable = $index.versions | Where-Object { $_ -notmatch '-' }
+ $Version = $stable | Select-Object -Last 1
+ if (-not $Version) { throw "No stable version found for package: $PackageId" }
+}
+
+Write-Host " Target version : $Version"
+
+# ── Version cache: skip if already extracted at this version ──────────────────
+$markerPath = $OutputPath + ".version"
+if (-not $Force -and (Test-Path $OutputPath) -and (Test-Path $markerPath)) {
+ $cached = (Get-Content $markerPath -Raw).Trim()
+ if ($cached -eq $Version) {
+ Write-Host "NuGet XML already present at version $Version -- skipping download." -ForegroundColor DarkGray
+ Write-Output (Resolve-Path $OutputPath).Path
+ exit 0
+ }
+}
+
+# ── Ensure output directory exists ────────────────────────────────────────────
+$outDir = Split-Path $OutputPath -Parent
+if ($outDir -and -not (Test-Path $outDir)) {
+ New-Item -ItemType Directory -Path $outDir -Force | Out-Null
+}
+
+# ── Download .nupkg ───────────────────────────────────────────────────────────
+$downloadUrl = "https://api.nuget.org/v3-flatcontainer/$PackageId/$Version/$PackageId.$Version.nupkg"
+Write-Host " Downloading: $downloadUrl"
+
+$tmpPkg = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), [System.IO.Path]::GetRandomFileName() + ".nupkg")
+try {
+ Invoke-WebRequest -Uri $downloadUrl -OutFile $tmpPkg -UseBasicParsing
+
+ # ── Extract XML documentation file ────────────────────────────────────────
+ Add-Type -AssemblyName System.IO.Compression.FileSystem
+ $zip = [System.IO.Compression.ZipFile]::OpenRead($tmpPkg)
+ try {
+ # Prefer netstandard2.0, then any net* TFM
+ $xmlEntry = $zip.Entries |
+ Where-Object { $_.FullName -match 'lib/netstandard2\.0/.*\.xml$' } |
+ Select-Object -First 1
+ if (-not $xmlEntry) {
+ $xmlEntry = $zip.Entries |
+ Where-Object { $_.FullName -match '^lib/net[^/]+/.*\.xml$' } |
+ Select-Object -First 1
+ }
+ if (-not $xmlEntry) {
+ throw "No XML documentation file found inside NuGet package $PackageId $Version"
+ }
+
+ Write-Host " Extracting : $($xmlEntry.FullName) → $OutputPath"
+ $entryStream = $xmlEntry.Open()
+ $fileStream = [System.IO.File]::Create($OutputPath)
+ try {
+ $entryStream.CopyTo($fileStream)
+ } finally {
+ $fileStream.Close()
+ $entryStream.Close()
+ }
+ } finally {
+ $zip.Dispose()
+ }
+} finally {
+ if (Test-Path $tmpPkg) { Remove-Item $tmpPkg -Force }
+}
+
+# ── Write version marker ──────────────────────────────────────────────────────
+$Version | Set-Content $markerPath -NoNewline
+
+Write-Host "NuGet XML extracted: $OutputPath (version $Version)" -ForegroundColor Green
+Write-Output (Resolve-Path $OutputPath).Path
diff --git a/scripts/ai-docs-scripts/Get-JavaSdkSources.ps1 b/scripts/ai-docs-scripts/Get-JavaSdkSources.ps1
new file mode 100644
index 0000000..3456dee
--- /dev/null
+++ b/scripts/ai-docs-scripts/Get-JavaSdkSources.ps1
@@ -0,0 +1,129 @@
+<#
+.SYNOPSIS
+ Downloads the latest lc-public-api-sdk sources JAR from Maven Central
+ and extracts the *Api.java interface files needed by Update-SdkSections.ps1.
+
+.DESCRIPTION
+ Queries the Maven Central repository metadata for the latest release version,
+ downloads the -sources.jar (which is a ZIP), and extracts all *Api.java files
+ from com/rws/lt/lc/publicapi/sdk/api/ into the output directory (flat — no
+ subdirectories). The extracted files are identical in structure to the
+ SDK_JAVA/api/ directory used by the existing local-source parser.
+
+.PARAMETER GroupId
+ Maven groupId (dot-separated). Default: com.rws.lt.lc.public-api
+
+.PARAMETER ArtifactId
+ Maven artifactId. Default: lc-public-api-sdk
+
+.PARAMETER OutputDir
+ Directory to write extracted *Api.java files into.
+ Default: .\tools\java-api
+
+.PARAMETER Version
+ Specific artifact version. If empty, the latest release version is
+ auto-discovered from maven-metadata.xml.
+
+.PARAMETER Force
+ Re-download even when the output directory is already at the correct version.
+
+.EXAMPLE
+ .\scripts\ai-docs-scripts\Get-JavaSdkSources.ps1
+ .\scripts\ai-docs-scripts\Get-JavaSdkSources.ps1 -OutputDir .\tools\java-api
+ .\scripts\ai-docs-scripts\Get-JavaSdkSources.ps1 -Version 25.0.10
+#>
+param(
+ [string] $GroupId = "com.rws.lt.lc.public-api",
+ [string] $ArtifactId = "lc-public-api-sdk",
+ [string] $OutputDir = ".\tools\java-api",
+ [string] $Version = "",
+ [switch] $Force
+)
+
+Set-StrictMode -Version Latest
+$ErrorActionPreference = "Stop"
+
+# Convert groupId dots to path separators
+$groupPath = $GroupId -replace '\.', '/'
+
+# ── Discover latest release version ───────────────────────────────────────────
+if (-not $Version) {
+ $metaUrl = "https://repo1.maven.org/maven2/$groupPath/$ArtifactId/maven-metadata.xml"
+ Write-Host "Querying Maven Central for latest version of ${GroupId}:$ArtifactId ..." -ForegroundColor Cyan
+ $metaContent = (Invoke-WebRequest -Uri $metaUrl -UseBasicParsing).Content
+ [xml]$meta = $metaContent
+ $Version = $meta.metadata.versioning.release
+ if (-not $Version) { $Version = $meta.metadata.versioning.latest }
+ if (-not $Version) { throw "Could not determine latest version from: $metaUrl" }
+}
+
+Write-Host " Target version : $Version"
+
+# ── Version cache: skip if already extracted at this version ──────────────────
+$markerPath = Join-Path $OutputDir ".version"
+if (-not $Force -and (Test-Path $markerPath)) {
+ $cached = (Get-Content $markerPath -Raw).Trim()
+ if ($cached -eq $Version) {
+ Write-Host "Java SDK sources already present at version $Version -- skipping download." -ForegroundColor DarkGray
+ Write-Output (Resolve-Path $OutputDir).Path
+ exit 0
+ }
+}
+
+# ── Download sources JAR ───────────────────────────────────────────────────────
+$jarName = "$ArtifactId-$Version-sources.jar"
+$downloadUrl = "https://repo1.maven.org/maven2/$groupPath/$ArtifactId/$Version/$jarName"
+Write-Host " Downloading: $downloadUrl"
+
+$tmpJar = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), [System.IO.Path]::GetRandomFileName() + ".jar")
+try {
+ Invoke-WebRequest -Uri $downloadUrl -OutFile $tmpJar -UseBasicParsing
+
+ # ── Prepare output directory (clean slate) ─────────────────────────────────
+ if (Test-Path $OutputDir) {
+ # Remove only .java files so the version marker survives while we overwrite
+ Get-ChildItem $OutputDir -Filter "*.java" | Remove-Item -Force
+ } else {
+ New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
+ }
+
+ # ── Extract *Api.java files from the api/ package ─────────────────────────
+ Add-Type -AssemblyName System.IO.Compression.FileSystem
+ $zip = [System.IO.Compression.ZipFile]::OpenRead($tmpJar)
+ try {
+ # Match entries under the api/ directory that end in Api.java
+ $apiPattern = 'com/rws/lt/lc/publicapi/sdk/api/\w+Api\.java$'
+ $extracted = 0
+
+ foreach ($entry in $zip.Entries) {
+ if ($entry.FullName -notmatch $apiPattern) { continue }
+
+ $destPath = Join-Path $OutputDir $entry.Name
+ $entryStream = $entry.Open()
+ $fileStream = [System.IO.File]::Create($destPath)
+ try {
+ $entryStream.CopyTo($fileStream)
+ } finally {
+ $fileStream.Close()
+ $entryStream.Close()
+ }
+ $extracted++
+ }
+
+ if ($extracted -eq 0) {
+ throw "No *Api.java files matching '$apiPattern' found in sources JAR"
+ }
+
+ Write-Host " Extracted $extracted *Api.java files → $OutputDir"
+ } finally {
+ $zip.Dispose()
+ }
+} finally {
+ if (Test-Path $tmpJar) { Remove-Item $tmpJar -Force }
+}
+
+# ── Write version marker ──────────────────────────────────────────────────────
+$Version | Set-Content $markerPath -NoNewline
+
+Write-Host "Java SDK sources ready: $OutputDir (version $Version)" -ForegroundColor Green
+Write-Output (Resolve-Path $OutputDir).Path
diff --git a/scripts/ai-docs-scripts/Invoke-AiDocsPipeline.ps1 b/scripts/ai-docs-scripts/Invoke-AiDocsPipeline.ps1
new file mode 100644
index 0000000..1429b00
--- /dev/null
+++ b/scripts/ai-docs-scripts/Invoke-AiDocsPipeline.ps1
@@ -0,0 +1,303 @@
+<#
+.SYNOPSIS
+ Orchestrates the deterministic AI docs generation pipeline.
+
+.DESCRIPTION
+ Accepts a list of changed files (e.g. from a git diff on a PR) and
+ routes each change to the appropriate deterministic script. Only the
+ parts of the documentation that are affected by the changes are
+ regenerated — making the pipeline fast and idempotent.
+
+ DETERMINISTIC STAGES (all run via scripts, no AI required):
+ 1. Reference generation — C2M4AI.exe from api.json
+ 2. SDK section injection — Update-SdkSections.ps1 from SDK source
+ 3. Guide index rebuild — Update-GuidesIndex.ps1 from guide files
+ 4. Removed operation cleanup — delete .md files for removed operations
+
+ AI-REQUIRED STAGES (flagged but not executed by this script):
+ A. Guide content update — when docs/guides source files change
+
+ CHANGE ROUTING:
+ Changed file → Stage(s) triggered
+ ─────────────────────────────────────────────────────────────────
+ articles/LCPublicAPI/api/Public-API.v1.json → 1 (C2M4AI) + 2 (SDK)
+ articles/LCPublicAPI/aidocs/guides/*.md → 3 (guide index rebuild)
+ docs/guides/*.md → 3 + [A] flag AI update
+
+ If no ChangedFiles are provided (or -All is set), all stages run.
+
+.PARAMETER RootDir
+ Workspace root. Default: current directory.
+
+.PARAMETER ChangedFiles
+ List of relative file paths that changed (from git diff --name-only).
+ Example: @("articles/LCPublicAPI/api/Public-API.v1.json")
+
+.PARAMETER All
+ Ignore ChangedFiles and regenerate everything.
+
+.PARAMETER Force
+ Pass -Force to all sub-scripts (rewrite even when content is unchanged).
+
+.PARAMETER DryRun
+ Print what would run without executing any scripts.
+
+.PARAMETER DotNetXmlFile
+ Path to the NuGet XML documentation file for the .NET SDK
+ (e.g. tools\Rws.LanguageCloud.Sdk.xml downloaded by Get-DotNetSdkXml.ps1).
+ When provided, Update-SdkSections.ps1 uses the XML parser instead of
+ the C# source parser. Overrides the default SDK_NET path.
+
+.PARAMETER JavaApiDirPath
+ Override path to the directory containing *Api.java source files.
+ Default: {RootDir}\SDK_JAVA\api
+ In CI: set to the directory produced by Get-JavaSdkSources.ps1.
+
+.PARAMETER C2M4AIExePath
+ Override path to the C2M4AI.exe binary.
+ Default: {RootDir}\C2M4AI.exe
+ In CI: set to the path produced by Get-C2M4AI.ps1.
+
+.PARAMETER OldApiSpec
+ Path to the previous version of api.json, used by Get-ApiSpecDiff.ps1
+ to produce a targeted list of changed operations. If omitted, all
+ reference files are processed when the spec changes.
+
+.EXAMPLE
+ # Full regeneration
+ .\scripts\ai-docs-scripts\Invoke-AiDocsPipeline.ps1 -All
+
+ # Incremental update
+ $changed = git diff --name-only HEAD~1 HEAD
+ .\scripts\ai-docs-scripts\Invoke-AiDocsPipeline.ps1 -ChangedFiles $changed
+
+ # Dry run to preview
+ .\scripts\ai-docs-scripts\Invoke-AiDocsPipeline.ps1 -All -DryRun
+#>
+param(
+ [string] $RootDir = ".",
+ [string[]] $ChangedFiles = @(),
+ [switch] $All,
+ [switch] $Force,
+ [switch] $DryRun,
+ [string] $OldApiSpec = ""
+)
+
+Set-StrictMode -Version Latest
+$ErrorActionPreference = "Stop"
+
+# Ensure ChangedFiles is always a true array (git output can be a scalar string)
+$ChangedFiles = @($ChangedFiles | Where-Object { $_ })
+
+$ScriptDir = $PSScriptRoot
+
+# ── Resolve absolute paths ─────────────────────────────────────────────────────
+$ApiContract = Join-Path $RootDir "articles\LCPublicAPI\api\Public-API.v1.json"
+$ReferenceDir = Join-Path $RootDir "articles\LCPublicAPI\aidocs\reference"
+$GuidesDir = Join-Path $RootDir "articles\LCPublicAPI\aidocs\guides"
+$ToolsDir = Join-Path $RootDir "tools"
+$C2M4AIExe = Join-Path $ToolsDir "C2M4AI.exe"
+$DotNetXmlFile = Join-Path $ToolsDir "Rws.LanguageCloud.Sdk.xml"
+$JavaApiDir = Join-Path $ToolsDir "java-api"
+
+# ── Helper: run or simulate a script ──────────────────────────────────────────
+function Invoke-Stage ([string]$label, [string]$script, [hashtable]$params) {
+ Write-Host ""
+ Write-Host "--- $label ---" -ForegroundColor Yellow
+
+ if ($DryRun) {
+ Write-Host "[DRY RUN] Would call: $script" -ForegroundColor DarkGray
+ foreach ($kv in $params.GetEnumerator()) {
+ Write-Host " -$($kv.Key) $($kv.Value)" -ForegroundColor DarkGray
+ }
+ return
+ }
+
+ & $script @params
+ if ($LASTEXITCODE -and $LASTEXITCODE -ne 0) {
+ Write-Error "$label failed with exit code $LASTEXITCODE"
+ exit $LASTEXITCODE
+ }
+}
+
+# ── Auto-detect changed files when none supplied and -All not set ──────────────
+if (-not $All -and $ChangedFiles.Count -eq 0) {
+ Write-Host "No -ChangedFiles supplied -- detecting changes via git diff HEAD~1 HEAD ..." -ForegroundColor DarkGray
+ Push-Location $RootDir
+ try { $ChangedFiles = @(git diff --name-only HEAD~1 HEAD | Where-Object { $_ }) }
+ finally { Pop-Location }
+ if ($ChangedFiles.Count -eq 0) {
+ Write-Host " No changes detected. Nothing to do." -ForegroundColor DarkGray
+ } else {
+ Write-Host " Detected $($ChangedFiles.Count) changed file(s)." -ForegroundColor DarkGray
+ }
+}
+
+# ── Determine what changed ─────────────────────────────────────────────────────
+$normalised = $ChangedFiles | ForEach-Object { $_ -replace '\\', '/' }
+
+$apiChanged = $All -or ($normalised | Where-Object { $_ -match '(?i)articles/LCPublicAPI/api/Public-API\.v1\.json$' })
+$guideSrcChg = @($normalised | Where-Object { $_ -match '(?i)articles/LCPublicAPI/docs/.*\.md$' })
+$guideDocChg = @($normalised | Where-Object { $_ -match '(?i)articles/LCPublicAPI/aidocs/guides/.*\.md$' })
+
+# ── Banner ─────────────────────────────────────────────────────────────────────
+Write-Host ""
+Write-Host "#======================================#" -ForegroundColor Cyan
+Write-Host "# AI Docs Pipeline -- Deterministic #" -ForegroundColor Cyan
+Write-Host "#======================================#" -ForegroundColor Cyan
+Write-Host ""
+Write-Host "Root : $RootDir"
+Write-Host "Changed files : $($ChangedFiles.Count) (use -All to ignore)"
+if ($DryRun) { Write-Host "Mode : DRY RUN" -ForegroundColor Magenta }
+if ($Force) { Write-Host "Mode : FORCE" -ForegroundColor Yellow }
+Write-Host ""
+Write-Host "Triggers:"
+Write-Host " API spec changed : $([bool]$apiChanged)"
+Write-Host " Guide source changed : $($guideSrcChg.Count) file(s)"
+Write-Host " Guide doc changed : $($guideDocChg.Count) file(s)"
+
+# ── Guard: nothing to do? ──────────────────────────────────────────────────────
+if (-not $All -and -not $apiChanged -and
+ -not $guideSrcChg -and -not $guideDocChg -and $ChangedFiles.Count -gt 0) {
+ Write-Host ""
+ Write-Host "No relevant changes detected. Nothing to do." -ForegroundColor DarkGray
+ exit 0
+}
+
+# ─────────────────────────────────────────────────────────────────────────────
+# STAGE 0 — Fetch tools and SDK packages from public registries
+# Runs unconditionally. The individual scripts cache by version, so
+# subsequent runs complete in <1 s when the version hasn’t changed.
+# ─────────────────────────────────────────────────────────────────────────────
+Write-Host ""
+Write-Host "--- Stage 0 -- Fetch tools and SDK packages ---" -ForegroundColor Yellow
+
+if (-not $DryRun) {
+ & "$ScriptDir\Get-C2M4AI.ps1" -OutputPath $C2M4AIExe
+ & "$ScriptDir\Get-DotNetSdkXml.ps1" -OutputPath $DotNetXmlFile
+ & "$ScriptDir\Get-JavaSdkSources.ps1" -OutputDir $JavaApiDir
+} else {
+ Write-Host "[DRY RUN] Would run: Get-C2M4AI.ps1 / Get-DotNetSdkXml.ps1 / Get-JavaSdkSources.ps1" -ForegroundColor DarkGray
+}
+
+# ─────────────────────────────────────────────────────────────────────────────
+# STAGE 1 — Reference generation (C2M4AI)
+# ─────────────────────────────────────────────────────────────────────────────
+if ($apiChanged) {
+ $c2mParams = @{
+ ApiContract = $ApiContract
+ OutputDir = $ReferenceDir
+ C2M4AIExe = $C2M4AIExe
+ }
+ if ($Force) { $c2mParams['Force'] = $true }
+
+ Invoke-Stage "Stage 1 — Reference generation (C2M4AI)" `
+ "$ScriptDir\Invoke-C2M4AI.ps1" `
+ $c2mParams
+
+ # If we have an old spec, diff it to find targeted operations for Stage 2
+ if ($OldApiSpec -and (Test-Path $OldApiSpec) -and -not $All) {
+ Write-Host ""
+ Write-Host " Diffing specs for targeted SDK update..." -ForegroundColor DarkGray
+ $diffJson = & "$ScriptDir\Get-ApiSpecDiff.ps1" -OldSpec $OldApiSpec -NewSpec $ApiContract
+ $diff = $diffJson | ConvertFrom-Json
+ $targetOps = @($diff.Added) + @($diff.Changed)
+ Write-Host " Targeted operations: $($targetOps.Count)"
+ } else {
+ $targetOps = @() # empty = process all in Stage 2
+ }
+}
+
+# ─────────────────────────────────────────────────────────────────────────────
+# STAGE 2 — SDK section injection
+# ─────────────────────────────────────────────────────────────────────────────
+if ($apiChanged -or $All) {
+ $sdkParams = @{
+ ReferenceDir = $ReferenceDir
+ DotNetXmlFile = $DotNetXmlFile
+ JavaApiDir = $JavaApiDir
+ }
+ if ($Force) { $sdkParams['Force'] = $true }
+ if ($targetOps -and $targetOps.Count -gt 0) { $sdkParams['Operations'] = $targetOps }
+
+ Invoke-Stage "Stage 2 — SDK section injection" `
+ "$ScriptDir\Update-SdkSections.ps1" `
+ $sdkParams
+}
+
+# ─────────────────────────────────────────────────────────────────────────────
+# STAGE 3 — Guide index rebuild
+# ─────────────────────────────────────────────────────────────────────────────
+if (($guideSrcChg -or $guideDocChg -or $All) -and (Test-Path $GuidesDir)) {
+ Invoke-Stage "Stage 3 — Guide index rebuild" `
+ "$ScriptDir\Update-GuidesIndex.ps1" `
+ @{ GuidesDir = $GuidesDir }
+} elseif ($guideSrcChg -or $guideDocChg -or $All) {
+ Write-Host ""
+ Write-Host "--- Stage 3 -- Guide index rebuild ---" -ForegroundColor Yellow
+ Write-Host " Skipped: guides directory does not exist yet ($GuidesDir)" -ForegroundColor DarkGray
+}
+
+# ─────────────────────────────────────────────────────────────────────────────
+# STAGE 4 — Removed operation cleanup
+# (only when we have a spec diff available)
+# ─────────────────────────────────────────────────────────────────────────────
+if ($apiChanged -and $OldApiSpec -and (Test-Path $OldApiSpec) -and -not $All) {
+ if ($diff -and $diff.Removed -and $diff.Removed.Count -gt 0) {
+ Write-Host ""
+ Write-Host "--- Stage 4 -- Remove deleted operations ---" -ForegroundColor Yellow
+ foreach ($opId in $diff.Removed) {
+ $target = Join-Path $ReferenceDir "$opId.md"
+ if (Test-Path $target) {
+ if ($DryRun) {
+ Write-Host " [DRY RUN] Would delete: $target" -ForegroundColor DarkGray
+ } else {
+ Remove-Item $target
+ Write-Host " [deleted] $opId.md" -ForegroundColor Red
+ }
+ }
+ }
+ }
+}
+
+# ─────────────────────────────────────────────────────────────────────────────
+# STAGE A — Guide content update (AI-assisted, human-driven)
+# Source docs changed → emit advisory with command to generate the prompt
+# ─────────────────────────────────────────────────────────────────────────────
+if ($guideSrcChg.Count -gt 0 -or ($All -and -not $DryRun)) {
+ Write-Host ""
+ Write-Host "--- Stage A -- Guide content update [AI REQUIRED] ---" -ForegroundColor Magenta
+ Write-Host ""
+ if ($guideSrcChg.Count -gt 0) {
+ Write-Host " Source docs changed:" -ForegroundColor Magenta
+ foreach ($f in $guideSrcChg) {
+ Write-Host " · $f" -ForegroundColor Magenta
+ }
+ Write-Host ""
+ $changedArgs = ($guideSrcChg | ForEach-Object { "`"$_`"" }) -join ', '
+ Write-Host " Run to generate an incremental Copilot prompt:" -ForegroundColor Yellow
+ Write-Host " .\scripts\ai-docs-scripts\New-GuidesPrompt.ps1 -ChangedSources @($changedArgs)" -ForegroundColor White
+ } else {
+ Write-Host " Run to generate a full-refresh Copilot prompt:" -ForegroundColor Yellow
+ Write-Host " .\scripts\ai-docs-scripts\New-GuidesPrompt.ps1 -All" -ForegroundColor White
+ }
+ Write-Host ""
+ Write-Host " Then open _update-prompt.md in VS Code and run it in Copilot agent mode." -ForegroundColor DarkGray
+}
+
+# ─────────────────────────────────────────────────────────────────────────────
+# STAGE 5 — Master aidocs/index.md rebuild
+# Runs whenever reference or guides content may have changed.
+# ─────────────────────────────────────────────────────────────────────────────
+if ($apiChanged -or $guideSrcChg -or $guideDocChg -or $All) {
+ $AiDocsDir = Join-Path $RootDir "articles\LCPublicAPI\aidocs"
+ Invoke-Stage "Stage 5 — Master AI docs index" `
+ "$ScriptDir\Update-AiDocsIndex.ps1" `
+ @{ AiDocsDir = $AiDocsDir }
+}
+
+Write-Host ""
+Write-Host "======================================" -ForegroundColor Cyan
+Write-Host " Pipeline complete." -ForegroundColor Cyan
+Write-Host "======================================" -ForegroundColor Cyan
+Write-Host ""
diff --git a/scripts/ai-docs-scripts/Invoke-C2M4AI.ps1 b/scripts/ai-docs-scripts/Invoke-C2M4AI.ps1
new file mode 100644
index 0000000..cadad8a
--- /dev/null
+++ b/scripts/ai-docs-scripts/Invoke-C2M4AI.ps1
@@ -0,0 +1,108 @@
+<#
+.SYNOPSIS
+ Runs C2M4AI.exe to generate reference markdown from an OpenAPI spec.
+ Uses SHA-256 hashing for change detection — skips execution if spec unchanged.
+
+.PARAMETER ApiContract
+ Path to the OpenAPI JSON or YAML file.
+ Default: .\articles\LCPublicAPI\api\Public-API.v1.json
+
+.PARAMETER OutputDir
+ Path where reference .md files are written. Default: .\articles\LCPublicAPI\aidocs\reference
+
+.PARAMETER C2M4AIExe
+ Path to C2M4AI.exe. Default: .\tools\C2M4AI.exe
+ If the file does not exist at this path, run Get-C2M4AI.ps1 first
+ (or use Invoke-AiDocsPipeline.ps1 which does this automatically).
+
+.PARAMETER NoSchema
+ Pass --no-schema to C2M4AI to suppress generation of Schemas.md.
+ Defaults to true — schemas are already inlined in each operation file.
+
+.PARAMETER Force
+ Run C2M4AI even if the spec hash has not changed.
+
+.OUTPUTS
+ Writes the new spec hash to /.api-spec-hash after a
+ successful run. Exits with code 0 on success, non-zero on failure.
+
+.EXAMPLE
+ .\scripts\ai-docs-scripts\Invoke-C2M4AI.ps1
+ .\scripts\ai-docs-scripts\Invoke-C2M4AI.ps1 -ApiContract .\articles\LCPublicAPI\api\Public-API.v1.json -Force
+#>
+param(
+ [string] $ApiContract = ".\articles\LCPublicAPI\api\Public-API.v1.json",
+ [string] $OutputDir = ".\articles\LCPublicAPI\aidocs\reference",
+ [string] $C2M4AIExe = ".\tools\C2M4AI.exe",
+ [switch] $NoSchema = $true,
+ [switch] $Force
+)
+
+Set-StrictMode -Version Latest
+$ErrorActionPreference = "Stop"
+
+# Hash file lives next to the reference dir (in aidocs/)
+$hashFile = Join-Path (Split-Path $OutputDir -Parent) ".api-spec-hash"
+
+function Get-FileSHA256 ([string]$path) {
+ return (Get-FileHash -Path $path -Algorithm SHA256).Hash
+}
+
+Write-Host ""
+Write-Host "=== Invoke-C2M4AI ===" -ForegroundColor Cyan
+Write-Host "Contract : $ApiContract"
+Write-Host "Output : $OutputDir"
+Write-Host "Exe : $C2M4AIExe"
+Write-Host ""
+
+# Validate inputs
+if (-not (Test-Path $ApiContract)) {
+ Write-Error "API contract not found: $ApiContract"
+ exit 1
+}
+if (-not (Test-Path $C2M4AIExe)) {
+ Write-Error "C2M4AI.exe not found: $C2M4AIExe"
+ exit 1
+}
+
+$currentHash = Get-FileSHA256 $ApiContract
+
+# Change detection
+if (-not $Force -and (Test-Path $hashFile)) {
+ $storedHash = (Get-Content $hashFile -Raw).Trim()
+ if ($storedHash -eq $currentHash) {
+ Write-Host "API spec unchanged (SHA-256 match). Skipping generation." -ForegroundColor DarkGray
+ Write-Host "Use -Force to regenerate regardless."
+ exit 0
+ }
+ Write-Host "API spec changed -- running C2M4AI."
+} elseif ($Force) {
+ Write-Host "Force mode -- running C2M4AI."
+} else {
+ Write-Host "No stored hash -- running C2M4AI for the first time."
+}
+
+# Ensure output directory exists
+if (-not (Test-Path $OutputDir)) {
+ New-Item -ItemType Directory -Path $OutputDir | Out-Null
+ Write-Host "Created output dir: $OutputDir"
+}
+
+# Run C2M4AI
+$noSchemaFlag = @(if ($NoSchema) { "--no-schema" })
+Write-Host ""
+Write-Host "Running: $C2M4AIExe $ApiContract -o $OutputDir$($NoSchema ? ' --no-schema' : '')"
+Write-Host ""
+
+& $C2M4AIExe $ApiContract -o $OutputDir @noSchemaFlag
+
+if ($LASTEXITCODE -ne 0) {
+ Write-Error "C2M4AI exited with code $LASTEXITCODE"
+ exit $LASTEXITCODE
+}
+
+# Store new hash only after success
+$currentHash | Set-Content -Path $hashFile -Encoding UTF8
+Write-Host ""
+Write-Host "Done. Reference files written to: $OutputDir" -ForegroundColor Cyan
+Write-Host "Hash stored : $hashFile ($currentHash)"
diff --git a/scripts/ai-docs-scripts/New-GuidesPrompt.ps1 b/scripts/ai-docs-scripts/New-GuidesPrompt.ps1
new file mode 100644
index 0000000..af246b9
--- /dev/null
+++ b/scripts/ai-docs-scripts/New-GuidesPrompt.ps1
@@ -0,0 +1,247 @@
+<#
+.SYNOPSIS
+ Generates a VS Code Copilot agent-mode prompt for creating or updating aidocs/guides/*.md.
+
+.DESCRIPTION
+ Detects whether this is an initial generation (no guides exist yet), a full refresh
+ (-All), or an incremental update (-ChangedSources). Writes a ready-to-use .prompt.md
+ file that the human opens in VS Code and runs via Copilot agent mode.
+
+ The generated prompt references workspace file paths so Copilot reads source docs
+ and existing guides directly — no content embedding required.
+
+ Run from the repository root.
+
+.PARAMETER ChangedSources
+ List of repo-relative paths of source docs that changed (from git diff --name-only).
+ Example: @("articles/LCPublicAPI/docs/API-rate-limits.md")
+ When omitted with no -All, defaults to -All behaviour.
+
+.PARAMETER All
+ Generate a full-refresh prompt covering all source docs.
+
+.PARAMETER OutputFile
+ Where to write the generated prompt.
+ Default: _update-prompt.md in the repository root.
+
+.EXAMPLE
+ # Initial generation or full refresh
+ .\scripts\ai-docs-scripts\New-GuidesPrompt.ps1 -All
+
+ # Incremental: only source docs that changed
+ $changed = git diff --name-only HEAD~1 HEAD
+ .\scripts\ai-docs-scripts\New-GuidesPrompt.ps1 -ChangedSources $changed
+
+ # Custom output path
+ .\scripts\ai-docs-scripts\New-GuidesPrompt.ps1 -All -OutputFile .\my-prompt.md
+#>
+param(
+ [string[]] $ChangedSources = @(),
+ [switch] $All,
+ [string] $OutputFile = ""
+)
+
+Set-StrictMode -Version Latest
+$ErrorActionPreference = "Stop"
+
+# ── Resolve output file path ───────────────────────────────────────────────────
+if (-not $OutputFile) {
+ $OutputFile = Join-Path $PWD "_update-prompt.md"
+}
+
+# ── Resolve key paths ──────────────────────────────────────────────────────────
+$repoRoot = $PWD.Path
+$sourceDocsDir = Join-Path $repoRoot "articles\LCPublicAPI\docs"
+$guidesDir = Join-Path $repoRoot "articles\LCPublicAPI\aidocs\guides"
+$relSourceDocsDir = 'articles/LCPublicAPI/docs'
+$relGuidesDir = 'articles/LCPublicAPI/aidocs/guides'
+
+if (-not (Test-Path $sourceDocsDir)) {
+ Write-Error "Source docs directory not found: $sourceDocsDir"
+ exit 1
+}
+
+# ── Determine mode ─────────────────────────────────────────────────────────────
+$existingGuides = @()
+if (Test-Path $guidesDir) {
+ $existingGuides = @(Get-ChildItem $guidesDir -Filter "*.md" |
+ Where-Object { $_.Name -ne "index.md" -and $_.Name -notlike "_*" } |
+ Sort-Object Name)
+}
+
+$isInitial = ($existingGuides.Count -eq 0)
+
+# Normalise changed-sources to repo-relative, forward-slash paths
+$normChanged = @($ChangedSources |
+ ForEach-Object { ($_ -replace '\\', '/').TrimStart('.').TrimStart('/') } |
+ Where-Object { $_ -match '(?i)articles/LCPublicAPI/docs/' })
+
+# Auto-detect changed sources from git when no -ChangedSources and no -All
+if (-not $isInitial -and $normChanged.Count -eq 0 -and -not $All) {
+ Write-Host "No -ChangedSources supplied -- auto-detecting via git diff HEAD~1 HEAD..." -ForegroundColor DarkGray
+ $gitOut = @(git diff --name-only HEAD~1 HEAD 2>$null | Where-Object { $_ })
+ $normChanged = @($gitOut |
+ ForEach-Object { ($_ -replace '\\', '/').TrimStart('.').TrimStart('/') } |
+ Where-Object { $_ -match '(?i)articles/LCPublicAPI/docs/' })
+ if ($normChanged.Count -eq 0) {
+ Write-Warning "No guide source changes detected — use -All for a full refresh."
+ exit 0
+ }
+}
+
+$mode = if ($isInitial) { "Initial generation" }
+ elseif ($All) { "Full refresh" }
+ else { "Incremental update" }
+
+Write-Host ""
+Write-Host "=== New-GuidesPrompt ===" -ForegroundColor Cyan
+Write-Host "Mode : $mode"
+Write-Host "Source docs : $sourceDocsDir"
+Write-Host "Guides dir : $guidesDir"
+Write-Host "Existing guides: $($existingGuides.Count)"
+Write-Host "Output file : $OutputFile"
+Write-Host ""
+
+# ── Build source-docs section ──────────────────────────────────────────────────
+$fence = '```'
+$sourceDocsSection = if ($isInitial -or $All) {
+ "Read **all** ```.md``` files recursively under this path:`n`n$fence`n$relSourceDocsDir`n$fence"
+} else {
+ $bullets = $normChanged | ForEach-Object {
+ $rel = "$relSourceDocsDir/" + ($_ -replace '(?i).*articles/LCPublicAPI/docs/', '')
+ "- ``$rel``"
+ }
+ "Read the following changed source files:`n`n$($bullets -join "`n")"
+}
+
+# ── Build existing-guides section ──────────────────────────────────────────────
+$guidesSection = if ($existingGuides.Count -eq 0) {
+ "None — this is the initial generation run."
+} else {
+ $bullets = $existingGuides | ForEach-Object {
+ $rel = "$relGuidesDir/$($_.Name)"
+ "- ``$rel``"
+ }
+ "The following guide files already exist in ``$relGuidesDir``:`n`n$($bullets -join "`n")"
+}
+
+# ── Build mode-specific instructions ──────────────────────────────────────────
+$modeInstructions = switch ($mode) {
+ "Initial generation" {
+@"
+This is the **first time** guides are being generated. No guide files exist yet.
+
+1. Read all source files listed above.
+2. Design a guide structure: determine which cross-cutting topics deserve their own file.
+3. Create one `.md` file per topic in the output directory.
+4. The suggested initial topics are listed at the bottom of this prompt as a reference — you may deviate if you see a better structure based on the source material.
+"@
+ }
+ "Full refresh" {
+@"
+This is a **full refresh** — regenerate all guides from scratch.
+
+1. Read all source files listed above.
+2. For each existing guide, rewrite it with up-to-date content.
+3. If you find topics in the source material that have no corresponding guide, create new guide files.
+4. Remove any guide that no longer has relevant source material.
+"@
+ }
+ "Incremental update" {
+@"
+This is an **incremental update** — only the listed source files changed.
+
+1. Read each changed source file listed above.
+2. Determine which existing guide(s) are affected by the changes.
+3. Update only the affected guides. Do not touch unaffected guides.
+4. If a change introduces a concept that has no home in any existing guide, create a new guide file.
+"@
+ }
+}
+
+# ── Build suggested initial topics (reference) ─────────────────────────────────
+$suggestedTopics = @"
+> **Suggested examples of initial guide structure** (agent may deviate based on source material):
+>
+> | Output file | Primary source file(s) |
+> |---|---|
+> | ``auth.md`` | ``Authentication.md``, ``Service-credentials.md``, ``Service-users-and-custom-applications.md``, ``Headers-considerations.md``, ``Multi-region.md`` |
+> | ``pagination.md`` | ``Use-paging-and-sorting-for-lists.md`` |
+> | ``fields.md`` | ``Use-fields-in-your-requests.md`` |
+> | ``errors.md`` | ``How-to-report-an-issue.md`` |
+> | ``rate-limits.md`` | ``API-rate-limits.md`` |
+> | ``async-polling.md`` | ``Track-projects.md``, ``Interact-with-tasks.md``, ``File-formats.md`` |
+> | ``file-upload.md`` | ``How-to-multipart.md``, ``File-formats.md`` |
+> | ``webhooks.md`` | ``webhooks/`` subfolder |
+> | ``locations-folders.md`` | ``How-to-use-location-and-folders.md`` |
+> | ``put-semantics.md`` | ``Updating-data-with-PUT.md`` |
+> | ``custom-fields.md`` | ``Custom-Fields.md`` |
+> | ``api-clients.md`` | ``api-clients/java/``, ``api-clients/net/`` |
+"@
+
+# ── Assemble the full prompt ───────────────────────────────────────────────────
+$prompt = @"
+---
+mode: agent
+description: "Generate or update Trados Language Cloud API guides in articles/LCPublicAPI/aidocs/guides/"
+---
+
+**Your task is to summarize and reorganize the source user documentation into a set of concise, AI-consumable guides focused on cross-cutting concerns and concepts.**
+**Execute this task now. Do not ask for clarification — follow the instructions below exactly.**
+
+# Guide generation — $mode
+
+## Instructions
+
+$modeInstructions
+
+## Source docs
+
+$sourceDocsSection
+
+## Existing guides
+
+$guidesSection
+
+## Output directory
+
+Write all guide files to:
+
+``````
+$relGuidesDir
+``````
+
+## Guide authoring rules
+
+- Each guide is a single `.md` file focused on one cross-cutting concern.
+- Content must be AI-consumable: **dense, factual, no marketing language, no redundant prose**.
+- Include concrete values wherever the source provides them: exact header names, enum values, status codes, numeric limits, algorithm steps.
+- Use **tables** for reference data (error codes, rate limits, event types, status values, etc.).
+- Use **fenced code blocks** for HTTP examples and SDK snippets.
+- All links between files must be **relative** and correct for static serving.
+- Do **not** add YAML front matter to guide files.
+- Do **not** add "last updated" timestamps.
+- Do **not** add any content that cannot be directly sourced from the provided docs.
+- Do **not** add any changelog information like What's New or What's Deprecated.
+
+$suggestedTopics
+"@
+
+# ── Write output ──────────────────────────────────────────────────────────────
+[System.IO.File]::WriteAllText($OutputFile, $prompt, [System.Text.Encoding]::UTF8)
+
+Write-Host "Prompt written to: $OutputFile" -ForegroundColor Green
+Write-Host ""
+Write-Host "Next steps:" -ForegroundColor Yellow
+Write-Host " 1. Open VS Code"
+Write-Host " 2. Open Copilot Chat in agent mode"
+Write-Host " 3. Type: #_update-prompt.md"
+Write-Host " (or drag the file into the chat input)"
+Write-Host " 4. Send -- Copilot will read source docs and write/update guides"
+Write-Host " 5. Review the changes in .\articles\LCPublicAPI\aidocs\guides\"
+Write-Host " 6. generate the guides index:"
+Write-Host " .\scripts\ai-docs-scripts\Update-GuidesIndex.ps1" -ForegroundColor White
+Write-Host " 7. Refresh the master index:"
+Write-Host " .\scripts\ai-docs-scripts\Update-AiDocsIndex.ps1" -ForegroundColor White
+Write-Host " 8. Commit when satisfied"
+Write-Host ""
diff --git a/scripts/ai-docs-scripts/README.md b/scripts/ai-docs-scripts/README.md
new file mode 100644
index 0000000..ca6bf63
--- /dev/null
+++ b/scripts/ai-docs-scripts/README.md
@@ -0,0 +1,104 @@
+# AI Docs Pipeline
+
+Generates and maintains the `aidocs/` section of the Language Cloud API docs.
+
+- **`aidocs/reference/`** — fully automated from the OpenAPI spec
+- **`aidocs/guides/`** — human-reviewed, AI-assisted via VS Code Copilot
+
+
+## How it works
+
+When the API spec (`Public-API.v1.json`) changes, the pipeline regenerates all reference pages, including code examples from the .NET and Java SDKs.
+
+When source documentation in `articles/LCPublicAPI/docs/` changes, the pipeline alerts you and provides the exact command to generate an AI prompt for updating the guides. You review and approve the output before it's committed.
+
+All ai docs generation changes are committed to a dedicated branch.
+
+---
+
+## Release process
+
+### AI docs pipeline
+
+```powershell
+# 1. Regenerate reference docs and flag any guide changes
+.\scripts\ai-docs-scripts\Invoke-AiDocsPipeline.ps1 -{params}
+
+# 2. Generate the AI prompt for guides
+.\scripts\ai-docs-scripts\New-GuidesPrompt.ps1 -{params}
+
+# 3. Manual step in VS Code: open Copilot Chat in agent mode and type:
+# #_update-prompt.md → send
+# (or drag the generated _update-prompt.md file into the chat input)
+#
+# This is a human/local action. GitHub Actions cannot open VS Code or Copilot Chat.
+# The automation stops after pushing the AI docs review branch. A person opens the PR.
+
+# 4. Review and accept the generated guide updates in .\aidocs\guides\
+
+# 5. Refresh the master AI docs index
+.\scripts\ai-docs-scripts\Update-AiDocsIndex.ps1
+
+# 6. Commit and push
+
+```
+
+### Review-branch workflow
+
+For release integration, the repository can also create a dedicated AI docs review branch. A person then finishes the guides locally, opens the pull request and merges it into main.
+
+```powershell
+# Run from GitHub Actions
+# Actions → Generate AI Docs → Run workflow
+#
+# Inputs:
+# base_branch = main
+# release_name = YYYY.MM.RXX or a custom label
+# force_regenerate = true/false
+```
+
+This workflow:
+- creates a branch such as `ai-docs/2026.10.1`
+- runs the AI docs generation scripts against the final release state
+- pushes the generated `aidocs/` output to that branch
+- writes a next-steps summary on the workflow run page, with a link to this guide and a ready-made "create PR" link
+- does **not** open a PR and does **not** merge to `main`. Both are done by a person.
+
+> Important: the Copilot/agent step is still a manual local action in VS Code. A GitHub Action cannot launch the VS Code Copilot chat experience for a person.
+
+### Reviewer guide (step by step, no technical knowledge needed)
+
+Do this after the workflow run has finished. Do the steps in order and do not skip any.
+
+1. **Get the branch name.** On GitHub open Actions > the finished **Generate AI Docs** run. The summary at the bottom shows the branch name (it starts with `ai-docs/`).
+2. **Open VS Code** and open the `languagecloud-api-docs` folder (File > Open Folder).
+3. **Switch to the branch.** Click the branch name at the bottom-left of VS Code, then pick `origin/ai-docs/...` (the name from step 1).
+4. **Open the terminal.** In the VS Code menu choose Terminal > New Terminal. A panel opens at the bottom.
+5. **Create the AI prompt.** Copy this line into the terminal and press Enter:
+ ```powershell
+ .\scripts\ai-docs-scripts\New-GuidesPrompt.ps1 -All
+ ```
+ You should see `Prompt written to: ...\_update-prompt.md` in green.
+6. **Run the AI agent.**
+ 1. Open Copilot Chat (chat icon at the top of VS Code, or press Ctrl+Alt+I).
+ 2. In the chat box, make sure the mode says **Agent** (use the dropdown if it does not).
+ 3. Type `#_update-prompt.md` and press Enter.
+ 4. Wait until Copilot says it is finished (this can take several minutes), then click **Keep** to accept the changes.
+7. **Rebuild the indexes.** Copy these two lines into the terminal, one at a time, pressing Enter after each:
+ ```powershell
+ .\scripts\ai-docs-scripts\Update-GuidesIndex.ps1
+ .\scripts\ai-docs-scripts\Update-AiDocsIndex.ps1
+ ```
+ Each prints `Updated` (green) or `Unchanged` (grey). Both are fine. A red message means something went wrong: stop and ask the docs team.
+8. **Save your work.** Open Source Control (branch icon on the left), type a short message such as `docs: update AI guides`, click **Commit**, then **Sync Changes**.
+9. **Open the PR yourself.** Go back to the workflow run summary and click **Create PR into main** (or on GitHub click **Compare & pull request** for your branch). Read through the changed files and ask a teammate to approve it. The merge to `main` is always done by a person, never automatically.
+
+#### Optional parameters (-{params})
+```powershell
+-All | [optional] | When supplied, regenerates everything, else only updates existing content.
+```
+---
+
+### Trigger
+
+**Actions → Generate AI Docs → Run workflow** in the GitHub UI.
diff --git a/scripts/ai-docs-scripts/Update-AiDocsIndex.ps1 b/scripts/ai-docs-scripts/Update-AiDocsIndex.ps1
new file mode 100644
index 0000000..0700f78
--- /dev/null
+++ b/scripts/ai-docs-scripts/Update-AiDocsIndex.ps1
@@ -0,0 +1,67 @@
+<#
+.SYNOPSIS
+ Regenerates aidocs/index.md — the master entry point for the AI docs hierarchy.
+
+.DESCRIPTION
+ Scans the aidocs directory for a reference section and an optional guides
+ section, then writes a compact, AI-agent-optimised top-level index.md.
+
+ The index is a navigation map — categories and guide titles are surfaced
+ at the top level so an AI agent can route to the right file in one read
+ without loading the full reference index or all guide files.
+
+ Reference section : aidocs/reference/Index.md must exist.
+ - Operation count derived from .md files (excl. Index.md, Schemas.md, components.md).
+ - Category list extracted from H2 headings in reference/Index.md.
+ Guides section : included only when aidocs/guides/*.md files exist.
+ - Each guide listed with its H1 title and first-paragraph description.
+
+ Idempotent: only writes the file when content has actually changed.
+
+.PARAMETER AiDocsDir
+ Path to the aidocs directory. Default: .\articles\LCPublicAPI\aidocs
+
+.EXAMPLE
+ .\scripts\ai-docs-scripts\Update-AiDocsIndex.ps1
+ .\scripts\ai-docs-scripts\Update-AiDocsIndex.ps1 -AiDocsDir .\articles\LCPublicAPI\aidocs
+#>
+param(
+ [string] $AiDocsDir = ".\articles\LCPublicAPI\aidocs"
+)
+
+Set-StrictMode -Version Latest
+$ErrorActionPreference = "Stop"
+
+Write-Host ""
+Write-Host "=== Update-AiDocsIndex ===" -ForegroundColor Cyan
+Write-Host "AI docs dir : $AiDocsDir"
+Write-Host ""
+
+$referenceDir = Join-Path $AiDocsDir "reference"
+$guidesDir = Join-Path $AiDocsDir "guides"
+$outputPath = Join-Path $AiDocsDir "index.md"
+
+if (-not (Test-Path $referenceDir)) {
+ Write-Warning "Reference directory not found: $referenceDir — skipping index generation."
+ exit 0
+}
+
+# ── Build content ─────────────────────────────────────────────────────────────
+$lines = [System.Collections.Generic.List[string]]::new()
+$lines.Add("# Language Cloud Public API — AI Docs")
+$lines.Add("")
+$lines.Add("## API Reference → [reference/Index.md](./reference/Index.md)")
+$lines.Add("")
+$lines.Add("## Guides → [guides/index.md](./guides/index.md)")
+
+$indexContent = ($lines -join "`n") + "`n"
+
+# ── Write if changed ──────────────────────────────────────────────────────────
+$current = if (Test-Path $outputPath) { [System.IO.File]::ReadAllText($outputPath) } else { "" }
+
+if ($current.Trim() -eq $indexContent.Trim()) {
+ Write-Host "Unchanged: $outputPath" -ForegroundColor DarkGray
+} else {
+ [System.IO.File]::WriteAllText($outputPath, $indexContent, [System.Text.Encoding]::UTF8)
+ Write-Host "Updated : $outputPath" -ForegroundColor Green
+}
diff --git a/scripts/ai-docs-scripts/Update-GuidesIndex.ps1 b/scripts/ai-docs-scripts/Update-GuidesIndex.ps1
new file mode 100644
index 0000000..966bd18
--- /dev/null
+++ b/scripts/ai-docs-scripts/Update-GuidesIndex.ps1
@@ -0,0 +1,111 @@
+<#
+.SYNOPSIS
+ Regenerates aidocs/guides/index.md from the current set of guide files.
+
+.DESCRIPTION
+ Scans GuidesDir for all *.md files (excluding index.md itself), reads
+ the first H1 heading as the title, and the first non-heading paragraph
+ as the short description. Writes a flat Markdown table to index.md.
+
+ Idempotent: only writes the file when content has actually changed.
+
+.PARAMETER GuidesDir
+ Path to the articles/LCPublicAPI/aidocs/guides directory. Default: .\articles\LCPublicAPI\aidocs\guides
+
+.EXAMPLE
+ .\scripts\ai-docs-scripts\Update-GuidesIndex.ps1
+ .\scripts\ai-docs-scripts\Update-GuidesIndex.ps1 -GuidesDir .\articles\LCPublicAPI\aidocs\guides
+#>
+param(
+ [string] $GuidesDir = ".\articles\LCPublicAPI\aidocs\guides"
+)
+
+Set-StrictMode -Version Latest
+$ErrorActionPreference = "Stop"
+
+Write-Host ""
+Write-Host "=== Update-GuidesIndex ===" -ForegroundColor Cyan
+Write-Host "Guides dir : $GuidesDir"
+Write-Host ""
+
+if (-not (Test-Path $GuidesDir)) {
+ Write-Error "Guides directory not found: $GuidesDir"
+ exit 1
+}
+
+$guideFiles = Get-ChildItem $GuidesDir -Filter "*.md" |
+ Where-Object { $_.Name -ne "index.md" -and $_.Name -notlike "_*" } |
+ Sort-Object Name
+
+if ($guideFiles.Count -eq 0) {
+ Write-Warning "No guide files found in $GuidesDir"
+ exit 0
+}
+
+function Get-GuideInfo ([string]$filePath) {
+ $lines = [System.IO.File]::ReadAllLines($filePath)
+ $title = ""
+ $desc = ""
+ $pastH1 = $false
+
+ foreach ($line in $lines) {
+ $trimmed = $line.Trim()
+
+ # First H1 is the title
+ if (-not $title -and $trimmed -match '^#\s+(.+)$') {
+ $title = $matches[1].Trim()
+ $pastH1 = $true
+ continue
+ }
+
+ # First substantive non-heading, non-empty, non-table, non-code line after H1
+ if ($pastH1 -and -not $desc -and $trimmed -and
+ $trimmed -notmatch '^#+\s' -and
+ $trimmed -notmatch '^\|' -and
+ $trimmed -notmatch '^```' -and
+ $trimmed -notmatch '^---') {
+
+ # Strip inline markdown links → plain text for the description
+ $d = $trimmed -replace '\[([^\]]+)\]\([^)]+\)', '$1'
+ # Strip bold/italic markers
+ $d = $d -replace '\*\*?([^*]+)\*\*?', '$1'
+ # Truncate at 120 chars
+ if ($d.Length -gt 120) { $d = $d.Substring(0, 117) + "..." }
+ $desc = $d
+ }
+
+ if ($title -and $desc) { break }
+ }
+
+ # Fallback: use filename without extension as title
+ if (-not $title) { $title = [System.IO.Path]::GetFileNameWithoutExtension($filePath) }
+
+ return [PSCustomObject]@{ Title = $title; Description = $desc }
+}
+
+# Build table rows
+$rows = foreach ($file in $guideFiles) {
+ $info = Get-GuideInfo $file.FullName
+ "| [$($file.Name)](./$($file.Name)) | $($info.Description) |"
+ Write-Host " $($file.Name) → $($info.Title)"
+}
+
+$indexContent = @"
+# Guides Index
+
+| Guide | Description |
+|---|---|
+$($rows -join "`n")
+"@.TrimStart()
+
+$outputPath = Join-Path $GuidesDir "index.md"
+$current = if (Test-Path $outputPath) { [System.IO.File]::ReadAllText($outputPath) } else { "" }
+
+if ($current.Trim() -eq $indexContent.Trim()) {
+ Write-Host ""
+ Write-Host "Unchanged: $outputPath" -ForegroundColor DarkGray
+} else {
+ [System.IO.File]::WriteAllText($outputPath, $indexContent, [System.Text.Encoding]::UTF8)
+ Write-Host ""
+ Write-Host "Updated : $outputPath" -ForegroundColor Green
+}
diff --git a/scripts/ai-docs-scripts/Update-SdkSections.ps1 b/scripts/ai-docs-scripts/Update-SdkSections.ps1
new file mode 100644
index 0000000..9200425
--- /dev/null
+++ b/scripts/ai-docs-scripts/Update-SdkSections.ps1
@@ -0,0 +1,609 @@
+<#
+.SYNOPSIS
+ Injects or updates ## SDK sections in aidocs/reference/*.md files
+ by parsing .NET and Java SDK source files.
+
+.DESCRIPTION
+ For each operationId (= md filename without extension):
+ - Finds the matching async method in PublicApiClient.cs (IXxxClient interface)
+ - Finds the matching primary method in SDK_JAVA/api/*.java
+ - Builds a structured ## SDK Markdown section
+ - Appends or replaces that section in the reference .md file
+ - Only writes the file when content actually changes (idempotent)
+
+.PARAMETER ReferenceDir
+ Path to articles/LCPublicAPI/aidocs/reference containing *.md files. Default: .\articles\LCPublicAPI\aidocs\reference
+
+.PARAMETER DotNetClientFile
+ Path to PublicApiClient.cs. Default: .\SDK_NET\Autogenerated\PublicApiClient.cs
+ Ignored when -DotNetXmlFile is provided.
+
+.PARAMETER DotNetXmlFile
+ Path to the NuGet XML documentation file (Rws.LanguageCloud.Sdk.xml).
+ When this file exists it takes priority over -DotNetClientFile.
+ Default: .\tools\Rws.LanguageCloud.Sdk.xml
+ Obtain via: .\scripts\ai-docs-scripts\Get-DotNetSdkXml.ps1
+ (Invoke-AiDocsPipeline.ps1 downloads it automatically.)
+
+.PARAMETER JavaApiDir
+ Path to SDK_JAVA/api directory. Default: .\SDK_JAVA\api
+
+.PARAMETER Operations
+ Optional list of specific operationIds to process.
+ If omitted, all reference files are processed.
+
+.PARAMETER Force
+ Re-write every file even when the SDK section content is unchanged.
+
+.EXAMPLE
+ .\scripts\ai-docs-scripts\Update-SdkSections.ps1
+ .\scripts\ai-docs-scripts\Update-SdkSections.ps1 -Operations AcceptTask,GetProject
+ .\scripts\ai-docs-scripts\Update-SdkSections.ps1 -Force
+#>
+param(
+ [string] $ReferenceDir = ".\articles\LCPublicAPI\aidocs\reference",
+ [string] $DotNetXmlFile = ".\tools\Rws.LanguageCloud.Sdk.xml",
+ [string] $DotNetClientFile = ".\SDK_NET\Autogenerated\PublicApiClient.cs",
+ [string] $JavaApiDir = ".\tools\java-api",
+ [string[]] $Operations = @(),
+ [switch] $Force
+)
+
+Set-StrictMode -Version Latest
+$ErrorActionPreference = "Stop"
+
+# ── Helpers ────────────────────────────────────────────────────────────────────
+
+function ConvertTo-CamelCase ([string]$s) {
+ if ([string]::IsNullOrEmpty($s)) { return $s }
+ return $s[0].ToString().ToLower() + $s.Substring(1)
+}
+
+# Simplify .NET parameter list: strip CancellationToken, shorten System.* types
+function Format-DotNetParams ([string]$raw) {
+ $p = $raw
+ # CancellationToken is always the last param and may include default(...)
+ # Use [^,]* to capture through the closing ) of default(...)
+ $p = $p -replace ',?\s*System\.Threading\.CancellationToken[^,]*', ''
+ # Shorten generic collections
+ $p = $p -replace 'System\.Collections\.Generic\.IEnumerable<([^>]+)>', 'IEnumerable<$1>'
+ # Shorten nullable int/bool
+ $p = $p -replace 'System\.Nullable<([^>]+)>', '$1?'
+ return $p.Trim().TrimEnd(',').Trim()
+}
+
+# Split a parameter string on top-level commas
+# Handles generics (IEnumerable) and annotation parens (@Param("x"))
+function Split-Params ([string]$params) {
+ $result = [System.Collections.Generic.List[string]]::new()
+ $depth = 0
+ $current = [System.Text.StringBuilder]::new()
+ foreach ($c in $params.ToCharArray()) {
+ if ($c -eq '<' -or $c -eq '(') { $depth++ }
+ elseif ($c -eq '>' -or $c -eq ')') { $depth-- }
+ if ($c -eq ',' -and $depth -eq 0) {
+ $trimmed = $current.ToString().Trim()
+ if ($trimmed) { $result.Add($trimmed) }
+ [void]$current.Clear()
+ } else {
+ [void]$current.Append($c)
+ }
+ }
+ $last = $current.ToString().Trim()
+ if ($last) { $result.Add($last) }
+ return $result
+}
+
+# Build a markdown parameter table from a clean param list string
+function Build-DotNetParamTable ([string]$params) {
+ if ([string]::IsNullOrWhiteSpace($params)) { return "" }
+ $rows = Split-Params $params
+ $lines = @("| Parameter | Type | Required |", "|---|---|---|")
+ foreach ($row in $rows) {
+ # Pattern: [type] [name] [= default]
+ # e.g.: string taskId | int? top = null | IEnumerable location = null
+ if ($row -match '^(.+?)\s+(\w+)\s*(=.*)?$') {
+ $type = $matches[1].Trim()
+ $name = $matches[2].Trim()
+ $hasDefault = ($matches[3] -and $matches[3].Trim())
+ $required = if ($hasDefault) { "no" } else { "yes" }
+ # Strip nullable ? for display
+ $typeDisp = $type -replace '\?$', ''
+ $lines += "| ``$name`` | ``$typeDisp`` | $required |"
+ }
+ }
+ return $lines -join "`n"
+}
+
+# Build a markdown parameter table from Java raw params (with annotations)
+function Build-JavaParamTable ([string]$rawParams) {
+ if ([string]::IsNullOrWhiteSpace($rawParams)) { return "" }
+ $rows = Split-Params $rawParams
+ $lines = @("| Parameter | Type | Required |", "|---|---|---|")
+ foreach ($row in $rows) {
+ # Determine required from Nonnull/Nullable BEFORE stripping
+ $required = if ($row -match 'Nullable') { "no" } else { "yes" }
+ # Strip all annotations: @Param("x"), @javax.annotation.Nonnull/Nullable, @Nonnull, @Nullable
+ $clean = $row -replace '@javax\.annotation\.\w+\s*', ''
+ $clean = $clean -replace '@\w+(\("[^"]*"\))?\s*', ''
+ $clean = $clean.Trim()
+ # Now: "Type name"
+ if ($clean -match '^(.+?)\s+(\w+)$') {
+ $type = $matches[1].Trim() -replace '^java\.lang\.', ''
+ $name = $matches[2].Trim()
+ $lines += "| ``$name`` | ``$type`` | $required |"
+ }
+ }
+ return $lines -join "`n"
+}
+
+# ── Parse .NET SDK from NuGet XML documentation ──────────────────────────────
+
+# Split a comma-separated type list from a .NET XML member name signature.
+# Uses {} depth counting to handle generic type args e.g. Nullable{Int32}
+function Split-XmlTypeList ([string]$typeStr) {
+ $result = [System.Collections.Generic.List[string]]::new()
+ $depth = 0
+ $current = [System.Text.StringBuilder]::new()
+ foreach ($c in $typeStr.ToCharArray()) {
+ if ($c -eq '{') { $depth++ }
+ elseif ($c -eq '}') { $depth-- }
+ if ($c -eq ',' -and $depth -eq 0) {
+ $t = $current.ToString().Trim()
+ if ($t) { $result.Add($t) }
+ [void]$current.Clear()
+ } else {
+ [void]$current.Append($c)
+ }
+ }
+ $t = $current.ToString().Trim()
+ if ($t) { $result.Add($t) }
+ return $result
+}
+
+# Shorten a fully-qualified .NET type string from XML member-name format.
+# XML uses {} for generic brackets: System.Nullable{System.Int32}
+function Shorten-DotNetXmlType ([string]$fqType) {
+ $fqType = $fqType.Trim()
+
+ # Nullable → T?
+ if ($fqType -match '^System\.Nullable\{(.+)\}$') {
+ return (Shorten-DotNetXmlType $matches[1]) + '?'
+ }
+
+ # Generic outer type (e.g. IEnumerable{T}, List{T})
+ if ($fqType -match '^.+\.([A-Z]\w+)\{(.+)\}$') {
+ $baseName = $matches[1]
+ $innerShort = Shorten-DotNetXmlType $matches[2]
+ return "$baseName<$innerShort>"
+ }
+
+ # Primitive type aliases
+ switch ($fqType) {
+ 'System.String' { return 'string' }
+ 'System.Int32' { return 'int' }
+ 'System.Int64' { return 'long' }
+ 'System.Boolean' { return 'bool' }
+ 'System.Double' { return 'double' }
+ 'System.Single' { return 'float' }
+ 'System.Decimal' { return 'decimal'}
+ 'System.Object' { return 'object' }
+ 'System.Void' { return 'void' }
+ 'System.Byte' { return 'byte' }
+ }
+
+ # Strip namespace: take last dotted segment
+ $lastDot = $fqType.LastIndexOf('.')
+ if ($lastDot -ge 0) { return $fqType.Substring($lastDot + 1) }
+ return $fqType
+}
+
+# Infer the C# return type for a method from its operation name and text.
+# The XML member-name format does NOT include the return type, so we use heuristics.
+function Get-DotNetReturnTypeHeuristic ([string]$opId, [string]$returnsText) {
+ # Explicit void
+ if ($returnsText -match 'No Content') { return 'Task' }
+
+ # Void by operation-name convention
+ $voidPrefixes = 'Delete|Accept|Reject|Release|Reclaim|Assign|Cancel|Reschedule|' +
+ 'AddProjectsToGroup|RemoveProjectsFromGroup|CompleteTask|CompleteProject|' +
+ 'StartTask|UpdateTranslationEngine|UpdateWorkflow|UpdateProjectConfiguration'
+ if ($opId -match "^($voidPrefixes)") { return 'Task' }
+
+ # List* → Task
+ if ($opId -match '^List(.+)$') { return "Task" }
+
+ # Get* / Create* / Copy* → Task
+ if ($opId -match '^Get(.+)$') { return "Task<$($matches[1])>" }
+ if ($opId -match '^Create(.+)$') { return "Task<$($matches[1])>" }
+ if ($opId -match '^Copy(.+)$') { return "Task<$($matches[1])>" }
+
+ # Everything else (Download, Export, Poll, Import, Update, Add…) defaults to Task
+ return 'Task'
+}
+
+# Parse the NuGet XML documentation file into an operation-id → method map.
+# Targets interface members (I*Client) only.
+function Get-DotNetMethodMapFromXml ([string]$xmlPath) {
+ Write-Host " Parsing .NET SDK from NuGet XML..." -NoNewline
+ [xml]$doc = Get-Content $xmlPath -Raw
+ $map = @{}
+
+ # Optional param names that are typically nullable / defaulted to null in the SDK
+ $knownOptional = @('fields','top','skip','sort','filter','onBehalfOfGroup',
+ 'locationStrategy','createdFrom','createdTo','createdBy',
+ 'name','status','assigneeId','type')
+
+ foreach ($member in $doc.doc.members.member) {
+ $memberName = $member.name
+
+ # Match interface method members: M:...IXxxClient.MethodAsync(
+ if ($memberName -notmatch '^M:[\w.]+\.(I\w+Client)\.(\w+Async)\(') { continue }
+
+ $ifaceName = $matches[1] # e.g. ITaskClient
+ $methodName = $matches[2] # e.g. AcceptTaskAsync
+ $operationId = $methodName -replace 'Async$', ''
+
+ # Extract the comma-separated type list between the first ( and last )
+ $parenStart = $memberName.IndexOf('(')
+ $typeStr = if ($parenStart -ge 0) { $memberName.Substring($parenStart + 1).TrimEnd(')') } else { '' }
+ $allTypes = if ($typeStr) { @(Split-XmlTypeList $typeStr) } else { @() }
+
+ # Remove CancellationToken — it is always present but never shown
+ $paramTypes = @($allTypes | Where-Object { $_ -notmatch 'CancellationToken' })
+
+ # Get parameter names from elements.
+ # NSwag consistently places the cancellationToken FIRST in the XML
+ # doc comment, followed by the real parameters in declaration order.
+ $allParamEls = $member.SelectNodes('param')
+ $paramNames = @($allParamEls |
+ Where-Object { $_.name -ne 'cancellationToken' } |
+ ForEach-Object { $_.name })
+
+ # Shorten each type
+ $shortTypes = @($paramTypes | ForEach-Object { Shorten-DotNetXmlType $_ })
+
+ # Zip names and types; encode optional params with = null so the existing
+ # Build-DotNetParamTable function correctly marks them as not required
+ $count = [Math]::Min($paramNames.Count, $shortTypes.Count)
+ $parts = for ($i = 0; $i -lt $count; $i++) {
+ $sType = $shortTypes[$i]
+ $pName = $paramNames[$i]
+ $isOpt = $sType.EndsWith('?') -or ($pName -in $knownOptional)
+ if ($isOpt) { "$sType $pName = null" } else { "$sType $pName" }
+ }
+ $cleanParams = $parts -join ', '
+
+ # Return type via heuristic (not available in XML member name)
+ $returnsNode = $member.SelectSingleNode('returns')
+ $returnsText = if ($returnsNode) { $returnsNode.InnerText } else { '' }
+ $retType = Get-DotNetReturnTypeHeuristic $operationId $returnsText
+
+ $sig = if ($retType -eq 'Task') { "Task ${methodName}($cleanParams)" }
+ else { "$retType ${methodName}($cleanParams)" }
+
+ # First match wins (interface docs may appear before implementation docs)
+ if (-not $map.ContainsKey($operationId)) {
+ $summaryNode = $member.SelectSingleNode('summary')
+ $map[$operationId] = [PSCustomObject]@{
+ Interface = $ifaceName
+ Summary = if ($summaryNode) { $summaryNode.InnerText.Trim() } else { '' }
+ MethodName = $methodName
+ ReturnType = $retType
+ Parameters = $cleanParams
+ Signature = $sig
+ }
+ }
+ }
+
+ Write-Host " found $($map.Count) methods."
+ return $map
+}
+
+# Clean Java raw params for display in code block (strip annotations, keep type+name)
+function Format-JavaParams ([string]$rawParams) {
+ if ([string]::IsNullOrWhiteSpace($rawParams)) { return "" }
+ $parts = Split-Params $rawParams
+ $clean = $parts | ForEach-Object {
+ $p = $_ -replace '@javax\.annotation\.\w+\s*', ''
+ $p = $p -replace '@\w+(\("[^"]*"\))?\s*', ''
+ $p.Trim()
+ }
+ return ($clean | Where-Object { $_ }) -join ', '
+}
+
+# ── Parse .NET SDK ─────────────────────────────────────────────────────────────
+
+function Get-DotNetMethodMap ([string]$filePath) {
+ Write-Host " Parsing .NET SDK..." -NoNewline
+ $map = @{}
+ $lines = [System.IO.File]::ReadAllLines($filePath)
+ $currentIface = $null
+ $braceDepth = 0
+ $inIface = $false
+ $seenOpenBrace = $false
+ $lastSummary = $null
+
+ for ($i = 0; $i -lt $lines.Length; $i++) {
+ $line = $lines[$i]
+
+ # Detect interface start: matches 'public interface' and 'public partial interface'
+ if ($line -match '^\s*public (?:partial )?interface (I\w+)') {
+ $currentIface = $matches[1]
+ $inIface = $true
+ $braceDepth = 0
+ $seenOpenBrace = $false
+ }
+
+ if ($inIface) {
+ # Capture summary and method BEFORE updating brace depth so we
+ # process the closing-brace line correctly (depth goes to 0 after method check)
+
+ # Capture inline summary: /// text
+ if ($line -match '///\s*(.+?)') {
+ $lastSummary = $matches[1].Trim()
+ }
+
+ # Interface method declaration (ends with semicolon, not opening brace)
+ # System.Threading.Tasks.Task MethodNameAsync(params);
+ if ($currentIface -and
+ $line -match '^\s*System\.Threading\.Tasks\.Task(?:<([^>]+)>)?\s+(\w+Async)\((.+)\)\s*;') {
+
+ $retType = if ($matches[1]) { $matches[1].Trim() } else { 'void' }
+ $methodName = $matches[2]
+ $rawParams = $matches[3]
+ $operationId = $methodName -replace 'Async$', ''
+ $cleanParams = Format-DotNetParams $rawParams
+
+ # Produce the clean signature for display (no System.Threading prefixes)
+ $retDisp = $retType -replace '^System\.Threading\.Tasks\.', ''
+ $sig = if ($retType -eq 'void') { "Task $methodName($cleanParams)" } `
+ else { "Task<$retDisp> $methodName($cleanParams)" }
+
+ if (-not $map.ContainsKey($operationId)) {
+ $map[$operationId] = [PSCustomObject]@{
+ Interface = $currentIface
+ Summary = $lastSummary
+ MethodName = $methodName
+ ReturnType = $retType
+ Parameters = $cleanParams
+ Signature = $sig
+ }
+ }
+ $lastSummary = $null
+ }
+
+ # Update brace depth AFTER method detection
+ foreach ($c in $line.ToCharArray()) {
+ if ($c -eq '{') { $braceDepth++; $seenOpenBrace = $true }
+ elseif ($c -eq '}') { $braceDepth-- }
+ }
+ # Only end interface AFTER the opening brace has been seen
+ if ($seenOpenBrace -and $braceDepth -le 0) {
+ $inIface = $false
+ $currentIface = $null
+ $braceDepth = 0
+ $seenOpenBrace = $false
+ }
+ }
+ }
+
+ Write-Host " found $($map.Count) methods."
+ return $map
+}
+
+# ── Parse Java SDK ─────────────────────────────────────────────────────────────
+
+function Get-JavaMethodMap ([string]$apiDir) {
+ Write-Host " Parsing Java SDK..." -NoNewline
+ $map = @{}
+
+ foreach ($javaFile in Get-ChildItem $apiDir -Filter "*Api.java") {
+ $apiClass = $javaFile.BaseName # e.g. TaskApi
+ $lines = [System.IO.File]::ReadAllLines($javaFile.FullName)
+ # States: idle → gotRequestLine → collectingAnnotations → done
+ $requestLine = $null
+ $skipUntilSemi = $false
+ $inHeadersBlock = $false
+
+ for ($i = 0; $i -lt $lines.Length; $i++) {
+ $line = $lines[$i]
+
+ # Skip multi-line @Headers({...}) blocks
+ if ($inHeadersBlock) {
+ if ($line -match '\}') { $inHeadersBlock = $false }
+ continue
+ }
+
+ # Capture @RequestLine
+ if ($line -match '@RequestLine\("(\w+)\s+([^"]+)"\)') {
+ $requestLine = "$($matches[1]) $($matches[2])"
+ continue
+ }
+
+ # Start of @Headers block
+ if ($requestLine -and $line -match '@Headers\(\{') {
+ if ($line -notmatch '\}') { $inHeadersBlock = $true }
+ continue
+ }
+
+ # Skip other annotations between @RequestLine and method
+ if ($requestLine -and $line -match '^\s*@') {
+ continue
+ }
+
+ if ($requestLine) {
+ # Method declaration line: returnType methodName(params);
+ # Use greedy (.+) so annotations like @Param("x") with nested parens are captured
+ if ($line -match '^\s+(void|ApiResponse<[\w,\s<>]+>|[\w<>\[\]]+)\s+(\w+)\s*\((.+)\)\s*;') {
+ $retType = $matches[1].Trim()
+ $methodName = $matches[2].Trim()
+ $rawParams = $matches[3].Trim()
+
+ # Only take primary methods — skip *WithHttpInfo and *QueryMap variants
+ if ($methodName -notmatch 'WithHttpInfo$' -and
+ $rawParams -notmatch '@QueryMap') {
+
+ $operationId = $methodName[0].ToString().ToUpper() + $methodName.Substring(1)
+ $cleanParams = Format-JavaParams $rawParams
+
+ if (-not $map.ContainsKey($operationId)) {
+ $map[$operationId] = [PSCustomObject]@{
+ ApiClass = $apiClass
+ MethodName = $methodName
+ ReturnType = $retType
+ RawParams = $rawParams
+ Parameters = $cleanParams
+ RequestLine = $requestLine
+ Signature = "$retType $methodName($cleanParams)"
+ }
+ }
+ }
+ $requestLine = $null
+ } elseif ($line.Trim() -and $line -notmatch '^\s*//' -and $line -notmatch '^\s*@') {
+ # Unexpected non-annotation non-comment line: reset state
+ $requestLine = $null
+ }
+ }
+ }
+ }
+
+ Write-Host " found $($map.Count) methods."
+ return $map
+}
+
+# ── Build ## SDK section ───────────────────────────────────────────────────────
+
+function Build-SdkSection ([string]$operationId, $dotNet, $java) {
+ $sb = [System.Text.StringBuilder]::new()
+
+ [void]$sb.AppendLine("## SDK")
+
+ # ── .NET ──────────────────────────────────────────────────────────────────
+ [void]$sb.AppendLine()
+ if ($dotNet) {
+ [void]$sb.AppendLine("### .NET — ``$($dotNet.Interface)``")
+ [void]$sb.AppendLine()
+ [void]$sb.AppendLine('```csharp')
+ [void]$sb.AppendLine($dotNet.Signature + ";")
+ [void]$sb.AppendLine('```')
+ $table = Build-DotNetParamTable $dotNet.Parameters
+ if ($table) {
+ [void]$sb.AppendLine()
+ [void]$sb.AppendLine($table)
+ }
+ } else {
+ [void]$sb.AppendLine("### .NET")
+ [void]$sb.AppendLine()
+ [void]$sb.AppendLine("_Not found in .NET SDK — [Manual Review Needed]_")
+ }
+
+ # ── Java ──────────────────────────────────────────────────────────────────
+ [void]$sb.AppendLine()
+ if ($java) {
+ [void]$sb.AppendLine("### Java — ``$($java.ApiClass)``")
+ [void]$sb.AppendLine()
+ [void]$sb.AppendLine('```java')
+ [void]$sb.AppendLine("// $($java.RequestLine)")
+ [void]$sb.AppendLine($java.Signature + ";")
+ [void]$sb.AppendLine('```')
+ $table = Build-JavaParamTable $java.RawParams
+ if ($table) {
+ [void]$sb.AppendLine()
+ [void]$sb.AppendLine($table)
+ }
+ } else {
+ [void]$sb.AppendLine("### Java")
+ [void]$sb.AppendLine()
+ [void]$sb.AppendLine("_Not found in Java SDK — [Manual Review Needed]_")
+ }
+
+ return $sb.ToString().TrimEnd()
+}
+
+# ── Write reference file ───────────────────────────────────────────────────────
+
+function Update-ReferenceFile ([string]$filePath, [string]$sdkSection) {
+ $content = [System.IO.File]::ReadAllText($filePath)
+ $marker = "`n## SDK"
+ $idx = $content.IndexOf($marker)
+
+ if ($idx -ge 0) {
+ $existing = $content.Substring($idx).TrimStart()
+ if (-not $Force -and $existing.Trim() -eq $sdkSection.Trim()) {
+ return $false # no change
+ }
+ $newContent = $content.Substring(0, $idx).TrimEnd() + "`n`n" + $sdkSection
+ } else {
+ $newContent = $content.TrimEnd() + "`n`n" + $sdkSection
+ }
+
+ [System.IO.File]::WriteAllText($filePath, $newContent, [System.Text.Encoding]::UTF8)
+ return $true
+}
+
+# ── Main ───────────────────────────────────────────────────────────────────────
+
+Write-Host ""
+Write-Host "=== Update-SdkSections ===" -ForegroundColor Cyan
+Write-Host "Reference dir : $ReferenceDir"
+if ($DotNetXmlFile) {
+ Write-Host ".NET SDK (XML) : $DotNetXmlFile"
+} else {
+ Write-Host ".NET SDK (src) : $DotNetClientFile"
+}
+Write-Host "Java SDK dir : $JavaApiDir"
+if ($Operations.Count -gt 0) { Write-Host "Operations : $($Operations -join ', ')" }
+if ($Force) { Write-Host "Mode : FORCE (rewrite all)" -ForegroundColor Yellow }
+Write-Host ""
+
+if ($DotNetXmlFile -and (Test-Path $DotNetXmlFile)) {
+ $dotNetMap = Get-DotNetMethodMapFromXml $DotNetXmlFile
+} elseif (Test-Path $DotNetClientFile) {
+ $dotNetMap = Get-DotNetMethodMap $DotNetClientFile
+} else {
+ Write-Warning "No .NET SDK source found. Checked:\n XML : $DotNetXmlFile\n CS : $DotNetClientFile\nRun: .\scripts\ai-docs-scripts\Get-DotNetSdkXml.ps1"
+ $dotNetMap = @{}
+}
+$javaMap = Get-JavaMethodMap $JavaApiDir
+Write-Host ""
+
+$exclude = @("Index.md", "components.md")
+$mdFiles = Get-ChildItem $ReferenceDir -Filter "*.md" |
+ Where-Object { $_.Name -notin $exclude } |
+ Sort-Object Name
+
+if ($Operations.Count -gt 0) {
+ $mdFiles = $mdFiles | Where-Object { $Operations -contains $_.BaseName }
+ Write-Host "Targeting $($mdFiles.Count) specified operation(s)"
+} else {
+ Write-Host "Processing $($mdFiles.Count) reference files"
+}
+Write-Host ""
+
+$updated = 0; $unchanged = 0; $noSdk = 0
+
+foreach ($file in $mdFiles) {
+ $opId = $file.BaseName
+ $dotNet = $dotNetMap[$opId]
+ $java = $javaMap[$opId]
+
+ if (-not $dotNet -and -not $java) {
+ Write-Warning " No SDK data: $opId"
+ $noSdk++
+ continue
+ }
+
+ $section = Build-SdkSection $opId $dotNet $java
+ $wrote = Update-ReferenceFile $file.FullName $section
+
+ if ($wrote) {
+ Write-Host " [updated ] $opId" -ForegroundColor Green
+ $updated++
+ } else {
+ $unchanged++
+ }
+}
+
+Write-Host ""
+Write-Host "Done. Updated: $updated Unchanged: $unchanged No SDK data: $noSdk" -ForegroundColor Cyan