From 7d59bff13feb783ecc7bb319111d60a4e70c7db5 Mon Sep 17 00:00:00 2001 From: Josh Crites Date: Thu, 8 Jan 2026 16:39:38 -0500 Subject: [PATCH] feat: Add Claude-powered documentation suggestions tool Adds a nightly job that scans aztec-packages for source code changes and generates suggestions for documentation updates using Claude. Features: - Scans recent commits for file changes on the configured branch - Identifies docs with `references:` frontmatter pointing to changed files - Uses Claude to analyze changes and suggest documentation updates - Generates prioritized markdown reports for DevRel review - Reports can be fed directly to Claude Code for making updates Configuration: - GITHUB_TOKEN: Required for GitHub API access - ANTHROPIC_API_KEY: Required for Claude API - BRANCH: Target branch (default: next for aztec-packages) - LOOKBACK_DAYS: Days to scan (default: 7) Co-Authored-By: Claude Opus 4.5 --- .github/workflows/doc-suggestions.yml | 108 ++ .gitignore | 1 + reports/.gitkeep | 0 tooling/doc-suggestions/.gitignore | 23 + tooling/doc-suggestions/README.md | 239 ++++ tooling/doc-suggestions/package-lock.json | 1123 +++++++++++++++++ tooling/doc-suggestions/package.json | 25 + tooling/doc-suggestions/reports/.gitkeep | 0 tooling/doc-suggestions/src/claude-client.ts | 252 ++++ tooling/doc-suggestions/src/github-scanner.ts | 311 +++++ tooling/doc-suggestions/src/index.ts | 185 +++ .../doc-suggestions/src/reference-analyzer.ts | 256 ++++ .../doc-suggestions/src/report-generator.ts | 188 +++ tooling/doc-suggestions/tsconfig.json | 20 + 14 files changed, 2731 insertions(+) create mode 100644 .github/workflows/doc-suggestions.yml create mode 100644 reports/.gitkeep create mode 100644 tooling/doc-suggestions/.gitignore create mode 100644 tooling/doc-suggestions/README.md create mode 100644 tooling/doc-suggestions/package-lock.json create mode 100644 tooling/doc-suggestions/package.json create mode 100644 tooling/doc-suggestions/reports/.gitkeep create mode 100644 tooling/doc-suggestions/src/claude-client.ts create mode 100644 tooling/doc-suggestions/src/github-scanner.ts create mode 100644 tooling/doc-suggestions/src/index.ts create mode 100644 tooling/doc-suggestions/src/reference-analyzer.ts create mode 100644 tooling/doc-suggestions/src/report-generator.ts create mode 100644 tooling/doc-suggestions/tsconfig.json diff --git a/.github/workflows/doc-suggestions.yml b/.github/workflows/doc-suggestions.yml new file mode 100644 index 00000000..4bf4ed9f --- /dev/null +++ b/.github/workflows/doc-suggestions.yml @@ -0,0 +1,108 @@ +name: Generate Documentation Suggestions + +on: + schedule: + # Run nightly at 2 AM UTC + - cron: '0 2 * * *' + workflow_dispatch: + inputs: + lookback_days: + description: 'Number of days to look back for changes' + default: '7' + type: string + +jobs: + generate: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - name: Checkout dev-rel + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: tooling/doc-suggestions/package-lock.json + + - name: Install dependencies + working-directory: tooling/doc-suggestions + run: npm install + + - name: Generate suggestions report + working-directory: tooling/doc-suggestions + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + LOOKBACK_DAYS: ${{ inputs.lookback_days || '7' }} + run: npx tsx src/index.ts + + - name: Upload report as artifact + uses: actions/upload-artifact@v4 + with: + name: doc-suggestions-${{ github.run_id }} + path: tooling/doc-suggestions/reports/ + retention-days: 30 + + - name: Create PR with report + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Check if latest.md exists and has content + if [ ! -f tooling/doc-suggestions/reports/latest.md ]; then + echo "No report generated" + exit 0 + fi + + DATE=$(date +%Y-%m-%d) + BRANCH_NAME="docs/suggestions-${DATE}" + + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + # Create new branch + git checkout -b "$BRANCH_NAME" + + # Copy to dated report in repo root reports directory + mkdir -p reports + cp tooling/doc-suggestions/reports/latest.md "reports/doc-suggestions-${DATE}.md" + + # Stage changes + git add reports/ + git add tooling/doc-suggestions/reports/ + + # Only proceed if there are changes + if git diff --staged --quiet; then + echo "No changes to commit" + exit 0 + fi + + git commit -m "docs: Add documentation suggestions for ${DATE}" + git push -u origin "$BRANCH_NAME" + + # Count suggestions by priority + HIGH=$(grep -c "^### High Priority" "reports/doc-suggestions-${DATE}.md" || echo "0") + MEDIUM=$(grep -c "^### Medium Priority" "reports/doc-suggestions-${DATE}.md" || echo "0") + LOW=$(grep -c "^### Low Priority" "reports/doc-suggestions-${DATE}.md" || echo "0") + + # Create PR and assign to DevRel team + gh pr create \ + --title "docs: Documentation suggestions for ${DATE}" \ + --body "## Documentation Update Suggestions + + This PR contains automatically generated suggestions for documentation updates based on recent source code changes in aztec-packages. + + **Review the report**: \`reports/doc-suggestions-${DATE}.md\` + + ### How to use + 1. Review the suggestions in the report + 2. For items to address, copy the relevant section + 3. Open Claude Code in aztec-packages and paste the suggestion + + --- + 🤖 Generated by the doc-suggestions tool" \ + --assignee "@AztecProtocol/devrel" diff --git a/.gitignore b/.gitignore index 19dfa3fc..791208a2 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ proofs debug_*.json crs yarn-error.log +.env \ No newline at end of file diff --git a/reports/.gitkeep b/reports/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/tooling/doc-suggestions/.gitignore b/tooling/doc-suggestions/.gitignore new file mode 100644 index 00000000..7cd80ff0 --- /dev/null +++ b/tooling/doc-suggestions/.gitignore @@ -0,0 +1,23 @@ +# Dependencies +node_modules/ + +# Build output +dist/ + +# Reports (except .gitkeep) +reports/* +!reports/.gitkeep + +# Environment files +.env +.env.local + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db diff --git a/tooling/doc-suggestions/README.md b/tooling/doc-suggestions/README.md new file mode 100644 index 00000000..520bb15a --- /dev/null +++ b/tooling/doc-suggestions/README.md @@ -0,0 +1,239 @@ +# Documentation Suggestions Tool + +A Claude-powered tool that scans the `aztec-packages` repository for source code changes and generates suggestions for documentation updates. + +## Overview + +This tool addresses a common DevRel challenge: keeping documentation in sync with rapidly evolving source code. It: + +1. **Scans** recent commits in `aztec-packages` for file changes +2. **Identifies** documentation files that reference those changed files +3. **Analyzes** the changes using Claude to determine if docs need updating +4. **Generates** a prioritized markdown report with specific suggestions + +The output is designed to be fed directly to Claude Code for making the actual documentation updates. + +## How It Works + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Nightly Cron (2 AM UTC) │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ GitHub Scanner │ +│ - Fetches commits from configured branch (default: next) │ +│ - Gets file changes and associated PRs │ +│ - Scans docs/ directory for `references:` frontmatter │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ Reference Analyzer │ +│ - Matches changed files to doc references │ +│ - Identifies stale docs (source newer than doc) │ +│ - Calculates staleness metrics │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ Claude Analysis │ +│ - Reviews source diff + current doc content │ +│ - Determines if update is needed │ +│ - Generates specific update suggestions │ +│ - Assigns priority (high/medium/low) │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ Report Generator │ +│ - Creates markdown report grouped by priority │ +│ - Includes copy-paste sections for Claude Code │ +│ - Saves to reports/ directory │ +└─────────────────────────────────────────────────────────────────┘ +``` + +## Documentation References + +The tool relies on documentation files having a `references` field in their YAML frontmatter: + +```markdown +--- +title: CLI Reference +references: ["yarn-project/aztec/src/cli/cli.ts", "yarn-project/aztec/src/cli/cmds/*.ts"] +--- + +# CLI Reference + +... +``` + +When files matching these references change, the tool flags the documentation for review. + +## Installation + +```bash +cd tooling/doc-suggestions +npm install +``` + +## Usage + +### Local Development + +```bash +# Set required environment variables +export GITHUB_TOKEN="ghp_your_token_here" +export ANTHROPIC_API_KEY="sk-ant-your_key_here" + +# Optional: customize settings +export LOOKBACK_DAYS=14 # Default: 7 days +export BRANCH=next # Default: next (aztec-packages main branch) + +# Run the tool +npx tsx src/index.ts +``` + +### GitHub Actions + +The tool runs automatically via GitHub Actions: + +- **Schedule**: Daily at 2 AM UTC +- **Manual**: Trigger via Actions tab with optional lookback days parameter + +Reports are: +1. Uploaded as workflow artifacts (retained 30 days) +2. Committed to the `reports/` directory in the repo + +## Configuration + +| Environment Variable | Required | Default | Description | +|---------------------|----------|---------|-------------| +| `GITHUB_TOKEN` | Yes | - | GitHub API token for reading aztec-packages | +| `ANTHROPIC_API_KEY` | Yes | - | Claude API key for generating suggestions | +| `LOOKBACK_DAYS` | No | `7` | Number of days to scan for changes | +| `OUTPUT_DIR` | No | `./reports` | Directory for output reports | +| `REPO` | No | `aztec-packages` | Repository to scan | +| `BRANCH` | No | `next` | Branch to scan (aztec-packages uses `next` as main) | + +## Output Format + +Reports are generated as markdown files with the following structure: + +```markdown +# Documentation Update Suggestions + +Generated: 2024-01-15T02:00:00Z +Scan period: Last 7 days + +## Summary +- High priority: 2 +- Medium priority: 3 +- Low priority: 1 + +## Suggestions + +### High Priority + +#### `docs/docs-network/reference/cli_reference.md` + +**Source file**: `yarn-project/aztec/src/cli/cli.ts` +**Related PR**: #12345 + +**What changed**: A new `--network` CLI flag was added... + +**Suggested updates**: +- Add `--network` flag to the CLI reference table +- Include description of valid network values +``` + +## Using Suggestions with Claude Code + +1. Open the generated report +2. Find a suggestion you want to address +3. Expand the "Copy for Claude Code" section +4. Open Claude Code in the `aztec-packages` repo +5. Paste the suggestion and ask Claude to make the update + +Example prompt: +``` +The following documentation needs updating based on source code changes: + +Documentation file: docs/docs-network/reference/cli_reference.md +Source file: yarn-project/aztec/src/cli/cli.ts +Related PR: https://github.com/AztecProtocol/aztec-packages/pull/12345 + +What changed: A new --network CLI flag was added with options for selecting the target network. + +Please make these updates to the documentation: +- Add --network flag to the CLI reference table +- Include description of valid network values +- Add example usage showing network selection +``` + +## Priority Levels + +| Priority | Criteria | +|----------|----------| +| **High** | Breaking changes, new required parameters, security-related changes, major feature additions | +| **Medium** | New optional features, changed defaults, significant API changes | +| **Low** | Minor changes, internal refactoring visible to users, cosmetic changes | + +## Cost Estimation + +- **Model**: Claude Sonnet +- **Per suggestion**: ~$0.015 +- **Typical nightly run**: 5-15 suggestions +- **Estimated monthly cost**: $2-10 + +## Expanding Coverage + +To make this tool more useful, add `references` frontmatter to more documentation files in `aztec-packages`. The tool scans all markdown files in the `docs/` directory. + +High-value targets for adding references: +- CLI reference documentation → reference the CLI source files +- API documentation → reference the API implementation files +- Tutorial/guide code examples → reference the example source files + +## Architecture + +``` +src/ +├── index.ts # Entry point, orchestrates the pipeline +├── github-scanner.ts # GitHub API interactions +├── reference-analyzer.ts # Staleness detection logic +├── claude-client.ts # Claude API wrapper +└── report-generator.ts # Markdown report generation +``` + +## Troubleshooting + +### No suggestions generated + +- Check that docs have `references:` frontmatter +- Verify the referenced files exist and have recent changes +- Ensure `LOOKBACK_DAYS` covers the period of changes +- Verify `BRANCH` is set correctly (default: `next` for aztec-packages) + +### 404 errors when scanning + +- Ensure the `BRANCH` environment variable matches the repository's main branch +- For aztec-packages, use `BRANCH=next` (the default) + +### Rate limiting + +- GitHub API: The tool fetches commit details sequentially to avoid rate limits +- Claude API: 500ms delay between suggestions + +### Authentication errors + +- `GITHUB_TOKEN`: For fine-grained PATs (starting with `github_pat_`), ensure "Contents: Read" permission is granted for the target repository +- `ANTHROPIC_API_KEY`: Must be a valid Claude API key + +## Future Enhancements + +- Slack/Discord notifications for high-priority items +- Dashboard showing suggestion trends +- Auto-expand references by analyzing `#include_code` macros +- Optional PR comments for teams that want direct integration diff --git a/tooling/doc-suggestions/package-lock.json b/tooling/doc-suggestions/package-lock.json new file mode 100644 index 00000000..de17b140 --- /dev/null +++ b/tooling/doc-suggestions/package-lock.json @@ -0,0 +1,1123 @@ +{ + "name": "doc-suggestions", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "doc-suggestions", + "version": "1.0.0", + "dependencies": { + "@anthropic-ai/sdk": "^0.36.0", + "gray-matter": "^4.0.3" + }, + "devDependencies": { + "@types/node": "^20.10.0", + "tsx": "^4.7.0", + "typescript": "^5.3.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.36.3", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.36.3.tgz", + "integrity": "sha512-+c0mMLxL/17yFZ4P5+U6bTWiCSFZUKJddrv01ud2aFBWnTPLdRncYV76D3q1tqfnL7aCnhRtykFnoCFzvr4U3Q==", + "license": "MIT", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + } + }, + "node_modules/@anthropic-ai/sdk/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@anthropic-ai/sdk/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "20.19.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz", + "integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.4" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", + "license": "MIT" + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "license": "MIT", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "license": "MIT", + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + } + } +} diff --git a/tooling/doc-suggestions/package.json b/tooling/doc-suggestions/package.json new file mode 100644 index 00000000..13c492af --- /dev/null +++ b/tooling/doc-suggestions/package.json @@ -0,0 +1,25 @@ +{ + "name": "doc-suggestions", + "version": "1.0.0", + "description": "Claude-powered documentation update suggestions for Aztec Protocol", + "type": "module", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "start": "tsx src/index.ts", + "lint": "eslint src/", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@anthropic-ai/sdk": "^0.36.0", + "gray-matter": "^4.0.3" + }, + "devDependencies": { + "@types/node": "^20.10.0", + "tsx": "^4.7.0", + "typescript": "^5.3.0" + }, + "engines": { + "node": ">=20.0.0" + } +} diff --git a/tooling/doc-suggestions/reports/.gitkeep b/tooling/doc-suggestions/reports/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/tooling/doc-suggestions/src/claude-client.ts b/tooling/doc-suggestions/src/claude-client.ts new file mode 100644 index 00000000..3c9b8bbf --- /dev/null +++ b/tooling/doc-suggestions/src/claude-client.ts @@ -0,0 +1,252 @@ +/** + * Claude Client - Generates documentation update suggestions using Claude API + */ + +import Anthropic from '@anthropic-ai/sdk'; +import type { StaleReference } from './reference-analyzer.js'; + +export interface DocumentationSuggestion { + docPath: string; + sourceFile: string; + changeSummary: string; + suggestedUpdates: string[]; + priority: 'high' | 'medium' | 'low'; + relevantPr: string | null; + confidence: number; +} + +interface GenerateSuggestionInput { + staleRef: StaleReference; + sourceContent: string; + docContent: string; + sourceDiff: string; +} + +const SUGGESTION_PROMPT = `You are reviewing documentation for the Aztec Protocol, a privacy-focused Layer 2 blockchain. + +A source file has changed but its corresponding documentation may be outdated. + +Source file: {source_file} +Documentation: {doc_path} + +Recent changes to source (diff): +\`\`\`diff +{source_diff} +\`\`\` + +Current documentation content: +\`\`\`markdown +{doc_content} +\`\`\` + +Current source file content (for context): +\`\`\` +{source_content} +\`\`\` + +Analyze the changes and determine if documentation updates are needed. + +Consider: +1. Are there new features, functions, or CLI flags that should be documented? +2. Have any default values, behaviors, or APIs changed? +3. Are there renamed or deprecated items? +4. Do code examples in the docs still work with the new source? + +If updates are needed, be specific about: +- Which sections of the documentation need changes +- What specific information should be added, modified, or removed +- Why this change matters to users + +Output your analysis in the following JSON format: +{ + "needs_update": true/false, + "change_summary": "1-2 sentence summary of what changed in the source", + "suggested_updates": [ + "Specific update 1", + "Specific update 2" + ], + "priority": "high/medium/low", + "confidence": 0.0-1.0, + "reasoning": "Brief explanation of why this priority was assigned" +} + +Priority guidelines: +- high: Breaking changes, new required parameters, security-related changes, or major feature additions +- medium: New optional features, changed defaults, or significant API changes +- low: Minor changes, internal refactoring that doesn't affect user-facing behavior, or cosmetic changes + +If the changes don't affect documentation (e.g., internal refactoring, test changes), set needs_update to false.`; + +export class ClaudeClient { + private client: Anthropic; + private model: string; + + constructor(apiKey: string, model: string = 'claude-sonnet-4-20250514') { + this.client = new Anthropic({ apiKey }); + this.model = model; + } + + /** + * Generate a documentation suggestion for a stale reference + */ + async generateSuggestion(input: GenerateSuggestionInput): Promise { + const { staleRef, sourceContent, docContent, sourceDiff } = input; + + // Build the prompt + const prompt = SUGGESTION_PROMPT + .replace('{source_file}', staleRef.sourceFile) + .replace('{doc_path}', staleRef.docPath) + .replace('{source_diff}', sourceDiff || 'No diff available') + .replace('{doc_content}', truncateContent(docContent, 4000)) + .replace('{source_content}', truncateContent(sourceContent, 3000)); + + try { + const response = await this.client.messages.create({ + model: this.model, + max_tokens: 1024, + messages: [ + { + role: 'user', + content: prompt, + }, + ], + }); + + // Extract text content from response + const textContent = response.content.find((block) => block.type === 'text'); + if (!textContent || textContent.type !== 'text') { + console.warn(`No text response for ${staleRef.docPath}`); + return null; + } + + // Parse the JSON response + const analysis = parseAnalysis(textContent.text); + if (!analysis || !analysis.needs_update) { + return null; + } + + // Find the most relevant PR + const relevantPr = staleRef.recentSourceChanges.find((c) => c.pr_number)?.pr_number; + + return { + docPath: staleRef.docPath, + sourceFile: staleRef.sourceFile, + changeSummary: analysis.change_summary, + suggestedUpdates: analysis.suggested_updates, + priority: analysis.priority, + relevantPr: relevantPr ? `#${relevantPr}` : null, + confidence: analysis.confidence, + }; + } catch (error) { + console.error(`Failed to generate suggestion for ${staleRef.docPath}:`, error); + return null; + } + } + + /** + * Generate suggestions for multiple stale references + */ + async generateSuggestions( + staleRefs: StaleReference[], + getContent: (path: string) => Promise, + getDiff: (path: string) => Promise + ): Promise { + const suggestions: DocumentationSuggestion[] = []; + + console.log(`Generating suggestions for ${staleRefs.length} stale references...`); + + for (const staleRef of staleRefs) { + try { + console.log(` Analyzing ${staleRef.docPath}...`); + + // Fetch content in parallel + const [sourceContent, docContent, sourceDiff] = await Promise.all([ + getContent(staleRef.sourceFile).catch(() => ''), + getContent(staleRef.docPath).catch(() => ''), + getDiff(staleRef.sourceFile).catch(() => ''), + ]); + + if (!docContent) { + console.warn(` Could not fetch doc content for ${staleRef.docPath}`); + continue; + } + + const suggestion = await this.generateSuggestion({ + staleRef, + sourceContent, + docContent, + sourceDiff, + }); + + if (suggestion) { + suggestions.push(suggestion); + console.log(` Found ${suggestion.priority} priority update needed`); + } else { + console.log(` No update needed`); + } + + // Rate limiting - be nice to the API + await sleep(500); + } catch (error) { + console.error(` Error processing ${staleRef.docPath}:`, error); + } + } + + return suggestions; + } +} + +/** + * Parse the JSON analysis from Claude's response + */ +function parseAnalysis(text: string): { + needs_update: boolean; + change_summary: string; + suggested_updates: string[]; + priority: 'high' | 'medium' | 'low'; + confidence: number; +} | null { + try { + // Try to extract JSON from the response + const jsonMatch = text.match(/\{[\s\S]*\}/); + if (!jsonMatch) { + console.warn('No JSON found in response'); + return null; + } + + const parsed = JSON.parse(jsonMatch[0]); + + // Validate required fields + if (typeof parsed.needs_update !== 'boolean') { + return null; + } + + return { + needs_update: parsed.needs_update, + change_summary: parsed.change_summary || '', + suggested_updates: Array.isArray(parsed.suggested_updates) ? parsed.suggested_updates : [], + priority: ['high', 'medium', 'low'].includes(parsed.priority) ? parsed.priority : 'low', + confidence: typeof parsed.confidence === 'number' ? parsed.confidence : 0.5, + }; + } catch (error) { + console.warn('Failed to parse analysis JSON:', error); + return null; + } +} + +/** + * Truncate content to a maximum length, keeping the beginning + */ +function truncateContent(content: string, maxLength: number): string { + if (content.length <= maxLength) { + return content; + } + return content.slice(0, maxLength) + '\n\n[... content truncated ...]'; +} + +/** + * Sleep for a specified number of milliseconds + */ +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/tooling/doc-suggestions/src/github-scanner.ts b/tooling/doc-suggestions/src/github-scanner.ts new file mode 100644 index 00000000..e86c5b8d --- /dev/null +++ b/tooling/doc-suggestions/src/github-scanner.ts @@ -0,0 +1,311 @@ +/** + * GitHub Scanner - Scans aztec-packages repository for recent changes + * to files that are referenced by documentation. + */ + +export interface RecentChange { + sha: string; + date: string; + author: string; + message: string; + files: ChangedFile[]; + pr_number?: number; + pr_title?: string; +} + +export interface ChangedFile { + filename: string; + status: string; + additions: number; + deletions: number; + patch?: string; +} + +export interface DocReference { + docPath: string; + references: string[]; + lastModified?: string; +} + +interface GitHubCommit { + sha: string; + commit: { + author: { + name: string; + date: string; + }; + message: string; + }; +} + +interface GitHubCommitDetail { + sha: string; + files?: Array<{ + filename: string; + status: string; + additions: number; + deletions: number; + patch?: string; + }>; +} + +interface GitHubPR { + number: number; + title: string; +} + +interface GitHubTreeItem { + path: string; + type: string; +} + +export class GitHubScanner { + private baseUrl = 'https://api.github.com'; + private owner = 'AztecProtocol'; + + constructor( + private token: string, + private repo: string = 'aztec-packages', + private lookbackDays: number = 7, + private branch: string = 'next' + ) {} + + private async fetch(url: string): Promise { + const response = await fetch(url, { + headers: { + Authorization: `Bearer ${this.token}`, + Accept: 'application/vnd.github.v3+json', + 'User-Agent': 'doc-suggestions-scanner', + }, + }); + + if (!response.ok) { + const errorBody = await response.text(); + const rateLimit = response.headers.get('x-ratelimit-remaining'); + const rateLimitReset = response.headers.get('x-ratelimit-reset'); + + let errorMsg = `GitHub API error: ${response.status} ${response.statusText}`; + if (errorBody) { + errorMsg += `\nResponse: ${errorBody}`; + } + if (rateLimit === '0' && rateLimitReset) { + const resetDate = new Date(parseInt(rateLimitReset) * 1000); + errorMsg += `\nRate limit exceeded. Resets at: ${resetDate.toISOString()}`; + } + throw new Error(errorMsg); + } + + return response.json() as Promise; + } + + /** + * Get recent commits from the repository + */ + async getRecentCommits(): Promise { + const since = new Date(); + since.setDate(since.getDate() - this.lookbackDays); + + console.log(`Fetching commits since ${since.toISOString()}...`); + + const commits = await this.fetch( + `${this.baseUrl}/repos/${this.owner}/${this.repo}/commits?sha=${this.branch}&since=${since.toISOString()}&per_page=100` + ); + + console.log(`Found ${commits.length} commits`); + + // Enrich commits with file information and PR data + const enrichedCommits = await this.enrichCommits(commits); + + return enrichedCommits; + } + + /** + * Enrich commits with file changes and PR information + */ + private async enrichCommits(commits: GitHubCommit[]): Promise { + const enriched: RecentChange[] = []; + + for (const commit of commits) { + try { + // Get detailed commit info including files + const detail = await this.fetch( + `${this.baseUrl}/repos/${this.owner}/${this.repo}/commits/${commit.sha}` + ); + + // Try to find associated PR + const pr = await this.findAssociatedPR(commit.sha); + + const files: ChangedFile[] = (detail.files || []).map((f) => ({ + filename: f.filename, + status: f.status, + additions: f.additions, + deletions: f.deletions, + patch: f.patch, + })); + + enriched.push({ + sha: commit.sha, + date: commit.commit.author.date, + author: commit.commit.author.name, + message: commit.commit.message.split('\n')[0], // First line only + files, + pr_number: pr?.number, + pr_title: pr?.title, + }); + } catch (error) { + console.warn(`Failed to enrich commit ${commit.sha}:`, error); + } + } + + return enriched; + } + + /** + * Find the PR associated with a commit + */ + private async findAssociatedPR(sha: string): Promise { + try { + const prs = await this.fetch( + `${this.baseUrl}/repos/${this.owner}/${this.repo}/commits/${sha}/pulls` + ); + + return prs.length > 0 ? prs[0] : null; + } catch { + return null; + } + } + + /** + * Get all documentation files that have references frontmatter + */ + async getDocReferences(): Promise { + console.log('Scanning for documentation files with references...'); + + // Get all markdown files in docs directories + const docPaths = await this.findDocFiles(); + const references: DocReference[] = []; + + for (const path of docPaths) { + try { + const content = await this.getFileContent(path); + const refs = this.parseReferences(content); + + if (refs.length > 0) { + const lastModified = await this.getFileLastModified(path); + references.push({ + docPath: path, + references: refs, + lastModified, + }); + } + } catch (error) { + // File might not exist or be inaccessible + console.warn(`Failed to process ${path}:`, error); + } + } + + console.log(`Found ${references.length} docs with references`); + return references; + } + + /** + * Find all markdown files in documentation directories + */ + private async findDocFiles(): Promise { + const docPaths: string[] = []; + + // Try to scan the entire docs directory recursively + try { + console.log(` Scanning docs/ directory on branch '${this.branch}'...`); + const tree = await this.fetch<{ tree: GitHubTreeItem[] }>( + `${this.baseUrl}/repos/${this.owner}/${this.repo}/git/trees/${this.branch}:docs?recursive=1` + ); + + for (const item of tree.tree) { + if (item.type === 'blob' && item.path.endsWith('.md')) { + docPaths.push(`docs/${item.path}`); + } + } + + console.log(` Found ${docPaths.length} markdown files in docs/`); + } catch (error) { + console.warn(`Failed to scan docs/:`, error); + } + + return docPaths; + } + + /** + * Get file content from the repository + */ + async getFileContent(path: string): Promise { + const response = await this.fetch<{ content: string; encoding: string }>( + `${this.baseUrl}/repos/${this.owner}/${this.repo}/contents/${path}?ref=${this.branch}` + ); + + if (response.encoding === 'base64') { + return Buffer.from(response.content, 'base64').toString('utf-8'); + } + + return response.content; + } + + /** + * Get the last modified date of a file + */ + private async getFileLastModified(path: string): Promise { + try { + const commits = await this.fetch( + `${this.baseUrl}/repos/${this.owner}/${this.repo}/commits?path=${path}&sha=${this.branch}&per_page=1` + ); + + return commits.length > 0 ? commits[0].commit.author.date : undefined; + } catch { + return undefined; + } + } + + /** + * Parse references from markdown frontmatter + */ + private parseReferences(content: string): string[] { + // Match YAML frontmatter + const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); + if (!frontmatterMatch) { + return []; + } + + const frontmatter = frontmatterMatch[1]; + + // Look for references field + const referencesMatch = frontmatter.match(/references:\s*\[([\s\S]*?)\]/); + if (!referencesMatch) { + return []; + } + + // Parse the array of references + const refsString = referencesMatch[1]; + const refs = refsString + .split(',') + .map((ref) => ref.trim().replace(/['"]/g, '')) + .filter((ref) => ref.length > 0); + + return refs; + } + + /** + * Get the diff for a specific file between two commits + */ + async getFileDiff(path: string, baseSha: string, headSha: string = 'HEAD'): Promise { + try { + const comparison = await this.fetch<{ files?: Array<{ filename: string; patch?: string }> }>( + `${this.baseUrl}/repos/${this.owner}/${this.repo}/compare/${baseSha}...${headSha}` + ); + + const file = comparison.files?.find((f) => f.filename === path); + return file?.patch || ''; + } catch { + return ''; + } + } +} diff --git a/tooling/doc-suggestions/src/index.ts b/tooling/doc-suggestions/src/index.ts new file mode 100644 index 00000000..9ff9d6ce --- /dev/null +++ b/tooling/doc-suggestions/src/index.ts @@ -0,0 +1,185 @@ +#!/usr/bin/env node +/** + * Documentation Suggestions Tool + * + * Scans aztec-packages for recent changes to files referenced by documentation, + * then generates suggestions for documentation updates using Claude. + */ + +import * as fs from 'node:fs/promises'; +import * as path from 'node:path'; +import { GitHubScanner } from './github-scanner.js'; +import { findStaleReferences, getChangeStatistics } from './reference-analyzer.js'; +import { ClaudeClient } from './claude-client.js'; +import { generateReport, generateReportFilename, generateNotificationSummary } from './report-generator.js'; + +interface Config { + githubToken: string; + anthropicApiKey: string; + lookbackDays: number; + outputDir: string; + repo: string; + branch: string; +} + +function loadConfig(): Config { + const githubToken = process.env.GITHUB_TOKEN; + const anthropicApiKey = process.env.ANTHROPIC_API_KEY; + const lookbackDays = parseInt(process.env.LOOKBACK_DAYS || '7', 10); + const outputDir = process.env.OUTPUT_DIR || './reports'; + const repo = process.env.REPO || 'aztec-packages'; + const branch = process.env.BRANCH || 'next'; + + if (!githubToken) { + throw new Error('GITHUB_TOKEN environment variable is required'); + } + + if (!anthropicApiKey) { + throw new Error('ANTHROPIC_API_KEY environment variable is required'); + } + + return { + githubToken, + anthropicApiKey, + lookbackDays, + outputDir, + repo, + branch, + }; +} + +async function main() { + console.log('=== Documentation Suggestions Tool ===\n'); + + // Load configuration + const config = loadConfig(); + console.log(`Configuration:`); + console.log(` Repository: ${config.repo}`); + console.log(` Branch: ${config.branch}`); + console.log(` Lookback period: ${config.lookbackDays} days`); + console.log(` Output directory: ${config.outputDir}\n`); + + // Initialize clients + const scanner = new GitHubScanner(config.githubToken, config.repo, config.lookbackDays, config.branch); + const claude = new ClaudeClient(config.anthropicApiKey); + + // Step 1: Get recent commits + console.log('Step 1: Fetching recent commits...'); + const recentChanges = await scanner.getRecentCommits(); + const stats = getChangeStatistics(recentChanges); + console.log(` Found ${stats.totalCommits} commits, ${stats.totalFilesChanged} files changed`); + console.log(` ${stats.uniqueAuthors} authors, ${stats.prsIncluded} PRs\n`); + + if (recentChanges.length === 0) { + console.log('No recent changes found. Exiting.'); + return; + } + + // Step 2: Get documentation references + console.log('Step 2: Scanning documentation for references...'); + const docReferences = await scanner.getDocReferences(); + console.log(` Found ${docReferences.length} docs with references\n`); + + if (docReferences.length === 0) { + console.log('No documentation with references found.'); + console.log('Consider adding `references: ["path/to/file"]` frontmatter to docs.\n'); + + // Still generate a report (empty) + await generateEmptyReport(config); + return; + } + + // Step 3: Find stale references + console.log('Step 3: Analyzing for stale references...'); + const analysisResult = findStaleReferences(docReferences, recentChanges); + console.log(` Found ${analysisResult.staleReferences.length} potentially stale references\n`); + + if (analysisResult.staleReferences.length === 0) { + console.log('All documentation is up to date!'); + await generateEmptyReport(config); + return; + } + + // Step 4: Generate suggestions with Claude + console.log('Step 4: Generating suggestions with Claude...'); + const suggestions = await claude.generateSuggestions( + analysisResult.staleReferences, + async (filePath) => scanner.getFileContent(filePath), + async (filePath) => { + // Get diff from the oldest change date + const oldestChange = analysisResult.staleReferences + .flatMap((r) => r.recentSourceChanges) + .reduce((oldest, change) => { + const changeDate = new Date(change.date); + const oldestDate = oldest ? new Date(oldest.date) : new Date(); + return changeDate < oldestDate ? change : oldest; + }); + + if (oldestChange) { + return scanner.getFileDiff(filePath, `${oldestChange.sha}~1`, 'HEAD'); + } + return ''; + } + ); + + console.log(`\n Generated ${suggestions.length} suggestions\n`); + + // Step 5: Generate report + console.log('Step 5: Generating report...'); + const scanDate = new Date(); + const report = generateReport(suggestions, { + scanDate, + lookbackDays: config.lookbackDays, + analysisResult, + }); + + // Ensure output directory exists + await fs.mkdir(config.outputDir, { recursive: true }); + + // Write timestamped report + const filename = generateReportFilename(scanDate); + const filepath = path.join(config.outputDir, filename); + await fs.writeFile(filepath, report, 'utf-8'); + console.log(` Written to: ${filepath}`); + + // Also write a "latest" report for easy access + const latestPath = path.join(config.outputDir, 'latest.md'); + await fs.writeFile(latestPath, report, 'utf-8'); + console.log(` Latest report: ${latestPath}\n`); + + // Print summary + const summary = generateNotificationSummary(suggestions); + console.log('=== Summary ==='); + console.log(summary); + console.log('\nDone!'); +} + +async function generateEmptyReport(config: Config) { + const scanDate = new Date(); + const report = generateReport([], { + scanDate, + lookbackDays: config.lookbackDays, + analysisResult: { + staleReferences: [], + totalDocsAnalyzed: 0, + totalReferencesChecked: 0, + scanPeriodDays: config.lookbackDays, + }, + }); + + await fs.mkdir(config.outputDir, { recursive: true }); + + const filename = generateReportFilename(scanDate); + const filepath = path.join(config.outputDir, filename); + await fs.writeFile(filepath, report, 'utf-8'); + console.log(`Empty report written to: ${filepath}`); + + const latestPath = path.join(config.outputDir, 'latest.md'); + await fs.writeFile(latestPath, report, 'utf-8'); +} + +// Run the main function +main().catch((error) => { + console.error('Error:', error); + process.exit(1); +}); diff --git a/tooling/doc-suggestions/src/reference-analyzer.ts b/tooling/doc-suggestions/src/reference-analyzer.ts new file mode 100644 index 00000000..b1941abd --- /dev/null +++ b/tooling/doc-suggestions/src/reference-analyzer.ts @@ -0,0 +1,256 @@ +/** + * Reference Analyzer - Determines which documentation files need attention + * based on recent changes to their referenced source files. + */ + +import type { RecentChange, DocReference } from './github-scanner.js'; + +export interface StaleReference { + docPath: string; + sourceFile: string; + lastDocUpdate: string | undefined; + recentSourceChanges: RecentChange[]; + stalenessDays: number; +} + +export interface AnalysisResult { + staleReferences: StaleReference[]; + totalDocsAnalyzed: number; + totalReferencesChecked: number; + scanPeriodDays: number; +} + +/** + * Find documentation that may be stale due to recent source code changes + */ +export function findStaleReferences( + docReferences: DocReference[], + recentChanges: RecentChange[] +): AnalysisResult { + const staleReferences: StaleReference[] = []; + let totalReferencesChecked = 0; + + // Build a map of changed files to their changes for quick lookup + const fileChangesMap = buildFileChangesMap(recentChanges); + + for (const doc of docReferences) { + for (const refPath of doc.references) { + totalReferencesChecked++; + + // Check if any changed file matches this reference + const matchingChanges = findMatchingChanges(refPath, fileChangesMap); + + if (matchingChanges.length > 0) { + // Check if doc was updated after the source changes + const isStale = checkIfStale(doc.lastModified, matchingChanges); + + if (isStale) { + const stalenessDays = calculateStalenessDays(doc.lastModified, matchingChanges); + + staleReferences.push({ + docPath: doc.docPath, + sourceFile: refPath, + lastDocUpdate: doc.lastModified, + recentSourceChanges: matchingChanges, + stalenessDays, + }); + } + } + } + } + + // Sort by staleness (most stale first) + staleReferences.sort((a, b) => b.stalenessDays - a.stalenessDays); + + // Deduplicate - keep only the most stale entry per doc + const uniqueStaleRefs = deduplicateByDoc(staleReferences); + + return { + staleReferences: uniqueStaleRefs, + totalDocsAnalyzed: docReferences.length, + totalReferencesChecked, + scanPeriodDays: 7, // Default lookback period + }; +} + +/** + * Build a map from file paths to their recent changes + */ +function buildFileChangesMap(changes: RecentChange[]): Map { + const map = new Map(); + + for (const change of changes) { + for (const file of change.files) { + const existing = map.get(file.filename) || []; + existing.push(change); + map.set(file.filename, existing); + } + } + + return map; +} + +/** + * Find changes that match a reference path (supports glob-like patterns) + */ +function findMatchingChanges( + refPath: string, + fileChangesMap: Map +): RecentChange[] { + const matchingChanges: RecentChange[] = []; + + // Normalize the reference path + const normalizedRef = normalizePath(refPath); + + for (const [filePath, changes] of fileChangesMap) { + if (pathMatches(normalizedRef, filePath)) { + matchingChanges.push(...changes); + } + } + + // Deduplicate by SHA + const uniqueChanges = Array.from( + new Map(matchingChanges.map((c) => [c.sha, c])).values() + ); + + return uniqueChanges; +} + +/** + * Normalize a path for matching + */ +function normalizePath(path: string): string { + // Remove leading slashes and normalize + return path.replace(/^\/+/, '').replace(/\/+/g, '/'); +} + +/** + * Check if a reference path matches a changed file path + * Supports basic glob patterns (* and **) + */ +function pathMatches(refPath: string, changedPath: string): boolean { + // Exact match + if (refPath === changedPath) { + return true; + } + + // Handle glob patterns + if (refPath.includes('*')) { + const regex = globToRegex(refPath); + return regex.test(changedPath); + } + + // Check if ref is a prefix (directory reference) + if (changedPath.startsWith(refPath + '/')) { + return true; + } + + // Check if ref ends with the changed path (relative reference) + if (changedPath.endsWith(refPath)) { + return true; + } + + return false; +} + +/** + * Convert a glob pattern to a regex + */ +function globToRegex(glob: string): RegExp { + const escaped = glob + .replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape regex special chars + .replace(/\*\*/g, '{{GLOBSTAR}}') // Temporarily replace ** + .replace(/\*/g, '[^/]*') // * matches anything except / + .replace(/{{GLOBSTAR}}/g, '.*'); // ** matches anything + + return new RegExp(`^${escaped}$`); +} + +/** + * Check if documentation is stale compared to source changes + */ +function checkIfStale( + docLastModified: string | undefined, + sourceChanges: RecentChange[] +): boolean { + if (!docLastModified) { + // If we don't know when doc was last modified, assume it might be stale + return true; + } + + const docDate = new Date(docLastModified); + + // Check if any source change is newer than the doc + for (const change of sourceChanges) { + const changeDate = new Date(change.date); + if (changeDate > docDate) { + return true; + } + } + + return false; +} + +/** + * Calculate how many days the documentation is stale + */ +function calculateStalenessDays( + docLastModified: string | undefined, + sourceChanges: RecentChange[] +): number { + if (!docLastModified || sourceChanges.length === 0) { + return 0; + } + + const docDate = new Date(docLastModified); + + // Find the most recent source change + const latestChange = sourceChanges.reduce((latest, change) => { + const changeDate = new Date(change.date); + const latestDate = new Date(latest.date); + return changeDate > latestDate ? change : latest; + }); + + const latestChangeDate = new Date(latestChange.date); + const diffMs = latestChangeDate.getTime() - docDate.getTime(); + const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24)); + + return Math.max(0, diffDays); +} + +/** + * Deduplicate stale references, keeping the most stale entry per doc + */ +function deduplicateByDoc(staleRefs: StaleReference[]): StaleReference[] { + const byDoc = new Map(); + + for (const ref of staleRefs) { + const existing = byDoc.get(ref.docPath); + if (!existing || ref.stalenessDays > existing.stalenessDays) { + byDoc.set(ref.docPath, ref); + } + } + + return Array.from(byDoc.values()); +} + +/** + * Get aggregate statistics about source changes + */ +export function getChangeStatistics(changes: RecentChange[]): { + totalCommits: number; + totalFilesChanged: number; + uniqueAuthors: number; + prsIncluded: number; +} { + const uniqueAuthors = new Set(changes.map((c) => c.author)); + const prsIncluded = changes.filter((c) => c.pr_number).length; + const totalFilesChanged = changes.reduce((sum, c) => sum + c.files.length, 0); + + return { + totalCommits: changes.length, + totalFilesChanged, + uniqueAuthors: uniqueAuthors.size, + prsIncluded, + }; +} diff --git a/tooling/doc-suggestions/src/report-generator.ts b/tooling/doc-suggestions/src/report-generator.ts new file mode 100644 index 00000000..3628942c --- /dev/null +++ b/tooling/doc-suggestions/src/report-generator.ts @@ -0,0 +1,188 @@ +/** + * Report Generator - Creates markdown reports with documentation update suggestions + */ + +import type { DocumentationSuggestion } from './claude-client.js'; +import type { AnalysisResult } from './reference-analyzer.js'; + +export interface ReportOptions { + scanDate: Date; + lookbackDays: number; + analysisResult: AnalysisResult; +} + +/** + * Generate a markdown report with all documentation suggestions + */ +export function generateReport( + suggestions: DocumentationSuggestion[], + options: ReportOptions +): string { + const { scanDate, lookbackDays, analysisResult } = options; + + const high = suggestions.filter((s) => s.priority === 'high'); + const medium = suggestions.filter((s) => s.priority === 'medium'); + const low = suggestions.filter((s) => s.priority === 'low'); + + let report = `# Documentation Update Suggestions + +Generated: ${scanDate.toISOString()} +Scan period: Last ${lookbackDays} days + +## Summary + +| Metric | Value | +|--------|-------| +| Docs with references analyzed | ${analysisResult.totalDocsAnalyzed} | +| Total references checked | ${analysisResult.totalReferencesChecked} | +| Stale references found | ${analysisResult.staleReferences.length} | +| Suggestions generated | ${suggestions.length} | + +### By Priority + +- **High priority**: ${high.length} +- **Medium priority**: ${medium.length} +- **Low priority**: ${low.length} + +--- + +## How to Use This Report + +1. Review the suggestions below, starting with high priority items +2. For items you want to address, copy the relevant section +3. Open Claude Code in the aztec-packages repo +4. Paste the suggestion and ask Claude to make the changes + +### Example prompt for Claude Code: + +\`\`\` +The following documentation needs updating based on source code changes: + +[paste suggestion section here] + +Please update the documentation file to reflect these changes. +\`\`\` + +--- + +`; + + if (suggestions.length === 0) { + report += `## No Suggestions + +No documentation updates needed at this time. All referenced source files are either: +- Unchanged in the scan period +- Already have up-to-date documentation + +`; + return report; + } + + report += `## Suggestions + +`; + + if (high.length > 0) { + report += `### High Priority + +These documentation files may be significantly out of date with breaking changes or new features. + +`; + for (const s of high) { + report += formatSuggestion(s); + } + } + + if (medium.length > 0) { + report += `### Medium Priority + +These documentation files may need updates for new features or changed defaults. + +`; + for (const s of medium) { + report += formatSuggestion(s); + } + } + + if (low.length > 0) { + report += `### Low Priority + +These documentation files may have minor updates needed. + +`; + for (const s of low) { + report += formatSuggestion(s); + } + } + + return report; +} + +/** + * Format a single suggestion as markdown + */ +function formatSuggestion(s: DocumentationSuggestion): string { + const prLink = s.relevantPr + ? `https://github.com/AztecProtocol/aztec-packages/pull/${s.relevantPr.replace('#', '')}` + : null; + + return `#### \`${s.docPath}\` + +**Source file**: \`${s.sourceFile}\` +**Related PR**: ${prLink ? `[${s.relevantPr}](${prLink})` : 'N/A'} +**Confidence**: ${Math.round(s.confidence * 100)}% + +**What changed**: ${s.changeSummary} + +**Suggested updates**: +${s.suggestedUpdates.map((u) => `- ${u}`).join('\n')} + +
+Copy for Claude Code + +\`\`\` +Documentation file: ${s.docPath} +Source file: ${s.sourceFile} +${prLink ? `Related PR: ${prLink}` : ''} + +What changed: ${s.changeSummary} + +Please make these updates to the documentation: +${s.suggestedUpdates.map((u) => `- ${u}`).join('\n')} +\`\`\` + +
+ +--- + +`; +} + +/** + * Generate a filename for the report + */ +export function generateReportFilename(date: Date): string { + const dateStr = date.toISOString().split('T')[0]; + return `doc-suggestions-${dateStr}.md`; +} + +/** + * Generate a summary for Slack/Discord notification + */ +export function generateNotificationSummary(suggestions: DocumentationSuggestion[]): string { + const high = suggestions.filter((s) => s.priority === 'high').length; + const medium = suggestions.filter((s) => s.priority === 'medium').length; + const low = suggestions.filter((s) => s.priority === 'low').length; + const total = suggestions.length; + + if (total === 0) { + return 'No documentation updates needed today.'; + } + + const parts = []; + if (high > 0) parts.push(`${high} high`); + if (medium > 0) parts.push(`${medium} medium`); + if (low > 0) parts.push(`${low} low`); + + return `Documentation scan complete: ${total} updates suggested (${parts.join(', ')})`; +} diff --git a/tooling/doc-suggestions/tsconfig.json b/tooling/doc-suggestions/tsconfig.json new file mode 100644 index 00000000..c83c5333 --- /dev/null +++ b/tooling/doc-suggestions/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +}