ralph-hero-mcp-server 2.4.15 → 2.4.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/routing-config.js +100 -0
- package/dist/tools/routing-tools.js +93 -7
- package/package.json +1 -1
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Routing config loader and live validation.
|
|
3
|
+
*
|
|
4
|
+
* Two-phase validation:
|
|
5
|
+
* 1. Structural: YAML parse + Zod schema validation (loadRoutingConfig)
|
|
6
|
+
* 2. Referential: field option checks against FieldOptionCache (validateRulesLive)
|
|
7
|
+
*
|
|
8
|
+
* Consumers:
|
|
9
|
+
* - #178 configure_routing tool: loads config for CRUD operations
|
|
10
|
+
* - #179 validate_rules operation: calls validateRulesLive
|
|
11
|
+
*/
|
|
12
|
+
import { readFile } from "fs/promises";
|
|
13
|
+
import { parse as yamlParse } from "yaml";
|
|
14
|
+
import { RoutingConfigSchema } from "./routing-types.js";
|
|
15
|
+
const DEFAULT_CONFIG = {
|
|
16
|
+
version: 1,
|
|
17
|
+
stopOnFirstMatch: true,
|
|
18
|
+
rules: [],
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Load and validate a routing config file.
|
|
22
|
+
*
|
|
23
|
+
* Returns a discriminated union:
|
|
24
|
+
* - "loaded": valid config parsed from file
|
|
25
|
+
* - "missing": file not found, returns default empty config
|
|
26
|
+
* - "error": YAML parse or schema validation errors
|
|
27
|
+
*/
|
|
28
|
+
export async function loadRoutingConfig(configPath) {
|
|
29
|
+
// Read file — gracefully handle missing files
|
|
30
|
+
let contents;
|
|
31
|
+
try {
|
|
32
|
+
contents = await readFile(configPath, "utf-8");
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
if (err.code === "ENOENT") {
|
|
36
|
+
return { status: "missing", config: DEFAULT_CONFIG };
|
|
37
|
+
}
|
|
38
|
+
throw err;
|
|
39
|
+
}
|
|
40
|
+
// Parse YAML
|
|
41
|
+
let parsed;
|
|
42
|
+
try {
|
|
43
|
+
parsed = yamlParse(contents);
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
return {
|
|
47
|
+
status: "error",
|
|
48
|
+
errors: [
|
|
49
|
+
{
|
|
50
|
+
phase: "yaml_parse",
|
|
51
|
+
path: [],
|
|
52
|
+
message: err instanceof Error ? err.message : String(err),
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
// Validate against Zod schema
|
|
58
|
+
const result = RoutingConfigSchema.safeParse(parsed);
|
|
59
|
+
if (!result.success) {
|
|
60
|
+
const errors = result.error.issues.map((issue) => ({
|
|
61
|
+
phase: "schema_validation",
|
|
62
|
+
path: issue.path.map(String),
|
|
63
|
+
message: issue.message,
|
|
64
|
+
}));
|
|
65
|
+
return { status: "error", errors };
|
|
66
|
+
}
|
|
67
|
+
return { status: "loaded", config: result.data, filePath: configPath };
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Validate routing rules against live project field data.
|
|
71
|
+
*
|
|
72
|
+
* Checks that referenced workflow states exist in the FieldOptionCache.
|
|
73
|
+
* The caller must ensure the cache is populated (via ensureFieldCache)
|
|
74
|
+
* before calling this function.
|
|
75
|
+
*
|
|
76
|
+
* Skips disabled rules (enabled === false).
|
|
77
|
+
*/
|
|
78
|
+
export function validateRulesLive(config, fieldCache) {
|
|
79
|
+
const errors = [];
|
|
80
|
+
for (let i = 0; i < config.rules.length; i++) {
|
|
81
|
+
const rule = config.rules[i];
|
|
82
|
+
// Skip disabled rules
|
|
83
|
+
if (rule.enabled === false)
|
|
84
|
+
continue;
|
|
85
|
+
// Validate workflowState references
|
|
86
|
+
if (rule.action.workflowState) {
|
|
87
|
+
const optionId = fieldCache.resolveOptionId("Workflow State", rule.action.workflowState);
|
|
88
|
+
if (optionId === undefined) {
|
|
89
|
+
const valid = fieldCache.getOptionNames("Workflow State");
|
|
90
|
+
errors.push({
|
|
91
|
+
phase: "live_validation",
|
|
92
|
+
path: ["rules", String(i), "action", "workflowState"],
|
|
93
|
+
message: `Workflow state "${rule.action.workflowState}" not found in project. Valid: ${valid.join(", ")}`,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return errors;
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=routing-config.js.map
|
|
@@ -1,21 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MCP tool for managing routing rules in .ralph-routing.yml.
|
|
3
3
|
*
|
|
4
|
-
* Provides a single `ralph_hero__configure_routing` tool with
|
|
5
|
-
*
|
|
4
|
+
* Provides a single `ralph_hero__configure_routing` tool with six
|
|
5
|
+
* operations: list_rules, add_rule, update_rule, remove_rule,
|
|
6
|
+
* validate_rules, dry_run.
|
|
6
7
|
*/
|
|
7
8
|
import fs from "node:fs/promises";
|
|
8
9
|
import { parse, stringify } from "yaml";
|
|
9
10
|
import { z } from "zod";
|
|
10
|
-
import { toolSuccess, toolError } from "../types.js";
|
|
11
|
+
import { toolSuccess, toolError, resolveProjectOwner } from "../types.js";
|
|
12
|
+
import { ensureFieldCache } from "../lib/helpers.js";
|
|
13
|
+
import { validateRoutingConfig } from "../lib/routing-types.js";
|
|
14
|
+
import { evaluateRules } from "../lib/routing-engine.js";
|
|
11
15
|
// ---------------------------------------------------------------------------
|
|
12
16
|
// Register routing tools
|
|
13
17
|
// ---------------------------------------------------------------------------
|
|
14
|
-
export function registerRoutingTools(server,
|
|
15
|
-
server.tool("ralph_hero__configure_routing", "Manage routing rules in .ralph-routing.yml.
|
|
18
|
+
export function registerRoutingTools(server, client, fieldCache) {
|
|
19
|
+
server.tool("ralph_hero__configure_routing", "Manage routing rules in .ralph-routing.yml. Operations: list_rules, add_rule, update_rule, remove_rule (CRUD), validate_rules (check field references), dry_run (simulate matching for an issue). Config path: configPath arg > RALPH_ROUTING_CONFIG env var > .ralph-routing.yml.", {
|
|
16
20
|
operation: z
|
|
17
|
-
.enum([
|
|
18
|
-
|
|
21
|
+
.enum([
|
|
22
|
+
"list_rules",
|
|
23
|
+
"add_rule",
|
|
24
|
+
"update_rule",
|
|
25
|
+
"remove_rule",
|
|
26
|
+
"validate_rules",
|
|
27
|
+
"dry_run",
|
|
28
|
+
])
|
|
29
|
+
.describe("Operation to perform"),
|
|
19
30
|
configPath: z
|
|
20
31
|
.string()
|
|
21
32
|
.optional()
|
|
@@ -37,6 +48,10 @@ export function registerRoutingTools(server, _client, _fieldCache) {
|
|
|
37
48
|
.number()
|
|
38
49
|
.optional()
|
|
39
50
|
.describe("Zero-based rule index (required for update_rule, remove_rule)"),
|
|
51
|
+
issueNumber: z
|
|
52
|
+
.number()
|
|
53
|
+
.optional()
|
|
54
|
+
.describe("Issue number (required for dry_run)"),
|
|
40
55
|
}, async (args) => {
|
|
41
56
|
const configPath = args.configPath ??
|
|
42
57
|
process.env.RALPH_ROUTING_CONFIG ??
|
|
@@ -73,6 +88,77 @@ export function registerRoutingTools(server, _client, _fieldCache) {
|
|
|
73
88
|
config.rules = config.rules.filter((_, i) => i !== args.ruleIndex);
|
|
74
89
|
await fs.writeFile(configPath, stringify(config, { lineWidth: 0 }));
|
|
75
90
|
return toolSuccess({ rules: config.rules, configPath });
|
|
91
|
+
case "validate_rules": {
|
|
92
|
+
const errors = [];
|
|
93
|
+
// Populate field cache for the default project
|
|
94
|
+
const projectOwner = resolveProjectOwner(client.config);
|
|
95
|
+
const projectNumber = client.config.projectNumber;
|
|
96
|
+
if (projectOwner && projectNumber) {
|
|
97
|
+
await ensureFieldCache(client, fieldCache, projectOwner, projectNumber);
|
|
98
|
+
}
|
|
99
|
+
for (let i = 0; i < config.rules.length; i++) {
|
|
100
|
+
const rule = config.rules[i];
|
|
101
|
+
if (rule.action.workflowState) {
|
|
102
|
+
const optionId = fieldCache.resolveOptionId("Workflow State", rule.action.workflowState);
|
|
103
|
+
if (optionId === undefined) {
|
|
104
|
+
const valid = fieldCache.getOptionNames("Workflow State");
|
|
105
|
+
errors.push({
|
|
106
|
+
ruleIndex: i,
|
|
107
|
+
field: "action.workflowState",
|
|
108
|
+
message: `"${rule.action.workflowState}" is not a valid Workflow State. Valid: ${valid.join(", ")}`,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return toolSuccess({
|
|
114
|
+
valid: errors.length === 0,
|
|
115
|
+
ruleCount: config.rules.length,
|
|
116
|
+
errors,
|
|
117
|
+
configPath,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
case "dry_run": {
|
|
121
|
+
if (!args.issueNumber) {
|
|
122
|
+
return toolError("issueNumber is required for dry_run operation");
|
|
123
|
+
}
|
|
124
|
+
const owner = client.config.owner;
|
|
125
|
+
const repo = client.config.repo;
|
|
126
|
+
if (!owner || !repo) {
|
|
127
|
+
return toolError("owner and repo must be configured (set RALPH_GH_OWNER and RALPH_GH_REPO env vars)");
|
|
128
|
+
}
|
|
129
|
+
// Parse config through Zod for proper RoutingConfig type
|
|
130
|
+
const typedConfig = validateRoutingConfig(raw ? parse(raw) : { version: 1, rules: [] });
|
|
131
|
+
// Fetch issue details
|
|
132
|
+
const issueResult = await client.query(`query($owner: String!, $repo: String!, $issueNum: Int!) {
|
|
133
|
+
repository(owner: $owner, name: $repo) {
|
|
134
|
+
issue(number: $issueNum) {
|
|
135
|
+
number
|
|
136
|
+
title
|
|
137
|
+
labels(first: 20) { nodes { name } }
|
|
138
|
+
repository { nameWithOwner }
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}`, { owner, repo, issueNum: args.issueNumber });
|
|
142
|
+
const issue = issueResult.repository?.issue;
|
|
143
|
+
if (!issue) {
|
|
144
|
+
return toolError(`Issue #${args.issueNumber} not found in ${owner}/${repo}`);
|
|
145
|
+
}
|
|
146
|
+
const issueContext = {
|
|
147
|
+
repo: issue.repository.nameWithOwner,
|
|
148
|
+
labels: issue.labels.nodes.map((l) => l.name),
|
|
149
|
+
issueType: "issue",
|
|
150
|
+
};
|
|
151
|
+
const evalResult = evaluateRules(typedConfig, issueContext);
|
|
152
|
+
return toolSuccess({
|
|
153
|
+
issueNumber: args.issueNumber,
|
|
154
|
+
issueTitle: issue.title,
|
|
155
|
+
issueContext,
|
|
156
|
+
matchedRules: evalResult.matchedRules,
|
|
157
|
+
stoppedEarly: evalResult.stoppedEarly,
|
|
158
|
+
note: "No mutations performed -- dry run only",
|
|
159
|
+
configPath,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
76
162
|
}
|
|
77
163
|
}
|
|
78
164
|
catch (error) {
|