Skip to content

Commit 2752690

Browse files
Add 10 rig samples 351-360 — 2026-08-02 (#336)
1 parent 61800ab commit 2752690

10 files changed

Lines changed: 549 additions & 0 deletions
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# 351 - TS Import Depth Analyzer V2
2+
3+
```rig
4+
import { agent, p, s, defineTool, repair } from "rig";
5+
import { readFile } from "node:fs/promises";
6+
7+
const analyzeImportDepth = defineTool("analyzeImportDepth", {
8+
description: "Analyze relative import depth in a TypeScript file by counting '../' occurrences.",
9+
parameters: { filePath: s.path },
10+
handler: async ({ filePath }: { filePath: string }) => {
11+
const content = await readFile(filePath, "utf8");
12+
const importRegex = /from\s+['"]([^'"]+)['"]/g;
13+
const imports: string[] = [];
14+
let match: RegExpExecArray | null;
15+
while ((match = importRegex.exec(content)) !== null) {
16+
imports.push(match[1]);
17+
}
18+
const relativeImports = imports.filter((i: string) => i.startsWith("."));
19+
const depths = relativeImports.map((i: string) => (i.match(/\.\.\//g) ?? []).length);
20+
const maxDepth = depths.length > 0 ? Math.max(...depths) : 0;
21+
const deepImports = relativeImports.filter((i: string) => (i.match(/\.\.\//g) ?? []).length >= 2);
22+
return { maxDepth, deepImports, importCount: imports.length };
23+
},
24+
});
25+
26+
// Agent role: analyze relative import depth across TypeScript files to find overly deep imports.
27+
const tsImportDepthAnalyzer = agent({
28+
model: "small",
29+
instructions: p`Analyze TypeScript import depth across all source files.
30+
31+
Source files to analyze: ${p.glob("**/*.ts")}
32+
33+
For each .ts file (excluding node_modules), call analyzeImportDepth to get maxDepth, deepImports, and importCount.
34+
Calculate the average depth across all files (sum of maxDepths / file count).
35+
Identify the file with the highest maxDepth as deepestFile (omit if no files).
36+
Return the full per-file record plus summary stats.`,
37+
output: s.object({
38+
files: s.record(s.object({
39+
maxDepth: s.int,
40+
deepImports: s.array(s.string),
41+
importCount: s.int,
42+
})),
43+
deepestFile: s.optional(s.path),
44+
averageDepth: s.number,
45+
}),
46+
tools: [analyzeImportDepth],
47+
addons: [repair()],
48+
});
49+
50+
export default tsImportDepthAnalyzer;
51+
52+
```
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# 352 - Env File Completeness Checker V2
2+
3+
```rig
4+
import { agent, p, s, defineTool, repair } from "rig";
5+
6+
const parseEnvKeys = defineTool("parseEnvKeys", {
7+
description: "Parse an env file string and return the list of key names.",
8+
parameters: { content: s.string },
9+
handler: ({ content }: { content: string }) => {
10+
return content
11+
.split("\n")
12+
.map((line: string) => line.trim())
13+
.filter((line: string) => line.length > 0 && !line.startsWith("#"))
14+
.map((line: string) => line.split("=")[0].trim())
15+
.filter(Boolean);
16+
},
17+
});
18+
19+
// Agent role: compare .env.example with .env to check completeness of environment configuration.
20+
const envFileCompletenessChecker = agent({
21+
model: "small",
22+
instructions: p`Compare .env.example with .env to check that all required keys are present.
23+
24+
.env.example content: ${p.readOptional(".env.example")}
25+
26+
.env content: ${p.readOptional(".env")}
27+
28+
Steps:
29+
1. Call parseEnvKeys with the .env.example content to get exampleKeys.
30+
2. Call parseEnvKeys with the .env content to get presentKeys.
31+
3. Compute missingKeys (keys in exampleKeys but not presentKeys) and extraKeys (keys in presentKeys but not exampleKeys).
32+
4. Compute completeness as (exampleKeys.length - missingKeys.length) / exampleKeys.length, or 1.0 if exampleKeys is empty.
33+
5. Set status: "missing-example" if .env.example is absent, "missing-env" if .env is absent, "complete" if missingKeys is empty, "partial" otherwise.`,
34+
output: s.object({
35+
exampleKeys: s.array(s.string),
36+
presentKeys: s.array(s.string),
37+
missingKeys: s.array(s.string),
38+
extraKeys: s.array(s.string),
39+
completeness: s.number,
40+
status: s.enum("complete", "partial", "missing-example", "missing-env"),
41+
}),
42+
tools: [parseEnvKeys],
43+
addons: [repair()],
44+
});
45+
46+
export default envFileCompletenessChecker;
47+
48+
```
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# 353 - Stale Branch Detector V2
2+
3+
```rig
4+
import { agent, p, s, defineTool, repair } from "rig";
5+
6+
const classifyBranchAge = defineTool("classifyBranchAge", {
7+
description: "Classify a git branch as fresh, stale, or dead based on its last commit date.",
8+
parameters: { branchName: s.string, lastCommitDate: s.string },
9+
handler: ({ lastCommitDate }: { branchName: string; lastCommitDate: string }) => {
10+
const ageMs = Date.now() - new Date(lastCommitDate).getTime();
11+
const ageDays = ageMs / (1000 * 60 * 60 * 24);
12+
if (ageDays < 30) return "fresh" as const;
13+
if (ageDays < 90) return "stale" as const;
14+
return "dead" as const;
15+
},
16+
});
17+
18+
// Agent role: detect stale and dead local git branches and recommend candidates for deletion.
19+
const staleBranchDetector = agent({
20+
model: "small",
21+
instructions: p`Detect stale and dead local git branches.
22+
23+
Branch list with last commit dates:
24+
${p.bash("git for-each-ref --format='%(refname:short)|%(committerdate:iso8601)' refs/heads 2>/dev/null || echo ''")}
25+
26+
Steps:
27+
1. Parse each line as "branchName|lastCommitDate".
28+
2. For each branch, call classifyBranchAge to get its classification.
29+
3. Build the branches array with name, lastCommit, and classification fields.
30+
4. Count staleCount (classification = "stale") and deadCount (classification = "dead").
31+
5. Set recommendedForDeletion to the names of branches where classification is "dead".`,
32+
output: s.object({
33+
branches: s.array(s.object({
34+
name: s.string,
35+
lastCommit: s.string,
36+
classification: s.enum("fresh", "stale", "dead"),
37+
})),
38+
staleCount: s.int,
39+
deadCount: s.int,
40+
recommendedForDeletion: s.array(s.string),
41+
}),
42+
tools: [classifyBranchAge],
43+
maxTurns: 6,
44+
addons: [repair()],
45+
});
46+
47+
export default staleBranchDetector;
48+
49+
```
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# 354 - License Header Checker V2
2+
3+
```rig
4+
import { agent, p, s, defineTool, repair } from "rig";
5+
import { readFile } from "node:fs/promises";
6+
7+
const checkLicenseHeader = defineTool("checkLicenseHeader", {
8+
description: "Check whether a TypeScript file starts with the expected license header.",
9+
parameters: { filePath: s.string, expectedHeader: s.string },
10+
handler: async ({ filePath, expectedHeader }: { filePath: string; expectedHeader: string }) => {
11+
try {
12+
const content = await readFile(filePath, "utf-8");
13+
const headerLines = expectedHeader.split("\n");
14+
const fileStart = content.split("\n").slice(0, headerLines.length).join("\n");
15+
const hasHeader = fileStart === expectedHeader;
16+
const status = hasHeader
17+
? ("ok" as const)
18+
: content.includes(headerLines[0])
19+
? ("wrong" as const)
20+
: ("missing" as const);
21+
return { hasHeader, status };
22+
} catch {
23+
return { hasHeader: false, status: "missing" as const };
24+
}
25+
},
26+
});
27+
28+
// Agent role: check that all TypeScript source files contain the expected license header.
29+
const licenseHeaderChecker = agent({
30+
model: "small",
31+
input: s.object({ expectedHeader: s.string }),
32+
instructions: p`Check each TypeScript file for the expected license header provided in the input.
33+
34+
TypeScript files in the workspace:
35+
${p.bash("find . -name '*.ts' -not -path '*/node_modules/*' -not -path '*/.git/*' | head -60")}
36+
37+
Steps:
38+
1. Read expectedHeader from the agent input.
39+
2. For each file path, call checkLicenseHeader with filePath and expectedHeader.
40+
3. Build a files record keyed by file path containing hasHeader and status.
41+
4. Count missingCount as files where status is "missing" or "wrong".
42+
5. Set allCompliant to true only if missingCount is 0.`,
43+
output: s.object({
44+
files: s.record(s.object({
45+
hasHeader: s.boolean,
46+
status: s.enum("ok", "missing", "wrong"),
47+
})),
48+
missingCount: s.int,
49+
allCompliant: s.boolean,
50+
}),
51+
tools: [checkLicenseHeader],
52+
maxTurns: 8,
53+
addons: [repair()],
54+
});
55+
56+
export default licenseHeaderChecker;
57+
58+
```
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# 355 - YAML Config Diff V2
2+
3+
```rig
4+
import { agent, p, s, defineTool, repair } from "rig";
5+
6+
const extractYamlKeys = defineTool("extractYamlKeys", {
7+
description: "Extract top-level keys from a YAML string.",
8+
parameters: { content: s.string },
9+
handler: ({ content }: { content: string }) => {
10+
const keys: string[] = [];
11+
for (const line of content.split("\n")) {
12+
const m = line.match(/^([a-zA-Z_][a-zA-Z0-9_-]*):/);
13+
if (m) keys.push(m[1]);
14+
}
15+
return keys;
16+
},
17+
});
18+
19+
const diffKeys = defineTool("diffKeys", {
20+
description: "Compute added and removed keys between two key arrays.",
21+
parameters: { baseKeys: s.array(s.string), targetKeys: s.array(s.string) },
22+
handler: ({ baseKeys, targetKeys }: { baseKeys: string[]; targetKeys: string[] }) => {
23+
const addedKeys = targetKeys.filter((k: string) => !baseKeys.includes(k));
24+
const removedKeys = baseKeys.filter((k: string) => !targetKeys.includes(k));
25+
return { addedKeys, removedKeys };
26+
},
27+
});
28+
29+
// Agent role: diff top-level YAML keys between two config files and detect breaking changes.
30+
const yamlConfigDiff = agent({
31+
model: "small",
32+
input: s.object({ baseFile: s.path, targetFile: s.path }),
33+
instructions: p`Diff top-level YAML keys between two config files.
34+
35+
Base file content: ${p.readInput("baseFile")}
36+
Target file content: ${p.readInput("targetFile")}
37+
38+
Steps:
39+
1. Call extractYamlKeys on the base file content to get baseKeys.
40+
2. Call extractYamlKeys on the target file content to get targetKeys.
41+
3. Call diffKeys with baseKeys and targetKeys to get addedKeys and removedKeys.
42+
4. changedKeys: keys present in both but with differing values — estimate from content or leave empty.
43+
5. totalChanges = addedKeys.length + removedKeys.length + changedKeys.length.
44+
6. hasBreakingChanges = removedKeys.length > 0.`,
45+
output: s.object({
46+
addedKeys: s.array(s.string),
47+
removedKeys: s.array(s.string),
48+
changedKeys: s.array(s.string),
49+
totalChanges: s.int,
50+
hasBreakingChanges: s.boolean,
51+
}),
52+
tools: [extractYamlKeys, diffKeys],
53+
maxTurns: 6,
54+
addons: [repair()],
55+
});
56+
57+
export default yamlConfigDiff;
58+
59+
```
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# 356 - Monorepo Workspace Lister V2
2+
3+
```rig
4+
import { agent, p, s, defineTool, steering } from "rig";
5+
import { readFile } from "node:fs/promises";
6+
7+
const extractPackageInfo = defineTool("extractPackageInfo", {
8+
description: "Read a nested package.json and extract name, version, dependency count, and private flag.",
9+
parameters: { filePath: s.path },
10+
handler: async ({ filePath }: { filePath: string }) => {
11+
try {
12+
const content = await readFile(filePath, "utf-8");
13+
const pkg = JSON.parse(content);
14+
const dependencyCount =
15+
Object.keys(pkg.dependencies ?? {}).length +
16+
Object.keys(pkg.devDependencies ?? {}).length;
17+
return {
18+
name: (pkg.name ?? "(unnamed)") as string,
19+
version: (pkg.version ?? undefined) as string | undefined,
20+
dependencyCount,
21+
hasPrivate: pkg.private === true,
22+
};
23+
} catch {
24+
return { name: "(error)", version: undefined, dependencyCount: 0, hasPrivate: false };
25+
}
26+
},
27+
});
28+
29+
// Agent role: discover all workspace packages in a monorepo and list their metadata.
30+
const monorepoWorkspaceLister = agent({
31+
model: "small",
32+
instructions: p`Discover all nested package.json files in this monorepo and extract package metadata.
33+
34+
Nested package.json paths (excluding node_modules):
35+
${p.bash("find . -name 'package.json' -not -path '*/node_modules/*' -mindepth 2 -maxdepth 4 2>/dev/null")}
36+
37+
Steps:
38+
1. For each file path in the list above, call extractPackageInfo to get its metadata.
39+
2. Assemble the packages array with name, version (optional), dependencyCount, hasPrivate, and path for each.
40+
3. Set totalPackages to the length of the packages array.`,
41+
output: s.object({
42+
packages: s.array(s.object({
43+
name: s.string,
44+
version: s.optional(s.string),
45+
dependencyCount: s.int,
46+
hasPrivate: s.boolean,
47+
path: s.path,
48+
})),
49+
totalPackages: s.int,
50+
}),
51+
tools: [extractPackageInfo],
52+
maxTurns: 8,
53+
addons: [steering()],
54+
});
55+
56+
export default monorepoWorkspaceLister;
57+
58+
```
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# 357 - TS Decorator Usage Scanner
2+
3+
```rig
4+
import { agent, p, s, defineTool, repair } from "rig";
5+
import { readFile } from "node:fs/promises";
6+
7+
const scanDecorators = defineTool("scanDecorators", {
8+
description: "Scan a TypeScript file for decorator usage and return decorator names with their count.",
9+
parameters: { filePath: s.path },
10+
handler: async ({ filePath }: { filePath: string }) => {
11+
try {
12+
const content = await readFile(filePath, "utf-8");
13+
const decoratorRegex = /@([A-Z][a-zA-Z0-9]*)/g;
14+
const found: string[] = [];
15+
let match: RegExpExecArray | null;
16+
while ((match = decoratorRegex.exec(content)) !== null) {
17+
found.push(match[1]);
18+
}
19+
return { decorators: found, hasDecorators: found.length > 0 };
20+
} catch {
21+
return { decorators: [], hasDecorators: false };
22+
}
23+
},
24+
});
25+
26+
// Agent role: scan TypeScript files for decorator usage and summarize which decorators are most common.
27+
const tsDecoratorUsageScanner = agent({
28+
model: "small",
29+
instructions: p`Scan TypeScript files for decorator usage (e.g. @Injectable, @Component).
30+
31+
TypeScript files: ${p.glob("src/**/*.ts")}
32+
33+
Steps:
34+
1. For each file path, call scanDecorators to get the list of decorator names used.
35+
2. Aggregate across all files: build a decorators record keyed by decorator name (without @),
36+
with usageCount (total occurrences) and files (list of file paths where it appears).
37+
3. totalDecorated = number of files that had at least one decorator.
38+
4. mostUsedDecorator = decorator name with highest usageCount (omit if none found).`,
39+
output: s.object({
40+
decorators: s.record(s.object({
41+
usageCount: s.int,
42+
files: s.array(s.path),
43+
})),
44+
totalDecorated: s.int,
45+
mostUsedDecorator: s.optional(s.string),
46+
}),
47+
tools: [scanDecorators],
48+
addons: [repair()],
49+
});
50+
51+
export default tsDecoratorUsageScanner;
52+
53+
```

0 commit comments

Comments
 (0)