From 9c1345a80e88bc3aa004cc2976f31ae27e1ea594 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 17 Aug 2026 07:51:23 +0000 Subject: [PATCH] =?UTF-8?q?Add=2010=20rig=20samples=20(431=E2=80=93440)=20?= =?UTF-8?q?=E2=80=94=202026-08-17?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Samples cover git file size tracking, JSON pretty-printer stats, TS interface method counter, git tag annotation extractor, JSONL analyzer, npm peer dep conflict checker, TS symbol frequency recorder (s.record output), CSV-to-markdown table writer (p.write pattern), git commit risk classifier (nested s.object), and TS generic constraint reporter. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../rig/samples/431-git-file-size-tracker.md | 55 +++++++++++++++ .../samples/432-json-pretty-printer-stats.md | 67 +++++++++++++++++++ .../433-ts-interface-method-counter.md | 50 ++++++++++++++ .../434-git-tag-annotation-extractor.md | 52 ++++++++++++++ skills/rig/samples/435-jsonl-file-analyzer.md | 55 +++++++++++++++ .../436-npm-peer-dep-conflict-checker.md | 64 ++++++++++++++++++ .../437-ts-symbol-frequency-recorder.md | 45 +++++++++++++ .../rig/samples/438-markdown-table-writer.md | 47 +++++++++++++ .../samples/439-git-commit-risk-classifier.md | 62 +++++++++++++++++ .../440-ts-generic-constraint-reporter.md | 61 +++++++++++++++++ 10 files changed, 558 insertions(+) create mode 100644 skills/rig/samples/431-git-file-size-tracker.md create mode 100644 skills/rig/samples/432-json-pretty-printer-stats.md create mode 100644 skills/rig/samples/433-ts-interface-method-counter.md create mode 100644 skills/rig/samples/434-git-tag-annotation-extractor.md create mode 100644 skills/rig/samples/435-jsonl-file-analyzer.md create mode 100644 skills/rig/samples/436-npm-peer-dep-conflict-checker.md create mode 100644 skills/rig/samples/437-ts-symbol-frequency-recorder.md create mode 100644 skills/rig/samples/438-markdown-table-writer.md create mode 100644 skills/rig/samples/439-git-commit-risk-classifier.md create mode 100644 skills/rig/samples/440-ts-generic-constraint-reporter.md diff --git a/skills/rig/samples/431-git-file-size-tracker.md b/skills/rig/samples/431-git-file-size-tracker.md new file mode 100644 index 0000000..e31c086 --- /dev/null +++ b/skills/rig/samples/431-git-file-size-tracker.md @@ -0,0 +1,55 @@ +# 431 - Git File Size Tracker + +```rig +import { agent, p, s, defineTool, repair } from "rig"; +import { execSync } from "node:child_process"; + +const getFileSizeAtRevision = defineTool("getFileSizeAtRevision", { + description: "Get the byte size of a file at a specific git revision.", + parameters: s.object({ revision: s.string, filePath: s.string }), + handler({ revision, filePath }: { revision: string; filePath: string }) { + try { + const size = parseInt( + execSync(`git cat-file -s ${revision}:${filePath} 2>/dev/null || echo '0'`, { encoding: "utf-8" }).trim(), + 10 + ); + return { size: isNaN(size) ? 0 : size }; + } catch { + return { size: 0 }; + } + }, +}); + +// Agent role: track file size changes between the previous commit and HEAD. +const gitFileSizeTracker = agent({ + model: "small", + instructions: p`Track file size changes between the previous commit and HEAD. + +Files changed in the last commit: +${p.bash("git diff --name-only HEAD~1 HEAD 2>/dev/null || echo ''")} + +For each changed file, call getFileSizeAtRevision with revision "HEAD~1" and again with "HEAD". +Compute delta = currentSize - previousSize. +Classify change as "grew" if delta > 0, "shrank" if delta < 0, "unchanged" if delta == 0. +Set largestGrowth to the path with highest positive delta (omit if none grew). +Set largestShrink to the path with most negative delta (omit if none shrank).`, + output: s.object({ + files: s.array( + s.object({ + path: s.path, + previousSize: s.int, + currentSize: s.int, + delta: s.int, + change: s.enum("grew", "shrank", "unchanged"), + }) + ), + totalFiles: s.int, + largestGrowth: s.optional(s.string), + largestShrink: s.optional(s.string), + }), + tools: [getFileSizeAtRevision], + addons: [repair()], +}); + +export default gitFileSizeTracker; +``` diff --git a/skills/rig/samples/432-json-pretty-printer-stats.md b/skills/rig/samples/432-json-pretty-printer-stats.md new file mode 100644 index 0000000..9f411a2 --- /dev/null +++ b/skills/rig/samples/432-json-pretty-printer-stats.md @@ -0,0 +1,67 @@ +# 432 - Json Pretty Printer Stats + +```rig +import { agent, p, s, defineTool } from "rig"; + +const analyzeJsonStructure = defineTool("analyzeJsonStructure", { + description: "Parse a JSON string and compute key count, max nesting depth, array count, and object count.", + parameters: s.object({ json: s.string }), + handler({ json }: { json: string }) { + function analyze(val: unknown, depth: number): { totalKeys: number; maxDepth: number; arrayCount: number; objectCount: number } { + if (Array.isArray(val)) { + const results = val.map((v: unknown) => analyze(v, depth + 1)); + return { + totalKeys: results.reduce((a, r) => a + r.totalKeys, 0), + maxDepth: Math.max(depth, ...results.map(r => r.maxDepth)), + arrayCount: 1 + results.reduce((a, r) => a + r.arrayCount, 0), + objectCount: results.reduce((a, r) => a + r.objectCount, 0), + }; + } else if (val !== null && typeof val === "object") { + const keys = Object.keys(val as object); + const results = keys.map(k => analyze((val as Record)[k], depth + 1)); + return { + totalKeys: keys.length + results.reduce((a, r) => a + r.totalKeys, 0), + maxDepth: Math.max(depth, ...results.map(r => r.maxDepth)), + arrayCount: results.reduce((a, r) => a + r.arrayCount, 0), + objectCount: 1 + results.reduce((a, r) => a + r.objectCount, 0), + }; + } + return { totalKeys: 0, maxDepth: depth, arrayCount: 0, objectCount: 0 }; + } + try { + const parsed = JSON.parse(json); + return analyze(parsed, 0); + } catch { + return { totalKeys: 0, maxDepth: 0, arrayCount: 0, objectCount: 0 }; + } + }, +}); + +// Agent role: pretty-print a JSON file and report structural statistics. +const jsonPrettyPrinterStats = agent({ + model: "small", + input: s.object({ inputFile: s.path, outputFile: s.optional(s.path) }), + instructions: p`Pretty-print the JSON file and report structural statistics. + +File contents: +${p.readInput("inputFile")} + +1. Call analyzeJsonStructure with the raw file content. +2. Parse the JSON and re-serialize it with 2-space indentation as "formatted". +3. If outputFile was provided, set outputWritten to true; otherwise false. +4. Return formatted, stats, and outputWritten.`, + output: s.object({ + formatted: s.string, + stats: s.object({ + totalKeys: s.int, + maxDepth: s.int, + arrayCount: s.int, + objectCount: s.int, + }), + outputWritten: s.boolean, + }), + tools: [analyzeJsonStructure], +}); + +export default jsonPrettyPrinterStats; +``` diff --git a/skills/rig/samples/433-ts-interface-method-counter.md b/skills/rig/samples/433-ts-interface-method-counter.md new file mode 100644 index 0000000..430debe --- /dev/null +++ b/skills/rig/samples/433-ts-interface-method-counter.md @@ -0,0 +1,50 @@ +# 433 - TS Interface Method Counter + +```rig +import { agent, p, s, defineTool } from "rig"; + +const countInterfaceMethods = defineTool("countInterfaceMethods", { + description: "Count method signatures in TypeScript interfaces within a source file.", + parameters: s.object({ filePath: s.path }), + async handler({ filePath }: { filePath: string }) { + const { readFile } = await import("node:fs/promises"); + const content = await readFile(filePath, "utf8"); + const interfaces: Array<{ name: string; methodCount: number }> = []; + const ifaceRe = /interface\s+(\w+)[^{]*\{([^}]*)\}/gs; + let match: RegExpExecArray | null; + while ((match = ifaceRe.exec(content)) !== null) { + const name = match[1]; + const body = match[2]; + const methods = body.match(/\w+\??\s*\([^)]*\)/g) || []; + interfaces.push({ name, methodCount: methods.length }); + } + return { interfaceCount: interfaces.length, totalMethods: interfaces.reduce((a, i) => a + i.methodCount, 0), interfaces }; + }, +}); + +// Agent role: count method signatures in TypeScript interfaces across all source files. +const tsInterfaceMethodCounter = agent({ + model: "small", + instructions: p`Count TypeScript interface method signatures across source files. + +TypeScript source files: +${p.glob("src/**/*.ts")} + +For each file path, call countInterfaceMethods and collect the results. +Compute totalFiles (files processed), totalInterfaces (sum of interfaceCount across all files), +totalMethods (sum of totalMethods across all files). +Build files array with file, interfaceCount, methodCount per file. +Set topInterface to the interface name with most methods across all files, or omit if none found.`, + tools: [countInterfaceMethods], + output: s.object({ + totalFiles: s.int, + totalInterfaces: s.int, + totalMethods: s.int, + files: s.array(s.object({ file: s.path, interfaceCount: s.int, methodCount: s.int })), + topInterface: s.optional(s.string), + }), + maxTurns: 6, +}); + +export default tsInterfaceMethodCounter; +``` diff --git a/skills/rig/samples/434-git-tag-annotation-extractor.md b/skills/rig/samples/434-git-tag-annotation-extractor.md new file mode 100644 index 0000000..bd1ce83 --- /dev/null +++ b/skills/rig/samples/434-git-tag-annotation-extractor.md @@ -0,0 +1,52 @@ +# 434 - Git Tag Annotation Extractor + +```rig +import { agent, p, s, defineTool, repair } from "rig"; + +const classifyTagKind = defineTool("classifyTagKind", { + description: "Classify a git tag as release, pre-release, milestone, or other based on its name.", + parameters: s.object({ tagName: s.string, subject: s.string }), + handler({ tagName, subject }: { tagName: string; subject: string }) { + const name = tagName.toLowerCase(); + if (/-(alpha|beta|rc|preview|pre|next|canary)\b/.test(name)) { + return { kind: "pre-release" as const }; + } + if (/^v?\d+\.\d+(\.\d+)?$/.test(name)) { + return { kind: "release" as const }; + } + if (/milestone|sprint|phase/.test(name) || /milestone|sprint|phase/.test(subject.toLowerCase())) { + return { kind: "milestone" as const }; + } + return { kind: "other" as const }; + }, +}); + +// Agent role: extract and classify git tag annotations in the repository. +const gitTagAnnotationExtractor = agent({ + model: "small", + instructions: p`Extract and classify git tag annotations. + +Tag listing with subject and date: +${p.bash("git tag -l --format='%(refname:short)|%(subject)|%(taggerdate:short)' 2>/dev/null | head -50")} + +For each line (format: name|subject|date), call classifyTagKind with tagName and subject. +Build a tags array with name, subject, date (omit if empty), and kind. +Count totalTags (total lines processed), releaseTags (kind === "release").`, + tools: [classifyTagKind], + output: s.object({ + tags: s.array( + s.object({ + name: s.string, + subject: s.string, + date: s.optional(s.string), + kind: s.enum("release", "pre-release", "milestone", "other"), + }) + ), + totalTags: s.int, + releaseTags: s.int, + }), + addons: [repair()], +}); + +export default gitTagAnnotationExtractor; +``` diff --git a/skills/rig/samples/435-jsonl-file-analyzer.md b/skills/rig/samples/435-jsonl-file-analyzer.md new file mode 100644 index 0000000..d0f8f67 --- /dev/null +++ b/skills/rig/samples/435-jsonl-file-analyzer.md @@ -0,0 +1,55 @@ +# 435 - JSONL File Analyzer + +```rig +import { agent, p, s, defineTool, repair } from "rig"; + +const parseJSONLFile = defineTool("parseJSONLFile", { + description: "Parse JSONL content: count valid/invalid lines and extract sample top-level keys.", + parameters: s.object({ content: s.string }), + handler({ content }: { content: string }) { + const lines = content.split("\n").filter((l: string) => l.trim().length > 0); + let validLines = 0; + let invalidLines = 0; + let sampleKeys: string[] = []; + for (const line of lines) { + try { + const obj = JSON.parse(line); + validLines++; + if (sampleKeys.length === 0 && obj !== null && typeof obj === "object" && !Array.isArray(obj)) { + sampleKeys = Object.keys(obj).slice(0, 10); + } + } catch { + invalidLines++; + } + } + return { lineCount: lines.length, validLines, invalidLines, sampleKeys }; + }, +}); + +// Agent role: analyze a JSONL file and report parse statistics. +const jsonlFileAnalyzer = agent({ + model: "small", + input: s.object({ inputFile: s.path }), + instructions: p`Analyze the JSONL (JSON Lines) file. + +File contents: +${p.readInput("inputFile")} + +Call parseJSONLFile with the complete file content. +Compute parseRate as validLines / lineCount (0 if lineCount is 0). +Set isEmpty to true if lineCount === 0. +Return all fields from the tool result plus parseRate and isEmpty.`, + tools: [parseJSONLFile], + output: s.object({ + lineCount: s.int, + validLines: s.int, + invalidLines: s.int, + parseRate: s.number, + sampleKeys: s.array(s.string), + isEmpty: s.boolean, + }), + addons: [repair()], +}); + +export default jsonlFileAnalyzer; +``` diff --git a/skills/rig/samples/436-npm-peer-dep-conflict-checker.md b/skills/rig/samples/436-npm-peer-dep-conflict-checker.md new file mode 100644 index 0000000..5ff32ed --- /dev/null +++ b/skills/rig/samples/436-npm-peer-dep-conflict-checker.md @@ -0,0 +1,64 @@ +# 436 - NPM Peer Dep Conflict Checker + +```rig +import { agent, p, s, defineTool, repair } from "rig"; + +const detectPeerConflict = defineTool("detectPeerConflict", { + description: "Check if an installed package version satisfies the expected peer dependency range.", + parameters: s.object({ + packageName: s.string, + requiredVersion: s.string, + installedVersion: s.string, + }), + handler({ requiredVersion, installedVersion }: { packageName: string; requiredVersion: string; installedVersion: string }) { + if (!installedVersion) { + return { hasConflict: true, severity: "error" as const, message: "Package not installed" }; + } + const reqMajor = parseInt(requiredVersion.replace(/[^0-9]/, ""), 10); + const instMajor = parseInt(installedVersion.replace(/[^0-9]/, ""), 10); + if (!isNaN(reqMajor) && !isNaN(instMajor) && reqMajor !== instMajor) { + return { hasConflict: true, severity: "error" as const, message: `Expected major ${reqMajor}, found ${instMajor}` }; + } + if (requiredVersion.startsWith(">") || requiredVersion.startsWith("^") || requiredVersion.startsWith("~")) { + return { hasConflict: false, severity: "none" as const, message: "Compatible" }; + } + return { hasConflict: false, severity: "none" as const, message: "Satisfied" }; + }, +}); + +// Agent role: identify peer dependency conflicts in the current npm project. +const npmPeerDepConflictChecker = agent({ + model: "small", + instructions: p`Check for peer dependency conflicts in the npm project. + +package.json: +${p.read("package.json")} + +npm dependency tree: +${p.bash("npm ls --json 2>&1 | head -200")} + +1. Extract peerDependencies from package.json. +2. For each peer dep, find its installed version from the npm ls output. +3. Call detectPeerConflict with packageName, requiredVersion (from peerDependencies), and installedVersion. +4. Build conflicts array. Set totalConflicts to count of entries with hasConflict true. +5. summary: one-sentence overview of the conflict status.`, + tools: [detectPeerConflict], + output: s.object({ + totalPeerDeps: s.int, + conflictsFound: s.int, + conflicts: s.array( + s.object({ + package: s.string, + required: s.string, + installed: s.optional(s.string), + severity: s.enum("none", "warning", "error"), + }) + ), + summary: s.string, + }), + maxTurns: 5, + addons: [repair()], +}); + +export default npmPeerDepConflictChecker; +``` diff --git a/skills/rig/samples/437-ts-symbol-frequency-recorder.md b/skills/rig/samples/437-ts-symbol-frequency-recorder.md new file mode 100644 index 0000000..0c8857d --- /dev/null +++ b/skills/rig/samples/437-ts-symbol-frequency-recorder.md @@ -0,0 +1,45 @@ +# 437 - TS Symbol Frequency Recorder + +```rig +import { agent, p, s, defineTool, repair } from "rig"; + +const countSymbolUsage = defineTool("countSymbolUsage", { + description: "Count identifier frequency in a TypeScript source file.", + parameters: s.object({ filePath: s.path }), + async handler({ filePath }: { filePath: string }) { + const { readFile } = await import("node:fs/promises"); + const content = await readFile(filePath, "utf8"); + const freq: Record = {}; + const matches = content.match(/\b[a-zA-Z_$][a-zA-Z0-9_$]{2,}\b/g) || []; + for (const sym of matches) { + freq[sym] = (freq[sym] ?? 0) + 1; + } + return freq; + }, +}); + +// Agent role: record identifier frequency across TypeScript source files. +const tsSymbolFrequencyRecorder = agent({ + model: "small", + instructions: p`Record identifier frequency across TypeScript source files. + +TypeScript files: +${p.glob("src/**/*.ts")} + +For each file path, call countSymbolUsage to get a frequency record. +Merge all per-file records into a single global symbolFrequency map (sum counts for duplicates). +Compute totalFiles (count of files processed) and uniqueSymbols (number of distinct keys in symbolFrequency). +Build topSymbols as the top 10 entries by count sorted descending, each with symbol and count.`, + tools: [countSymbolUsage], + output: s.object({ + totalFiles: s.int, + uniqueSymbols: s.int, + topSymbols: s.array(s.object({ symbol: s.string, count: s.int })), + symbolFrequency: s.record(s.int), + }), + maxTurns: 6, + addons: [repair()], +}); + +export default tsSymbolFrequencyRecorder; +``` diff --git a/skills/rig/samples/438-markdown-table-writer.md b/skills/rig/samples/438-markdown-table-writer.md new file mode 100644 index 0000000..45fffcd --- /dev/null +++ b/skills/rig/samples/438-markdown-table-writer.md @@ -0,0 +1,47 @@ +# 438 - Markdown Table Writer + +```rig +import { agent, p, s, defineTool } from "rig"; + +const convertCsvToMarkdown = defineTool("convertCsvToMarkdown", { + description: "Convert CSV text to a Markdown table string.", + parameters: s.object({ csv: s.string }), + handler({ csv }: { csv: string }) { + const rows = csv.split("\n").map((r: string) => r.split(",").map((c: string) => c.trim())).filter((r: string[]) => r.some((c: string) => c.length > 0)); + if (rows.length === 0) return { markdown: "", rowCount: 0, columnCount: 0, headers: [] }; + const [header, ...data] = rows; + const sep = header.map(() => "---").join(" | "); + const lines = [ + `| ${header.join(" | ")} |`, + `| ${sep} |`, + ...data.map((row: string[]) => `| ${row.join(" | ")} |`), + ]; + return { markdown: lines.join("\n"), rowCount: data.length, columnCount: header.length, headers: header }; + }, +}); + +// Agent role: convert a CSV file to a Markdown table and write the result to an output file. +const markdownTableWriter = agent({ + model: "small", + input: s.object({ csvFile: s.path, outputFile: s.path }), + instructions: p`Convert a CSV file to a Markdown table and write it to the output file. + +CSV file contents: +${p.readInput("csvFile")} + +1. Call convertCsvToMarkdown with the CSV content. +2. The markdownTable field in the output should contain the generated Markdown. +3. Use the outputFile path value provided as input for the outputFile output field. +4. Set rowCount, columnCount, and headers from the tool result.`, + tools: [convertCsvToMarkdown], + output: s.object({ + markdownTable: s.string, + rowCount: s.int, + columnCount: s.int, + headers: s.array(s.string), + outputFile: s.path, + }), +}); + +export default markdownTableWriter; +``` diff --git a/skills/rig/samples/439-git-commit-risk-classifier.md b/skills/rig/samples/439-git-commit-risk-classifier.md new file mode 100644 index 0000000..29701aa --- /dev/null +++ b/skills/rig/samples/439-git-commit-risk-classifier.md @@ -0,0 +1,62 @@ +# 439 - Git Commit Risk Classifier + +```rig +import { agent, p, s, defineTool } from "rig"; + +const classifyCommitRisk = defineTool("classifyCommitRisk", { + description: "Classify the risk level of a git commit based on its change stats and message.", + parameters: s.object({ + sha: s.string, + message: s.string, + filesChanged: s.int, + insertions: s.int, + deletions: s.int, + }), + handler({ message, filesChanged, insertions, deletions }: { sha: string; message: string; filesChanged: number; insertions: number; deletions: number }) { + if (/BREAKING.CHANGE/i.test(message)) { + return { risk: "critical" as const, reasoning: "Commit message contains BREAKING CHANGE" }; + } + if (filesChanged > 20 || insertions > 500) { + return { risk: "high" as const, reasoning: `Large change: ${filesChanged} files, ${insertions} insertions` }; + } + if (filesChanged > 5 || insertions > 100) { + return { risk: "medium" as const, reasoning: `Moderate change: ${filesChanged} files, ${insertions} insertions` }; + } + return { risk: "low" as const, reasoning: `Small change: ${filesChanged} files, ${insertions} insertions, ${deletions} deletions` }; + }, +}); + +// Agent role: classify risk level of recent git commits based on change statistics. +const gitCommitRiskClassifier = agent({ + model: "small", + instructions: p`Classify the risk level of recent git commits. + +Recent commit stats: +${p.bash("git log --stat -10 --format='COMMIT:%H %s' 2>/dev/null")} + +Parse the output to extract each commit's sha, message, filesChanged, insertions, and deletions. +Call classifyCommitRisk for each commit. +Build the commits array with all fields. +Compute summary.total (total commits), and summary.byRisk counting each risk level.`, + tools: [classifyCommitRisk], + output: s.object({ + commits: s.array( + s.object({ + sha: s.string, + message: s.string, + filesChanged: s.int, + insertions: s.int, + deletions: s.int, + risk: s.enum("low", "medium", "high", "critical"), + reasoning: s.string, + }) + ), + summary: s.object({ + total: s.int, + byRisk: s.object({ low: s.int, medium: s.int, high: s.int, critical: s.int }), + }), + }), +}); + +export default gitCommitRiskClassifier; +``` diff --git a/skills/rig/samples/440-ts-generic-constraint-reporter.md b/skills/rig/samples/440-ts-generic-constraint-reporter.md new file mode 100644 index 0000000..dd196da --- /dev/null +++ b/skills/rig/samples/440-ts-generic-constraint-reporter.md @@ -0,0 +1,61 @@ +# 440 - TS Generic Constraint Reporter + +```rig +import { agent, p, s, defineTool, repair } from "rig"; + +const extractGenericConstraints = defineTool("extractGenericConstraints", { + description: "Extract generic type parameters and their extends constraints from a TypeScript file.", + parameters: s.object({ filePath: s.path }), + async handler({ filePath }: { filePath: string }) { + const { readFile } = await import("node:fs/promises"); + const content = await readFile(filePath, "utf8"); + const generics: Array<{ name: string; constraint?: string }> = []; + const typeParamRe = /<([^>]+)>/g; + let match: RegExpExecArray | null; + while ((match = typeParamRe.exec(content)) !== null) { + const parts = match[1].split(","); + for (const part of parts) { + const trimmed = part.trim(); + const extendsMatch = trimmed.match(/^(\w+)\s+extends\s+(.+)$/); + if (extendsMatch) { + generics.push({ name: extendsMatch[1], constraint: extendsMatch[2].trim() }); + } else if (/^\w+$/.test(trimmed)) { + generics.push({ name: trimmed }); + } + } + } + return { generics }; + }, +}); + +// Agent role: report generic type constraints across TypeScript source files. +const tsGenericConstraintReporter = agent({ + model: "small", + instructions: p`Report generic type constraints found in TypeScript source files. + +TypeScript files: +${p.glob("src/**/*.ts")} + +For each file, call extractGenericConstraints and collect the results. +Compute totalFiles (files processed), totalGenerics (sum of all generics found), +constrainedGenerics (generics that have a constraint), unconstrained (generics without a constraint). +Build files array with file path and its generics list.`, + tools: [extractGenericConstraints], + output: s.object({ + totalFiles: s.int, + totalGenerics: s.int, + constrainedGenerics: s.int, + unconstrained: s.int, + files: s.array( + s.object({ + file: s.path, + generics: s.array(s.object({ name: s.string, constraint: s.optional(s.string) })), + }) + ), + }), + maxTurns: 6, + addons: [repair()], +}); + +export default tsGenericConstraintReporter; +```