From 88d23ae8ae71a4f944ebcc05b180146292a7e47f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 13 Aug 2026 08:08:58 +0000 Subject: [PATCH] Add 10 rig samples 411-420 (2026-08-13) Samples cover: ts generic constraints, readme badge analysis, python risk mapper, git merge complexity, ts reexport chain, comment density, git blame age, ts mapped types, dotenv drift detection, vitest snapshot reporting. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../411-ts-generic-constraint-extractor.md | 46 +++++++++++++++ .../rig/samples/412-readme-badge-analyzer.md | 46 +++++++++++++++ .../413-python-requirements-risk-mapper.md | 54 +++++++++++++++++ .../414-git-merge-complexity-scorer.md | 50 ++++++++++++++++ .../samples/415-ts-reexport-chain-tracer.md | 43 ++++++++++++++ .../416-source-comment-density-reporter.md | 57 ++++++++++++++++++ .../417-git-blame-line-age-analyzer.md | 58 +++++++++++++++++++ .../samples/418-ts-mapped-type-extractor.md | 57 ++++++++++++++++++ .../rig/samples/419-dotenv-drift-detector.md | 45 ++++++++++++++ .../samples/420-vitest-snapshot-reporter.md | 51 ++++++++++++++++ 10 files changed, 507 insertions(+) create mode 100644 skills/rig/samples/411-ts-generic-constraint-extractor.md create mode 100644 skills/rig/samples/412-readme-badge-analyzer.md create mode 100644 skills/rig/samples/413-python-requirements-risk-mapper.md create mode 100644 skills/rig/samples/414-git-merge-complexity-scorer.md create mode 100644 skills/rig/samples/415-ts-reexport-chain-tracer.md create mode 100644 skills/rig/samples/416-source-comment-density-reporter.md create mode 100644 skills/rig/samples/417-git-blame-line-age-analyzer.md create mode 100644 skills/rig/samples/418-ts-mapped-type-extractor.md create mode 100644 skills/rig/samples/419-dotenv-drift-detector.md create mode 100644 skills/rig/samples/420-vitest-snapshot-reporter.md diff --git a/skills/rig/samples/411-ts-generic-constraint-extractor.md b/skills/rig/samples/411-ts-generic-constraint-extractor.md new file mode 100644 index 0000000..31486ae --- /dev/null +++ b/skills/rig/samples/411-ts-generic-constraint-extractor.md @@ -0,0 +1,46 @@ +# 411 - TS Generic Constraint Extractor + +```rig +import { agent, p, s, defineTool, repair } from "rig"; +import { readFile } from "node:fs/promises"; + +// Agent role: extract generic type constraints from TypeScript files and summarize per file. +const tsGenericConstraintExtractor = agent({ + model: "small", + instructions: p`Extract TypeScript generic type constraints (patterns like \`T extends ...\`) from source files. + +TypeScript files: ${p.bash("find src -name '*.ts' 2>/dev/null | head -50 || echo ''")} + +For each file path, call extractGenericConstraints. Then produce the declared output.`, + tools: [ + defineTool("extractGenericConstraints", { + description: "Extract generic type constraints from a TypeScript file", + parameters: s.object({ filePath: s.path }), + async handler({ filePath }) { + try { + const content = await readFile(filePath, "utf-8"); + const regex = /\bextends\s+([^\s,>{=]+(?:\s*[<(][^>)]*[>)])?)/g; + const constraints: string[] = []; + let m: RegExpExecArray | null; + while ((m = regex.exec(content)) !== null) { + constraints.push(m[1].trim()); + } + return { filePath, constraints }; + } catch { + return { filePath, constraints: [] }; + } + }, + }), + ], + output: s.object({ + constraints: s.record(s.array(s.string)), + totalConstraints: s.int, + constrainedCount: s.int, + mostConstrainedFile: s.optional(s.string), + }), + addons: [repair()], +}); + +export default tsGenericConstraintExtractor; + +``` diff --git a/skills/rig/samples/412-readme-badge-analyzer.md b/skills/rig/samples/412-readme-badge-analyzer.md new file mode 100644 index 0000000..3ac363b --- /dev/null +++ b/skills/rig/samples/412-readme-badge-analyzer.md @@ -0,0 +1,46 @@ +# 412 - README Badge Analyzer + +```rig +import { agent, p, s, defineTool, steering } from "rig"; + +// Agent role: parse README.md badges and classify each by category. +const readmeBadgeAnalyzer = agent({ + model: "small", + instructions: p`Analyze badge images in the README and classify each badge. + +README content: +${p.readOptional("README.md", "")} + +Call parseBadge for each markdown image badge you find (pattern: [![...](imgUrl)](link)). +Then return the declared output.`, + tools: [ + defineTool("parseBadge", { + description: "Parse a badge URL and classify it as ci, coverage, version, license, or other", + parameters: s.object({ url: s.string, label: s.string }), + handler({ url, label }: { url: string; label: string }) { + const u = url.toLowerCase(); + const l = label.toLowerCase(); + let category: "ci" | "coverage" | "version" | "license" | "other" = "other"; + if (/github.*action|travis|circleci|appveyor|workflow|build/.test(u + l)) category = "ci"; + else if (/coverage|codecov|coveralls/.test(u + l)) category = "coverage"; + else if (/version|release|npm|pypi/.test(u + l)) category = "version"; + else if (/license|mit|apache|gpl/.test(u + l)) category = "license"; + return { url, label, category }; + }, + }), + ], + output: s.object({ + badges: s.array(s.object({ + url: s.string, + label: s.string, + category: s.enum("ci", "coverage", "version", "license", "other"), + })), + totalBadges: s.int, + hasCiBadge: s.boolean, + }), + addons: [steering()], +}); + +export default readmeBadgeAnalyzer; + +``` diff --git a/skills/rig/samples/413-python-requirements-risk-mapper.md b/skills/rig/samples/413-python-requirements-risk-mapper.md new file mode 100644 index 0000000..a00b95a --- /dev/null +++ b/skills/rig/samples/413-python-requirements-risk-mapper.md @@ -0,0 +1,54 @@ +# 413 - Python Requirements Risk Mapper + +```rig +import { agent, p, s, defineTool, steering } from "rig"; + +// Agent role: classify Python package risk from requirements.txt and pip list output. +const pythonRequirementsRiskMapper = agent({ + model: "small", + instructions: p`Analyze Python package risk based on requirements.txt and installed packages. + +requirements.txt: +${p.readOptional("requirements.txt", "(no requirements.txt found)")} + +Installed packages (pip list): +${p.bash("pip list --format=json 2>/dev/null || echo '[]'")} + +For each package, call classifyPackageRisk. Packages that are very old (version < 1.0), +known for past vulnerabilities (e.g. requests<2.20, pyyaml<5.4, pillow<9), +or have no version pinned are considered high risk. +Produce the declared output.`, + tools: [ + defineTool("classifyPackageRisk", { + description: "Classify a Python package by risk level based on name and version", + parameters: s.object({ name: s.string, version: s.string }), + handler({ name, version }: { name: string; version: string }) { + const n = name.toLowerCase(); + const major = parseInt(version.split(".")[0] ?? "0", 10); + let risk: "low" | "medium" | "high" = "low"; + let reason = "stable package"; + if (!version || version === "unknown") { risk = "high"; reason = "no version pinned"; } + else if (major === 0) { risk = "medium"; reason = "pre-1.0 release"; } + if (/(pyyaml|pillow|requests|urllib3|cryptography)/.test(n) && major < 2) { + risk = "high"; reason = "known past vulnerability in old version"; + } + return { name, version, risk, reason }; + }, + }), + ], + output: s.object({ + packages: s.record(s.object({ + version: s.string, + risk: s.enum("low", "medium", "high"), + reason: s.string, + })), + totalPackages: s.int, + riskyCount: s.int, + recommendedAction: s.enum("audit", "review", "ok"), + }), + addons: [steering()], +}); + +export default pythonRequirementsRiskMapper; + +``` diff --git a/skills/rig/samples/414-git-merge-complexity-scorer.md b/skills/rig/samples/414-git-merge-complexity-scorer.md new file mode 100644 index 0000000..be9fc58 --- /dev/null +++ b/skills/rig/samples/414-git-merge-complexity-scorer.md @@ -0,0 +1,50 @@ +# 414 - Git Merge Complexity Scorer + +```rig +import { agent, p, s, defineTool, repair } from "rig"; + +// Agent role: score git merge commits by complexity based on files changed. +const gitMergeComplexityScorer = agent({ + model: "small", + instructions: p`Score the complexity of recent merge commits in this repository. + +Recent merge commits: +${p.bash("git log --merges --oneline -20 2>/dev/null || echo '(no merges found)'")} + +For each merge commit hash, call scoreMergeComplexity. Then produce the declared output.`, + tools: [ + defineTool("scoreMergeComplexity", { + description: "Score a merge commit complexity by running git show --stat", + parameters: s.object({ hash: s.string, message: s.string }), + handler({ hash, message }: { hash: string; message: string }) { + const { execSync } = require("node:child_process"); + try { + const stat = execSync(`git show --stat ${hash} 2>/dev/null`, { encoding: "utf-8" }); + const match = stat.match(/(\d+) files? changed/); + const filesChanged = match ? parseInt(match[1], 10) : 0; + const complexity: "simple" | "moderate" | "complex" = + filesChanged <= 3 ? "simple" : filesChanged <= 10 ? "moderate" : "complex"; + return { hash, message, filesChanged, complexity }; + } catch { + return { hash, message, filesChanged: 0, complexity: "simple" as const }; + } + }, + }), + ], + output: s.object({ + merges: s.array(s.object({ + hash: s.string, + message: s.string, + filesChanged: s.int, + complexity: s.enum("simple", "moderate", "complex"), + })), + totalMerges: s.int, + complexMergeCount: s.int, + mostComplexMerge: s.optional(s.string), + }), + addons: [repair()], +}); + +export default gitMergeComplexityScorer; + +``` diff --git a/skills/rig/samples/415-ts-reexport-chain-tracer.md b/skills/rig/samples/415-ts-reexport-chain-tracer.md new file mode 100644 index 0000000..9ac8209 --- /dev/null +++ b/skills/rig/samples/415-ts-reexport-chain-tracer.md @@ -0,0 +1,43 @@ +# 415 - TS Reexport Chain Tracer + +```rig +import { agent, p, s, defineTool, repair } from "rig"; +import { readFile } from "node:fs/promises"; + +// Agent role: trace TypeScript re-export chains from an entry file. +const tsReexportChainTracer = agent({ + model: "small", + input: s.object({ entryFile: s.path }), + instructions: p`Trace TypeScript re-export chains starting from the entry file. +Use traceReexports to follow export * from and export { X } from chains. +Detect circular references. Produce the declared output.`, + tools: [ + defineTool("traceReexports", { + description: "Read a TypeScript file and extract its re-export paths", + parameters: s.object({ filePath: s.path }), + async handler({ filePath }) { + try { + const content = await readFile(filePath, "utf-8"); + const regex = /export\s+(?:\*|\{[^}]*\})\s+from\s+['"]([^'"]+)['"]/g; + const exports: string[] = []; + let m: RegExpExecArray | null; + while ((m = regex.exec(content)) !== null) exports.push(m[1]); + return { filePath, reexports: exports }; + } catch { + return { filePath, reexports: [] }; + } + }, + }), + ], + output: s.object({ + chain: s.array(s.object({ file: s.path, symbols: s.array(s.string) })), + chainDepth: s.int, + circularDetected: s.boolean, + }), + maxTurns: 6, + addons: [repair()], +}); + +export default tsReexportChainTracer; + +``` diff --git a/skills/rig/samples/416-source-comment-density-reporter.md b/skills/rig/samples/416-source-comment-density-reporter.md new file mode 100644 index 0000000..bd87930 --- /dev/null +++ b/skills/rig/samples/416-source-comment-density-reporter.md @@ -0,0 +1,57 @@ +# 416 - Source Comment Density Reporter + +```rig +import { agent, p, s, defineTool, repair } from "rig"; +import { readFile } from "node:fs/promises"; + +// Agent role: measure comment density across TypeScript source files. +const sourceCommentDensityReporter = agent({ + model: "small", + instructions: p`Measure comment density for TypeScript source files. + +Source files: ${p.glob("src/**/*.ts")} + +For each file path, call measureCommentDensity. Then produce the declared output.`, + tools: [ + defineTool("measureCommentDensity", { + description: "Count comment lines vs code lines in a TypeScript file", + parameters: s.object({ filePath: s.path }), + async handler({ filePath }) { + try { + const content = await readFile(filePath, "utf-8"); + const lines = content.split("\n"); + let commentLines = 0; + let inBlock = false; + for (const line of lines) { + const t = line.trim(); + if (inBlock) { commentLines++; if (t.includes("*/")) inBlock = false; } + else if (t.startsWith("/*") || t.startsWith("*")) { commentLines++; if (!t.includes("*/")) inBlock = true; } + else if (t.startsWith("//")) commentLines++; + } + const codeLines = lines.length - commentLines; + const density = lines.length > 0 ? commentLines / lines.length : 0; + return { commentLines, codeLines, density }; + } catch { + return { commentLines: 0, codeLines: 0, density: 0 }; + } + }, + }), + ], + output: s.object({ + files: s.record(s.object({ + commentLines: s.int, + codeLines: s.int, + density: s.number, + })), + overall: s.object({ + totalFiles: s.int, + averageDensity: s.number, + highDensityFiles: s.array(s.path), + }), + }), + addons: [repair()], +}); + +export default sourceCommentDensityReporter; + +``` diff --git a/skills/rig/samples/417-git-blame-line-age-analyzer.md b/skills/rig/samples/417-git-blame-line-age-analyzer.md new file mode 100644 index 0000000..ff37985 --- /dev/null +++ b/skills/rig/samples/417-git-blame-line-age-analyzer.md @@ -0,0 +1,58 @@ +# 417 - Git Blame Line Age Analyzer + +```rig +import { agent, p, s, defineTool, repair } from "rig"; + +// Agent role: analyze git blame line ages for TypeScript source files. +const gitBlameLineAgeAnalyzer = agent({ + model: "small", + instructions: p`Analyze the age of lines in TypeScript source files using git blame. + +TypeScript files (sample): ${p.bash("find src -name '*.ts' 2>/dev/null | head -10 || echo ''")} + +For each file, call analyzeBlameAge to get line age statistics. Then produce the declared output.`, + tools: [ + defineTool("analyzeBlameAge", { + description: "Run git blame on a file and compute line age statistics", + parameters: s.object({ filePath: s.path }), + handler({ filePath }: { filePath: string }) { + const { execSync } = require("node:child_process"); + try { + const out = execSync(`git blame --line-porcelain "${filePath}" 2>/dev/null`, { encoding: "utf-8" }); + const now = Date.now() / 1000; + const timestamps: number[] = []; + for (const line of out.split("\n")) { + if (line.startsWith("author-time ")) timestamps.push(parseInt(line.slice(12), 10)); + } + if (timestamps.length === 0) return { filePath, avgAgeDays: 0, staleLines: 0, recentLines: 0, totalLines: 0 }; + const dayMs = 86400; + let stale = 0, recent = 0; + for (const ts of timestamps) { + const days = (now - ts) / dayMs; + if (days < 30) recent++; + else if (days > 365) stale++; + } + const avgAgeDays = Math.round(timestamps.reduce((a, t) => a + (now - t) / dayMs, 0) / timestamps.length); + return { filePath, avgAgeDays, staleLines: stale, recentLines: recent, totalLines: timestamps.length }; + } catch { + return { filePath, avgAgeDays: 0, staleLines: 0, recentLines: 0, totalLines: 0 }; + } + }, + }), + ], + output: s.object({ + files: s.record(s.object({ + avgAgeDays: s.int, + staleLines: s.int, + recentLines: s.int, + totalLines: s.int, + })), + oldestFile: s.optional(s.string), + newestFile: s.optional(s.string), + }), + addons: [repair()], +}); + +export default gitBlameLineAgeAnalyzer; + +``` diff --git a/skills/rig/samples/418-ts-mapped-type-extractor.md b/skills/rig/samples/418-ts-mapped-type-extractor.md new file mode 100644 index 0000000..c8e9967 --- /dev/null +++ b/skills/rig/samples/418-ts-mapped-type-extractor.md @@ -0,0 +1,57 @@ +# 418 - TS Mapped Type Extractor + +```rig +import { agent, p, s, defineTool, steering } from "rig"; +import { readFile } from "node:fs/promises"; + +// Agent role: extract TypeScript mapped type declarations from source files. +const tsMappedTypeExtractor = agent({ + model: "small", + instructions: p`Extract TypeScript mapped type declarations from source files. + +TypeScript files: ${p.glob("src/**/*.ts")} + +For each file, call extractMappedTypes. Then produce the declared output.`, + tools: [ + defineTool("extractMappedTypes", { + description: "Extract mapped type declarations from a TypeScript file", + parameters: s.object({ filePath: s.path }), + async handler({ filePath }) { + try { + const content = await readFile(filePath, "utf-8"); + const regex = /type\s+(\w+)\s*(?:<[^>]*>)?\s*=\s*\{[^}]*\[(\w+)\s+in\s+([^\]]+)\]\s*(?:(readonly)\s*)?:\s*([^;}\n]+)/g; + const types: Array<{ name: string; keySource: string; valueType: string; isReadonly: boolean; sourceFile: string }> = []; + let m: RegExpExecArray | null; + while ((m = regex.exec(content)) !== null) { + types.push({ + name: m[1], + keySource: m[3].trim(), + valueType: m[5].trim(), + isReadonly: !!m[4], + sourceFile: filePath, + }); + } + return types; + } catch { + return []; + } + }, + }), + ], + output: s.object({ + types: s.record(s.object({ + keySource: s.string, + valueType: s.string, + isReadonly: s.boolean, + sourceFile: s.string, + })), + totalMappedTypes: s.int, + totalFiles: s.int, + mostUsedKeySource: s.optional(s.string), + }), + addons: [steering()], +}); + +export default tsMappedTypeExtractor; + +``` diff --git a/skills/rig/samples/419-dotenv-drift-detector.md b/skills/rig/samples/419-dotenv-drift-detector.md new file mode 100644 index 0000000..f8b88bd --- /dev/null +++ b/skills/rig/samples/419-dotenv-drift-detector.md @@ -0,0 +1,45 @@ +# 419 - Dotenv Drift Detector + +```rig +import { agent, p, s, defineTool, repair } from "rig"; + +// Agent role: detect drift between .env.example declarations and actual process.env usage in source code. +const dotenvDriftDetector = agent({ + model: "small", + instructions: p`Detect drift between .env.example and actual process.env key usage in source code. + +.env.example contents: +${p.readOptional(".env.example", "(no .env.example found)")} + +process.env usages in source: +${p.bash("grep -rn 'process\\.env\\.' src/ --include='*.ts' 2>/dev/null | head -100 || echo '(none found)'")} + +For each env key found in either source, call classifyEnvKey. Produce the declared output.`, + tools: [ + defineTool("classifyEnvKey", { + description: "Classify an env key based on whether it is in .env.example and/or used in code", + parameters: s.object({ key: s.string, inExample: s.boolean, usedInCode: s.boolean }), + handler({ key, inExample, usedInCode }: { key: string; inExample: boolean; usedInCode: boolean }) { + let status: "declared" | "undeclared" | "unused" = "declared"; + if (usedInCode && !inExample) status = "undeclared"; + else if (!usedInCode && inExample) status = "unused"; + return { key, inExample, usedInCode, status }; + }, + }), + ], + output: s.object({ + keys: s.record(s.object({ + inExample: s.boolean, + usedInCode: s.boolean, + status: s.enum("declared", "undeclared", "unused"), + })), + totalKeys: s.int, + missingFromExample: s.array(s.string), + unusedDeclarations: s.array(s.string), + }), + addons: [repair()], +}); + +export default dotenvDriftDetector; + +``` diff --git a/skills/rig/samples/420-vitest-snapshot-reporter.md b/skills/rig/samples/420-vitest-snapshot-reporter.md new file mode 100644 index 0000000..9f3034e --- /dev/null +++ b/skills/rig/samples/420-vitest-snapshot-reporter.md @@ -0,0 +1,51 @@ +# 420 - Vitest Snapshot Reporter + +```rig +import { agent, p, s, workflow } from "rig"; + +// Agent role: list vitest snapshot files using bash find. +const snapshotFileAgent = agent({ + model: "small", + instructions: p`List all vitest snapshot files in the repository. +${p.bash("find . -name '*.snap' -not -path '*/node_modules/*' 2>/dev/null | head -50 || echo '(none found)'")} +Return all found snapshot file paths and the total count.`, + output: s.object({ + files: s.array(s.path), + totalFiles: s.int, + }), +}); + +// Agent role: count snapshot entries in snapshot files using glob. +const snapshotCountAgent = agent({ + model: "small", + instructions: p`Count snapshot entries in vitest snapshot files. +Snapshot files: ${p.glob("**/__snapshots__/*.snap")} +Count how many snapshot entries each file contains (each entry starts with "exports["). +Return a record mapping file path to entry count, plus the total across all files.`, + output: s.object({ + entryCounts: s.record(s.int), + totalSnapshots: s.int, + }), +}); + +// Workflow role: run both snapshot agents in parallel and combine results. +const vitestSnapshotReporter = workflow({ + meta: { name: "vitestSnapshotReporter", description: "Count vitest snapshots across the repo", phases: ["Collect", "Summarize"] }, + body: async ({ call, phase }) => { + phase("Collect"); + const [fileResult, countResult] = await Promise.all([ + call(snapshotFileAgent, "list snapshot files"), + call(snapshotCountAgent, "count entries"), + ]); + phase("Summarize"); + const totalFiles = fileResult?.totalFiles ?? 0; + const totalSnapshots = countResult?.totalSnapshots ?? 0; + const entryCounts = countResult?.entryCounts ?? {}; + const largestSnapshotFile = Object.keys(entryCounts).sort((a, b) => (entryCounts[b] ?? 0) - (entryCounts[a] ?? 0))[0] ?? null; + return { totalSnapshots, totalFiles, largestSnapshotFile }; + }, +}); + +export default vitestSnapshotReporter; + +```