From 22e8b15417b63c868aee92be992c5812d441fbe5 Mon Sep 17 00:00:00 2001 From: Rares Tritean Date: Tue, 6 Oct 2026 12:07:26 +0300 Subject: [PATCH 1/3] Prepare ai docs generation workflow --- .github/workflows/generate-aidocs.yml | 180 ++++++ .gitignore | 3 + articles/LCPublicAPI/docs/AI-Development.md | 15 + articles/LCPublicAPI/toc.yml | 3 + docfx.json | 11 +- scripts/ai-docs-scripts/Get-ApiSpecDiff.ps1 | 104 +++ scripts/ai-docs-scripts/Get-C2M4AI.ps1 | 82 +++ scripts/ai-docs-scripts/Get-DotNetSdkXml.ps1 | 115 ++++ .../ai-docs-scripts/Get-JavaSdkSources.ps1 | 129 ++++ .../ai-docs-scripts/Invoke-AiDocsPipeline.ps1 | 303 +++++++++ scripts/ai-docs-scripts/Invoke-C2M4AI.ps1 | 108 ++++ scripts/ai-docs-scripts/New-GuidesPrompt.ps1 | 247 +++++++ scripts/ai-docs-scripts/README.md | 104 +++ .../ai-docs-scripts/Update-AiDocsIndex.ps1 | 67 ++ .../ai-docs-scripts/Update-GuidesIndex.ps1 | 111 ++++ .../ai-docs-scripts/Update-SdkSections.ps1 | 609 ++++++++++++++++++ 16 files changed, 2190 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/generate-aidocs.yml create mode 100644 articles/LCPublicAPI/docs/AI-Development.md create mode 100644 scripts/ai-docs-scripts/Get-ApiSpecDiff.ps1 create mode 100644 scripts/ai-docs-scripts/Get-C2M4AI.ps1 create mode 100644 scripts/ai-docs-scripts/Get-DotNetSdkXml.ps1 create mode 100644 scripts/ai-docs-scripts/Get-JavaSdkSources.ps1 create mode 100644 scripts/ai-docs-scripts/Invoke-AiDocsPipeline.ps1 create mode 100644 scripts/ai-docs-scripts/Invoke-C2M4AI.ps1 create mode 100644 scripts/ai-docs-scripts/New-GuidesPrompt.ps1 create mode 100644 scripts/ai-docs-scripts/README.md create mode 100644 scripts/ai-docs-scripts/Update-AiDocsIndex.ps1 create mode 100644 scripts/ai-docs-scripts/Update-GuidesIndex.ps1 create mode 100644 scripts/ai-docs-scripts/Update-SdkSections.ps1 diff --git a/.github/workflows/generate-aidocs.yml b/.github/workflows/generate-aidocs.yml new file mode 100644 index 0000000..6b104cc --- /dev/null +++ b/.github/workflows/generate-aidocs.yml @@ -0,0 +1,180 @@ +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 + run: | + $base = '${{ inputs.base_branch }}' + if ([string]::IsNullOrWhiteSpace($base)) { $base = 'main' } + + $label = '${{ inputs.release_name }}' + if ([string]::IsNullOrWhiteSpace($label)) { + $label = "auto-$([DateTime]::UtcNow.ToString('yyyyMMdd-HHmmss'))" + } + + $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 + run: | + git checkout ${{ steps.meta.outputs.base }} + git checkout -B "${{ steps.meta.outputs.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 + run: | + $params = @{ RootDir = '.' } + + if ('${{ inputs.force_regenerate }}' -eq 'true') { + $params.All = $true + $params.Force = $true + } else { + $params.All = $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 + 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 ${{ steps.meta.outputs.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 + run: git push origin "${{ steps.meta.outputs.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/articles/LCPublicAPI/toc.yml b/articles/LCPublicAPI/toc.yml index 6817a76..2d9166a 100644 --- a/articles/LCPublicAPI/toc.yml +++ b/articles/LCPublicAPI/toc.yml @@ -16,6 +16,9 @@ - name: Known issues href: docs/Known-Issues.md +- name: AI Agent Development + href: docs/AI-Development.md + - name: Getting started items: - name: Multi-region 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 From 0b6cefeae659b3cf2748dd9af142d412ff0fb721 Mon Sep 17 00:00:00 2001 From: Rares Tritean Date: Tue, 6 Oct 2026 12:11:17 +0300 Subject: [PATCH 2/3] Hide AI entry page in toc --- articles/LCPublicAPI/toc.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/articles/LCPublicAPI/toc.yml b/articles/LCPublicAPI/toc.yml index 2d9166a..6817a76 100644 --- a/articles/LCPublicAPI/toc.yml +++ b/articles/LCPublicAPI/toc.yml @@ -16,9 +16,6 @@ - name: Known issues href: docs/Known-Issues.md -- name: AI Agent Development - href: docs/AI-Development.md - - name: Getting started items: - name: Multi-region From 6677101427b283915516dbe45dc77a1fc0eda399 Mon Sep 17 00:00:00 2001 From: ldavidsdl <141138976+ldavidsdl@users.noreply.github.com> Date: Wed, 7 Oct 2026 10:03:34 +0300 Subject: [PATCH 3/3] sanitize input --- .github/workflows/generate-aidocs.yml | 38 +++++++++++++++++---------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/.github/workflows/generate-aidocs.yml b/.github/workflows/generate-aidocs.yml index 6b104cc..f50ddba 100644 --- a/.github/workflows/generate-aidocs.yml +++ b/.github/workflows/generate-aidocs.yml @@ -44,15 +44,22 @@ jobs: - name: Resolve review metadata id: meta shell: pwsh + env: + INPUT_BASE: ${{ inputs.base_branch }} + INPUT_LABEL: ${{ inputs.release_name }} run: | - $base = '${{ inputs.base_branch }}' + $base = $env:INPUT_BASE if ([string]::IsNullOrWhiteSpace($base)) { $base = 'main' } - $label = '${{ inputs.release_name }}' + $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 @@ -65,9 +72,12 @@ jobs: # ── 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 ${{ steps.meta.outputs.base }} - git checkout -B "${{ steps.meta.outputs.branch }}" + git checkout $env:BASE + git checkout -B $env:BRANCH # ── 4. Download required tooling and SDK artifacts ─────────────────────── - name: Download C2M4AI @@ -87,15 +97,11 @@ jobs: # ── 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 = '.' } - - if ('${{ inputs.force_regenerate }}' -eq 'true') { - $params.All = $true - $params.Force = $true - } else { - $params.All = $true - } + $params = @{ RootDir = '.'; All = $true } + if ($env:FORCE -eq 'true') { $params.Force = $true } .\scripts\ai-docs-scripts\Invoke-AiDocsPipeline.ps1 @params @@ -103,6 +109,8 @@ jobs: - 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]" @@ -111,7 +119,7 @@ jobs: $staged = git diff --cached --stat if ($staged) { - git commit -m "docs: regenerate AI docs ${{ steps.meta.outputs.label }} [skip ci]" + 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 { @@ -123,7 +131,9 @@ jobs: - name: Push review branch if: steps.commit.outputs.changed == 'true' shell: pwsh - run: git push origin "${{ steps.meta.outputs.branch }}" + 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