Skip to content

Commit 3baa3cc

Browse files
Add 10 rig samples (140-149): hotspot, loc-stats, cycle-detect, badges, licenses, coverage, permissions, submodules, decorators, dotfiles
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 35d34c4 commit 3baa3cc

10 files changed

Lines changed: 462 additions & 0 deletions
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# 140 - Git Hotspot Analyzer
2+
3+
```rig
4+
import { agent, p, s, defineTool } from "rig";
5+
import { steering } from "rig/addons";
6+
7+
const scoreFile = defineTool("scoreFile", {
8+
description: "Compute a churn score from commit count for a file.",
9+
parameters: s.object({ filename: s.string, commitCount: s.int }),
10+
handler({ commitCount }) {
11+
const score = Math.log1p(commitCount) * 10;
12+
return { churnScore: Math.round(score * 100) / 100 };
13+
},
14+
});
15+
16+
// Agent role: Analyze git history to identify hot-spot files by churn frequency.
17+
const gitHotspotAnalyzer = agent({
18+
model: "small",
19+
instructions: p`Analyze the git log to find frequently-changed files.
20+
21+
Use the git history:
22+
${p.bash("git log --name-only --format= | sort | uniq -c | sort -rn | head -30")}
23+
24+
For each file in the top results, get contributor info:
25+
${p.bash("git shortlog -sn --no-merges -- . | head -10")}
26+
27+
Use the scoreFile tool to compute a churnScore for each file.
28+
Return s.record output keyed by file path with churnScore, commitCount, and topContributors.`,
29+
tools: [scoreFile],
30+
addons: steering({ message: "Ensure all top files appear in the output keyed by their path." }),
31+
output: s.record(
32+
s.object({
33+
churnScore: s.number,
34+
commitCount: s.int,
35+
topContributors: s.array(s.string),
36+
}),
37+
),
38+
});
39+
40+
export default gitHotspotAnalyzer;
41+
```
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# 141 - LOC Statistics Gatherer
2+
3+
```rig
4+
import { agent, p, s, defineTool } from "rig";
5+
6+
const aggregateByExtension = defineTool("aggregateByExtension", {
7+
description: "Aggregate line counts and file counts by extension from wc -l output.",
8+
parameters: s.object({ wcOutput: s.string }),
9+
handler({ wcOutput }) {
10+
const lines = wcOutput.trim().split("\n");
11+
const result: Record<string, { lineCount: number; fileCount: number }> = {};
12+
for (const line of lines) {
13+
const match = line.trim().match(/^(\d+)\s+(.+)$/);
14+
if (!match) continue;
15+
const count = parseInt(match[1], 10);
16+
const file = match[2];
17+
const ext = file.includes(".") ? "." + file.split(".").pop()! : "(no-ext)";
18+
if (!result[ext]) result[ext] = { lineCount: 0, fileCount: 0 };
19+
result[ext].lineCount += count;
20+
result[ext].fileCount += 1;
21+
}
22+
return result;
23+
},
24+
});
25+
26+
// Agent role: Gather lines-of-code statistics grouped by file extension.
27+
const locStatisticsGatherer = agent({
28+
model: "small",
29+
instructions: p`Count lines of code across the workspace grouped by file extension.
30+
31+
Find source files and count lines:
32+
${p.bash("find . -maxdepth 4 -type f \\( -name '*.ts' -o -name '*.js' -o -name '*.py' -o -name '*.go' -o -name '*.md' \\) ! -path '*/node_modules/*' ! -path '*/.git/*' | xargs wc -l 2>/dev/null | tail -n +1")}
33+
34+
Use the aggregateByExtension tool to group by extension.
35+
Classify each extension's complexity based on total lineCount:
36+
- small: < 500, medium: 500-2000, large: 2000-10000, xlarge: > 10000
37+
Return s.record output keyed by extension with lineCount, fileCount, and complexity.`,
38+
tools: [aggregateByExtension],
39+
output: s.record(
40+
s.object({
41+
lineCount: s.int,
42+
fileCount: s.int,
43+
complexity: s.enum("small", "medium", "large", "xlarge"),
44+
}),
45+
),
46+
});
47+
48+
export default locStatisticsGatherer;
49+
```
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# 142 - Import Cycle Detector
2+
3+
```rig
4+
import { agent, p, s } from "rig";
5+
import { repair } from "rig/addons";
6+
7+
// Agent role: Detect import cycles in TypeScript source using madge and classify severity.
8+
const importCycleDetector = agent({
9+
model: "small",
10+
maxTurns: 3,
11+
instructions: p`Detect circular import cycles in this TypeScript project.
12+
13+
Run madge to find circular dependencies:
14+
${p.bash("npx madge --circular --json . 2>/dev/null || echo '[]'")}
15+
16+
Also read the TypeScript config:
17+
${p.readOptional("tsconfig.json", "{}")}
18+
19+
For each cycle found, classify severity:
20+
- high: cycle involves more than 3 files or includes entry points
21+
- medium: cycle involves 2-3 files
22+
- low: short cycle between utility files
23+
24+
Return the structured output with hasCycles, cycles array, and totalCycles count.`,
25+
addons: repair(),
26+
output: s.object({
27+
hasCycles: s.boolean,
28+
cycles: s.array(
29+
s.object({
30+
path: s.array(s.string),
31+
severity: s.enum("high", "medium", "low"),
32+
}),
33+
),
34+
totalCycles: s.int,
35+
}),
36+
});
37+
38+
export default importCycleDetector;
39+
```
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# 143 - Coverage Badge Updater
2+
3+
```rig
4+
import { agent, p, s } from "rig";
5+
6+
// Agent role: Update README.md with shields.io coverage badges from coverage summary JSON.
7+
const coverageBadgeUpdater = agent({
8+
model: "small",
9+
instructions: p`Read the coverage summary and generate shields.io badge markdown.
10+
11+
Coverage summary:
12+
${p.readOptional("coverage/coverage-summary.json", "{}")}
13+
14+
Compute coverage percentages per category (lines, statements, functions, branches) and overall.
15+
Generate shields.io badge markdown URL for each category.
16+
Classify overall coverage rating:
17+
- green: >= 80%, yellow: 50-79%, red: < 50%
18+
19+
Write the badge summary markdown to README.md using the p.writeOutput intent below.
20+
${p.writeOutput("badgeSummary", "README.md")}
21+
22+
Return coverageByCategory (record of percentages), overallPct, rating, badgesWritten, and badgeSummary.`,
23+
output: s.object({
24+
coverageByCategory: s.record(s.number),
25+
overallPct: s.number,
26+
rating: s.enum("green", "yellow", "red"),
27+
badgesWritten: s.boolean,
28+
badgeSummary: s.string,
29+
}),
30+
});
31+
32+
export default coverageBadgeUpdater;
33+
```
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# 144 - Dep License Auditor
2+
3+
```rig
4+
import { agent, p, s, defineTool } from "rig";
5+
6+
const extractLicense = defineTool("extractLicense", {
7+
description: "Read license field from a package in node_modules.",
8+
parameters: s.object({ packageName: s.string }),
9+
async handler({ packageName }) {
10+
const { readFileSync } = await import("node:fs");
11+
try {
12+
const pkgPath = `node_modules/${packageName}/package.json`;
13+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
14+
const license: string = pkg.license ?? pkg.licenses?.[0]?.type ?? "UNKNOWN";
15+
let category: "permissive" | "copyleft" | "unknown" = "unknown";
16+
const upper = license.toUpperCase();
17+
if (["MIT", "ISC", "BSD", "APACHE", "0BSD", "WTFPL"].some(l => upper.includes(l))) {
18+
category = "permissive";
19+
} else if (["GPL", "LGPL", "AGPL", "MPL", "EUPL"].some(l => upper.includes(l))) {
20+
category = "copyleft";
21+
}
22+
return { license, category };
23+
} catch {
24+
return { license: "UNKNOWN", category: "unknown" as const };
25+
}
26+
},
27+
});
28+
29+
// Agent role: Audit dependency licenses and flag copyleft packages.
30+
const depLicenseAuditor = agent({
31+
model: "small",
32+
instructions: p`Audit dependency licenses for this project.
33+
34+
List installed packages:
35+
${p.bash("npm ls --json --depth=0 2>/dev/null | node -e \"const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));console.log(Object.keys(d.dependencies||{}).join('\\n'))\" 2>/dev/null || ls node_modules | head -50")}
36+
37+
For each package, use the extractLicense tool to get its license and category.
38+
Return packages array, hasCopyleft flag, and totalPackages count.`,
39+
tools: [extractLicense],
40+
output: s.object({
41+
packages: s.array(
42+
s.object({
43+
name: s.string,
44+
license: s.string,
45+
category: s.enum("permissive", "copyleft", "unknown"),
46+
}),
47+
),
48+
hasCopyleft: s.boolean,
49+
totalPackages: s.int,
50+
}),
51+
});
52+
53+
export default depLicenseAuditor;
54+
```
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# 145 - 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: "Heuristically find test files matching a source file by name.",
9+
parameters: s.object({ sourcePath: s.string, testFiles: s.array(s.string) }),
10+
handler({ sourcePath, testFiles }) {
11+
const baseName = sourcePath.split("/").pop()!.replace(/\.ts$/, "");
12+
const patterns = [
13+
`${baseName}.test.ts`,
14+
`${baseName}.spec.ts`,
15+
`${baseName}.test.js`,
16+
`${baseName}.spec.js`,
17+
];
18+
const matched = testFiles.filter(f =>
19+
patterns.some(p => f.endsWith(p))
20+
);
21+
return { matched };
22+
},
23+
});
24+
25+
// Agent role: Map source files to their test counterparts and classify coverage.
26+
const testCoverageMapper = agent({
27+
model: "small",
28+
maxTurns: 2,
29+
instructions: p`Map source files to test files using filename heuristics.
30+
31+
Source files:
32+
${p.bash("find . -type f -name '*.ts' ! -name '*.test.ts' ! -name '*.spec.ts' ! -path '*/node_modules/*' ! -path '*/.git/*' | head -40")}
33+
34+
Test files:
35+
${p.bash("find . -type f \\( -name '*.test.ts' -o -name '*.spec.ts' \\) ! -path '*/node_modules/*' | head -40")}
36+
37+
For each source file, use matchTestFile to find matching test files.
38+
Classify coverage:
39+
- covered: at least one test file matched
40+
- uncovered: no test files matched
41+
- partial: test file exists but may not cover all exports (use heuristics)
42+
43+
Return s.record output keyed by source file path.`,
44+
tools: [matchTestFile],
45+
addons: repair(),
46+
output: s.record(
47+
s.object({
48+
coverage: s.enum("covered", "uncovered", "partial"),
49+
testFiles: s.array(s.string),
50+
reason: s.string,
51+
}),
52+
),
53+
});
54+
55+
export default testCoverageMapper;
56+
```
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# 146 - File Permission Scanner
2+
3+
```rig
4+
import { agent, p, s, defineTool } from "rig";
5+
6+
const parsePermissions = defineTool("parsePermissions", {
7+
description: "Parse a unix permission string and classify risk level.",
8+
parameters: s.object({ permissions: s.string, filename: s.string }),
9+
handler({ permissions }) {
10+
const isExecutable = permissions[3] === "x" || permissions[6] === "x" || permissions[9] === "x";
11+
const isWorldWritable = permissions[8] === "w";
12+
let riskLevel: "safe" | "warn" | "danger" = "safe";
13+
if (isWorldWritable) riskLevel = "danger";
14+
else if (isExecutable) riskLevel = "warn";
15+
return { isExecutable, isWorldWritable, riskLevel };
16+
},
17+
});
18+
19+
// Agent role: Scan workspace file permissions and flag dangerous or unusual access modes.
20+
const filePermissionScanner = agent({
21+
model: "small",
22+
instructions: p`Scan file permissions in the workspace and classify risk.
23+
24+
List files with permissions:
25+
${p.bash("find . -maxdepth 3 -type f ! -path '*/node_modules/*' ! -path '*/.git/*' | head -50 | xargs ls -la 2>/dev/null")}
26+
27+
For each file, use parsePermissions to determine isExecutable, isWorldWritable, and riskLevel.
28+
Return s.object with files record (keyed by filename), dangerCount, and allSafe.`,
29+
tools: [parsePermissions],
30+
output: s.object({
31+
files: s.record(
32+
s.object({
33+
mode: s.string,
34+
isExecutable: s.boolean,
35+
isWorldWritable: s.boolean,
36+
riskLevel: s.enum("safe", "warn", "danger"),
37+
}),
38+
),
39+
dangerCount: s.int,
40+
allSafe: s.boolean,
41+
}),
42+
});
43+
44+
export default filePermissionScanner;
45+
```
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# 147 - Git Submodule Health
2+
3+
```rig
4+
import { agent, p, s, defineTool } from "rig";
5+
6+
const parseSubmoduleStatus = defineTool("parseSubmoduleStatus", {
7+
description: "Parse a git submodule status line into structured fields.",
8+
parameters: s.object({ line: s.string }),
9+
handler({ line }) {
10+
const trimmed = line.trimStart();
11+
if (!trimmed) return null;
12+
const statusChar = line[0];
13+
const rest = trimmed.replace(/^[\+\- U]/, "").trim();
14+
const parts = rest.split(/\s+/);
15+
const sha = parts[0] ?? "";
16+
const path = parts[1] ?? "";
17+
let status: "clean" | "modified" | "uninitialized" | "missing" = "clean";
18+
if (statusChar === "+") status = "modified";
19+
else if (statusChar === "-") status = "uninitialized";
20+
else if (statusChar === "U") status = "missing";
21+
return { path, sha, status };
22+
},
23+
});
24+
25+
// Agent role: Inventory git submodules and report their health status.
26+
const gitSubmoduleHealth = agent({
27+
model: "small",
28+
instructions: p`Check the health of all git submodules in this repository.
29+
30+
Submodule status:
31+
${p.bash("git submodule status 2>/dev/null || echo '(no submodules)'")}
32+
33+
Submodule config:
34+
${p.readOptional(".gitmodules", "(no .gitmodules file)")}
35+
36+
For each status line, use parseSubmoduleStatus to extract path, sha, and status.
37+
Return submodules array, allClean flag, and totalCount.`,
38+
tools: [parseSubmoduleStatus],
39+
output: s.object({
40+
submodules: s.array(
41+
s.object({
42+
path: s.string,
43+
sha: s.string,
44+
status: s.enum("clean", "modified", "uninitialized", "missing"),
45+
}),
46+
),
47+
allClean: s.boolean,
48+
totalCount: s.int,
49+
}),
50+
});
51+
52+
export default gitSubmoduleHealth;
53+
```

0 commit comments

Comments
 (0)