Skip to content

Commit 2d69c48

Browse files
Add 10 rig samples 130-139 (2026-07-25) (#136)
Samples cover git analysis, LOC stats, import cycles, coverage badges, license auditing, test mapping, version drift, author stats, path alias validation, and markdown frontmatter checking. Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 5ad09a0 commit 2d69c48

10 files changed

Lines changed: 344 additions & 0 deletions
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# 130 - Git Hotspot Analyzer
2+
3+
```rig
4+
import { agent, p, s } from "rig";
5+
import { steering } from "rig/addons";
6+
7+
// Agent role: analyze which files are hot-spots by measuring commit churn and top contributors.
8+
const gitHotspotAnalyzer = agent({
9+
model: "small",
10+
instructions: p`Analyze file churn in this repository. List all tracked files: ${p.bash("git ls-files --exclude-standard | head -100")}. Get commit counts per file: ${p.bash("git log --follow --name-only --format='' -- . | sort | uniq -c | sort -rn | head -40")}. Get top contributors per file via: ${p.bash("git shortlog -sn --no-merges HEAD~50..HEAD 2>/dev/null || git shortlog -sn --no-merges | head -20")}. For each hot-spot file compute a churnScore 0–100 (based on commit frequency relative to max) and list topContributors.`,
11+
output: s.record(s.object({
12+
churnScore: s.number,
13+
topContributors: s.array(s.string),
14+
})),
15+
maxTurns: 5,
16+
addons: [steering({ message: "Every file must have a numeric churnScore between 0 and 100 and at least one topContributor entry." })],
17+
});
18+
19+
export default gitHotspotAnalyzer;
20+
```
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# 131 - Loc Statistics Gatherer
2+
3+
```rig
4+
import { agent, p, s, defineTool } from "rig";
5+
6+
const aggregateByExtension = defineTool("aggregateByExtension", {
7+
description: "Parse wc -l output and aggregate line and file counts by extension",
8+
parameters: s.object({ wcOutput: s.string }),
9+
handler: ({ wcOutput }) => {
10+
const totals: Record<string, { lineCount: number; fileCount: number }> = {};
11+
for (const line of wcOutput.trim().split("\n")) {
12+
const match = line.trim().match(/^(\d+)\s+(.+)$/);
13+
if (!match) continue;
14+
const count = parseInt(match[1], 10);
15+
const filePath = match[2];
16+
if (filePath === "total") continue;
17+
const ext = filePath.includes(".") ? "." + filePath.split(".").pop()! : "(none)";
18+
if (!totals[ext]) totals[ext] = { lineCount: 0, fileCount: 0 };
19+
totals[ext].lineCount += count;
20+
totals[ext].fileCount += 1;
21+
}
22+
return JSON.stringify(totals);
23+
},
24+
});
25+
26+
// Agent role: gather lines-of-code statistics per file extension and classify overall complexity.
27+
const locStatisticsGatherer = agent({
28+
model: "small",
29+
instructions: p`Gather lines-of-code statistics for this repository. List files: ${p.bash("find . -not -path '*/node_modules/*' -not -path '*/.git/*' -type f | head -200")}. Count lines per file: ${p.bash("find . -not -path '*/node_modules/*' -not -path '*/.git/*' -type f -exec wc -l {} + 2>/dev/null | tail -30")}. Use aggregateByExtension to sum lineCount and fileCount per extension. Compute complexityRating: small (<1000 total lines), medium (<5000), large (<20000), xlarge (>=20000).`,
30+
output: s.object({
31+
byExtension: s.record(s.object({
32+
lineCount: s.int,
33+
fileCount: s.int,
34+
})),
35+
complexityRating: s.enum("small", "medium", "large", "xlarge"),
36+
}),
37+
tools: [aggregateByExtension],
38+
});
39+
40+
export default locStatisticsGatherer;
41+
```
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# 132 - Import Cycle Detector
2+
3+
```rig
4+
import { agent, p, s } from "rig";
5+
import { repair } from "rig/addons";
6+
7+
// Agent role: detect circular import cycles in the TypeScript project and classify severity.
8+
const importCycleDetector = agent({
9+
model: "small",
10+
maxTurns: 6,
11+
addons: [repair()],
12+
instructions: p`Detect circular imports in this TypeScript project.
13+
14+
TypeScript config: ${p.bash("cat tsconfig.json 2>/dev/null || echo '{}'")}
15+
16+
Circular dependency check: ${p.bash("npx --yes madge --circular --extensions ts . 2>/dev/null || echo 'madge unavailable'")}
17+
18+
Import graph sample: ${p.bash("grep -r --include='*.ts' -h 'from ' . 2>/dev/null | grep -v node_modules | head -60")}
19+
20+
Identify all circular import chains. For each cycle, list the path as an array of module names and classify severity: high (core/shared modules), medium (feature modules), low (utility/test files). Set hasCycles to true if any cycles were found.`,
21+
output: s.object({
22+
hasCycles: s.boolean,
23+
cycles: s.array(s.object({
24+
path: s.array(s.string),
25+
severity: s.enum("high", "medium", "low"),
26+
})),
27+
}),
28+
});
29+
30+
export default importCycleDetector;
31+
```
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# 133 - Coverage Badge Updater
2+
3+
```rig
4+
import { agent, p, s } from "rig";
5+
6+
// Agent role: read coverage summary and generate shields.io badge markdown, writing it to a coverage report file.
7+
const coverageBadgeUpdater = agent({
8+
model: "small",
9+
instructions: p`Read coverage data and generate shields.io badge markdown.
10+
11+
Coverage summary: ${p.readOptional("coverage/coverage-summary.json", "{}")}
12+
13+
Current README: ${p.readOptional("README.md", "")}
14+
15+
Parse the coverage summary JSON. Extract per-category percentages for statements, branches, functions, and lines. Compute overallPct as the average. Set rating: green (>=80%), yellow (60–79%), red (<60%). Generate shields.io badge markdown for each category and the overall percentage. Write the badge markdown to the output field badgeMarkdown. Set badgesWritten to true after generating. ${p.writeOutput("badgeMarkdown", "coverage-badges.md")}`,
16+
output: s.object({
17+
coverageByCategory: s.record(s.number),
18+
overallPct: s.number,
19+
rating: s.enum("green", "yellow", "red"),
20+
badgesWritten: s.boolean,
21+
badgeMarkdown: s.string,
22+
}),
23+
});
24+
25+
export default coverageBadgeUpdater;
26+
```
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# 134 - Dep License Auditor
2+
3+
```rig
4+
import { agent, p, s, defineTool } from "rig";
5+
6+
const classifyLicense = defineTool("classifyLicense", {
7+
description: "Classify a license identifier as permissive, copyleft, or unknown",
8+
parameters: s.object({ license: s.string }),
9+
handler: ({ license }) => {
10+
const l = license.toLowerCase();
11+
const permissive = ["mit", "isc", "apache", "bsd", "0bsd", "unlicense", "cc0", "wtfpl", "zlib"];
12+
const copyleft = ["gpl", "lgpl", "agpl", "mpl", "eupl", "cddl"];
13+
if (permissive.some((p) => l.includes(p))) return "permissive";
14+
if (copyleft.some((c) => l.includes(c))) return "copyleft";
15+
return "unknown";
16+
},
17+
});
18+
19+
// Agent role: audit npm dependency licenses and flag copyleft or unknown packages.
20+
const depLicenseAuditor = agent({
21+
model: "small",
22+
instructions: p`Audit the licenses of all direct npm dependencies.
23+
24+
Direct dependencies: ${p.bash("npm ls --json --depth=0 2>/dev/null | head -300")}
25+
26+
License fields from node_modules: ${p.bash("node -e \"const fs=require('fs'); const d='./node_modules'; if(fs.existsSync(d)) { fs.readdirSync(d).filter(n=>!n.startsWith('.')).slice(0,60).forEach(n=>{ try{const p=JSON.parse(fs.readFileSync(d+'/'+n+'/package.json','utf8')); console.log(n+'|'+(p.license||'NONE'))}catch(e){} }) }\" 2>/dev/null")}
27+
28+
For each direct dependency, use classifyLicense to classify its license. Set hasCopyleft to true if any package has classification copyleft.`,
29+
output: s.object({
30+
packages: s.array(s.object({
31+
name: s.string,
32+
license: s.string,
33+
classification: s.enum("permissive", "copyleft", "unknown"),
34+
})),
35+
hasCopyleft: s.boolean,
36+
}),
37+
tools: [classifyLicense],
38+
});
39+
40+
export default depLicenseAuditor;
41+
```
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# 135 - Test Coverage Mapper
2+
3+
```rig
4+
import { agent, p, s, defineTool } from "rig";
5+
import { repair } from "rig/addons";
6+
7+
const matchTestFile = defineTool("matchTestFile", {
8+
description: "Find matching test files for a source file using filename heuristics",
9+
parameters: s.object({ sourceFile: s.string, testFiles: s.array(s.string) }),
10+
handler: ({ sourceFile, testFiles }) => {
11+
const base = sourceFile.replace(/\.tsx?$/, "").replace(/.*\//, "");
12+
const matches = testFiles.filter(
13+
(t) => t.includes(base + ".test") || t.includes(base + ".spec")
14+
);
15+
return JSON.stringify({ matches, partial: matches.length === 0 ? testFiles.filter((t) => t.includes(base)).slice(0, 2) : [] });
16+
},
17+
});
18+
19+
// Agent role: map each TypeScript source file to its test files using filename heuristics.
20+
const testCoverageMapper = agent({
21+
model: "small",
22+
maxTurns: 4,
23+
addons: [repair()],
24+
instructions: p`Map source files to their test counterparts.
25+
26+
Source files: ${p.bash("find . -type f -name '*.ts' -not -name '*.test.ts' -not -name '*.spec.ts' -not -path '*/node_modules/*' | head -50")}
27+
28+
Test files: ${p.bash("find . -type f \\( -name '*.test.ts' -o -name '*.spec.ts' \\) -not -path '*/node_modules/*' | head -50")}
29+
30+
For each source file use matchTestFile to find matches. Classify coverage: covered (matches found), partial (weak match), uncovered (none). Return a record keyed by source file path.`,
31+
output: s.record(s.object({
32+
coverage: s.enum("covered", "uncovered", "partial"),
33+
testFiles: s.array(s.string),
34+
reason: s.string,
35+
})),
36+
tools: [matchTestFile],
37+
});
38+
39+
export default testCoverageMapper;
40+
```
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# 136 - Pkg Version Drift
2+
3+
```rig
4+
import { agent, p, s } from "rig";
5+
6+
// Agent role: compare installed npm package versions against latest published versions and report drift.
7+
const pkgVersionDrift = agent({
8+
model: "small",
9+
instructions: p`Compare installed npm package versions to the latest published versions.
10+
11+
Package manifest: ${p.read("package.json")}
12+
13+
Installed versions: ${p.bash("npm ls --depth=0 --json 2>/dev/null | head -200")}
14+
15+
Latest versions for key packages: ${p.bash("npm outdated --json 2>/dev/null | head -200 || echo '{}'")}
16+
17+
For each dependency, classify driftLevel: ok (up to date), patch (patch version behind), minor (minor version behind), major (major version behind). Write a human-readable drift summary report via ${p.writeOutput("report", "version-drift-report.md")}. Set reportWritten to true.`,
18+
output: s.object({
19+
packages: s.array(s.object({
20+
name: s.string,
21+
current: s.string,
22+
latest: s.string,
23+
driftLevel: s.enum("ok", "patch", "minor", "major"),
24+
})),
25+
reportWritten: s.boolean,
26+
report: s.string,
27+
}),
28+
});
29+
30+
export default pkgVersionDrift;
31+
```
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# 137 - Git Author Stats
2+
3+
```rig
4+
import { agent, p, s } from "rig";
5+
6+
// Agent role: aggregate commit statistics per git author, including first/last commit dates and total counts.
7+
const gitAuthorStats = agent({
8+
model: "small",
9+
instructions: p`Aggregate git commit statistics per author.
10+
11+
Commit counts by author: ${p.bash("git shortlog -sne --all 2>/dev/null | head -40")}
12+
13+
Commit log with dates: ${p.bash("git log --format='%ae|%ad' --date=short 2>/dev/null | head -200")}
14+
15+
For each author email, compute commitCount, firstCommit date, and lastCommit date. Identify the topAuthor with the most commits. Compute totalCommits across all authors.`,
16+
output: s.object({
17+
authors: s.record(s.object({
18+
commitCount: s.int,
19+
firstCommit: s.string,
20+
lastCommit: s.string,
21+
})),
22+
topAuthor: s.string,
23+
totalCommits: s.int,
24+
}),
25+
});
26+
27+
export default gitAuthorStats;
28+
```
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# 138 - Ts Path Alias Validator
2+
3+
```rig
4+
import { agent, p, s, defineTool } from "rig";
5+
import { existsSync } from "node:fs";
6+
7+
const checkAlias = defineTool("checkAlias", {
8+
description: "Check whether a TypeScript path alias target directory exists",
9+
parameters: s.object({ alias: s.string, target: s.string }),
10+
handler: ({ alias, target }) => {
11+
const resolved = target.replace(/\/\*$/, "");
12+
const exists = existsSync(resolved);
13+
return JSON.stringify({ alias, target, exists });
14+
},
15+
});
16+
17+
// Agent role: validate TypeScript path aliases from tsconfig against actual filesystem paths.
18+
const tsPathAliasValidator = agent({
19+
model: "small",
20+
instructions: p`Validate TypeScript path alias configuration.
21+
22+
tsconfig.json: ${p.read("tsconfig.json")}
23+
24+
Files with aliased imports: ${p.bash("find . -name '*.ts' -not -path '*/node_modules/*' | xargs grep -l 'from [\"\\x27]' 2>/dev/null | head -20")}
25+
26+
Import alias usage: ${p.bash("grep -r --include='*.ts' -h 'from [\"\\x27]' . 2>/dev/null | grep -v node_modules | grep -v '\\.' | head -40")}
27+
28+
For each alias in compilerOptions.paths, use checkAlias to verify the target exists. Classify as valid (exists and used), broken (target missing), or unused (exists but no imports found). Report totalAliases and brokenCount.`,
29+
output: s.object({
30+
aliases: s.record(s.object({
31+
target: s.string,
32+
status: s.enum("valid", "broken", "unused"),
33+
})),
34+
totalAliases: s.int,
35+
brokenCount: s.int,
36+
}),
37+
tools: [checkAlias],
38+
});
39+
40+
export default tsPathAliasValidator;
41+
```
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# 139 - Markdown Frontmatter Checker
2+
3+
```rig
4+
import { agent, p, s, defineTool } from "rig";
5+
import { repair } from "rig/addons";
6+
import { readFileSync, existsSync } from "node:fs";
7+
8+
const parseFrontmatter = defineTool("parseFrontmatter", {
9+
description: "Parse YAML frontmatter from a markdown file and check for required fields",
10+
parameters: s.object({ filePath: s.string }),
11+
handler: ({ filePath }) => {
12+
if (!existsSync(filePath)) return JSON.stringify({ error: "file not found" });
13+
const content = readFileSync(filePath, "utf8");
14+
if (!content.startsWith("---")) return JSON.stringify({ presentFields: [], missingFields: ["title", "description", "date"], status: "missing" });
15+
const end = content.indexOf("---", 3);
16+
if (end === -1) return JSON.stringify({ presentFields: [], missingFields: ["title", "description", "date"], status: "missing" });
17+
const fm = content.slice(3, end);
18+
const required = ["title", "description", "date"];
19+
const present = required.filter((f) => new RegExp(`^${f}\\s*:`, "m").test(fm));
20+
const missing = required.filter((f) => !present.includes(f));
21+
const status = missing.length === 0 ? "complete" : present.length === 0 ? "missing" : "partial";
22+
return JSON.stringify({ presentFields: present, missingFields: missing, status });
23+
},
24+
});
25+
26+
// Agent role: check markdown files for required YAML frontmatter fields (title, description, date).
27+
const markdownFrontmatterChecker = agent({
28+
model: "small",
29+
maxTurns: 4,
30+
addons: [repair()],
31+
instructions: p`Check markdown files for required YAML frontmatter fields.
32+
33+
Markdown files: ${p.bash("find . -name '*.md' -not -path '*/node_modules/*' | head -50")}
34+
35+
For each markdown file, use parseFrontmatter to detect which required fields (title, description, date) are present or missing. Return a record keyed by filename.`,
36+
output: s.record(s.object({
37+
presentFields: s.array(s.string),
38+
missingFields: s.array(s.string),
39+
status: s.enum("complete", "partial", "missing"),
40+
})),
41+
tools: [parseFrontmatter],
42+
});
43+
44+
export default markdownFrontmatterChecker;
45+
```

0 commit comments

Comments
 (0)