Skip to content

Commit e34d43f

Browse files
Merge pull request #6176 from continuedev/tomasz/on-off-rules
Manually triggering rules on and off
2 parents cf54304 + d000fb0 commit e34d43f

File tree

9 files changed

+259
-46
lines changed

9 files changed

+259
-46
lines changed

core/llm/constructMessages.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { findLast } from "../util/findLast";
1111
import { normalizeToMessageParts } from "../util/messageContent";
1212
import { isUserOrToolMsg } from "./messages";
1313
import { getSystemMessageWithRules } from "./rules/getSystemMessageWithRules";
14+
import { RulePolicies } from "./rules/types";
1415

1516
export const DEFAULT_CHAT_SYSTEM_MESSAGE_URL =
1617
"https://github.com/continuedev/continue/blob/main/core/llm/constructMessages.ts";
@@ -108,6 +109,7 @@ export function constructMessages(
108109
history: ChatHistoryItem[],
109110
baseChatOrAgentSystemMessage: string | undefined,
110111
rules: RuleWithSource[],
112+
rulePolicies?: RulePolicies,
111113
): ChatMessage[] {
112114
const filteredHistory = history.filter(
113115
(item) => item.message.role !== "system",
@@ -164,6 +166,7 @@ export function constructMessages(
164166
rules,
165167
userMessage: lastUserMsg,
166168
contextItems: lastUserContextItems,
169+
rulePolicies,
167170
});
168171

169172
if (systemMessage.trim()) {

core/llm/rules/alwaysApplyRules.vitest.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
UserChatMessage,
77
} from "../..";
88
import { getApplicableRules } from "./getSystemMessageWithRules";
9+
import { RulePolicies } from "./types";
910

1011
describe("Rule application with alwaysApply", () => {
1112
// Create an always-apply rule
@@ -136,4 +137,38 @@ describe("Rule application with alwaysApply", () => {
136137
expect(applicableRules).toHaveLength(1);
137138
expect(applicableRules.map((r) => r.name)).toContain("Always Apply Rule");
138139
});
140+
141+
it("should respect 'off' rulePolicies over alwaysApply when there are no file paths", () => {
142+
// Rule with alwaysApply: true
143+
const globalRule: RuleWithSource = {
144+
name: "Global Rule",
145+
rule: "This rule has alwaysApply: true but is blocked by rulePolicies",
146+
alwaysApply: true,
147+
source: "rules-block",
148+
ruleFile: "src/some/path.md",
149+
};
150+
151+
// Message with no code blocks or file references
152+
const simpleMessage: UserChatMessage = {
153+
role: "user",
154+
content: "Can you help me with something?",
155+
};
156+
157+
// Create a rule policy that blocks the rule
158+
const rulePolicies: RulePolicies = {
159+
"Global Rule": "off",
160+
};
161+
162+
// The rule should NOT be applied due to the "off" policy,
163+
// even though it has alwaysApply: true and there are no file paths
164+
const applicableRules = getApplicableRules(
165+
simpleMessage,
166+
[globalRule],
167+
[],
168+
rulePolicies,
169+
);
170+
171+
expect(applicableRules).toHaveLength(0);
172+
expect(applicableRules.map((r) => r.name)).not.toContain("Global Rule");
173+
});
139174
});

core/llm/rules/getSystemMessageWithRules.ts

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
import { renderChatMessage } from "../../util/messageContent";
99
import { getCleanUriPath } from "../../util/uri";
1010
import { extractPathsFromCodeBlocks } from "../utils/extractPathsFromCodeBlocks";
11+
import { RulePolicies } from "./types";
1112

1213
/**
1314
* Checks if a path matches any of the provided globs
@@ -120,13 +121,21 @@ const isGlobalRule = (rule: RuleWithSource): boolean => {
120121
export const shouldApplyRule = (
121122
rule: RuleWithSource,
122123
filePaths: string[],
124+
rulePolicies: RulePolicies = {},
123125
): boolean => {
126+
const policy = rulePolicies[rule.name || ""];
127+
128+
// Never apply if policy is "off"
129+
if (policy === "off") {
130+
return false;
131+
}
132+
124133
// If it's a global rule, always apply it regardless of file paths
125134
if (isGlobalRule(rule)) {
126135
return true;
127136
}
128137

129-
// If there are no file paths to check:
138+
// If there are no file paths to check and we've made it here:
130139
// - We've already handled global rules above
131140
// - Don't apply other rules since we have no files to match against
132141
if (filePaths.length === 0) {
@@ -196,11 +205,9 @@ export const getApplicableRules = (
196205
userMessage: UserChatMessage | ToolResultChatMessage | undefined,
197206
rules: RuleWithSource[],
198207
contextItems: ContextItemWithId[],
208+
rulePolicies: RulePolicies = {},
199209
): RuleWithSource[] => {
200-
// First, extract any global rules that should always apply
201-
const globalRules = rules.filter((rule) => isGlobalRule(rule));
202-
203-
// Get file paths from message and context for regular rule matching
210+
// Get file paths from message and context for rule matching
204211
const filePathsFromMessage = userMessage
205212
? extractPathsFromCodeBlocks(renderChatMessage(userMessage))
206213
: [];
@@ -213,41 +220,34 @@ export const getApplicableRules = (
213220
// Combine file paths from both sources
214221
const allFilePaths = [...filePathsFromMessage, ...filePathsFromContextItems];
215222

216-
// If we have no file paths, just return the global rules
217-
if (allFilePaths.length === 0) {
218-
return globalRules;
219-
}
220-
221-
// Get rules that match file paths
222-
const matchingRules = rules
223-
.filter((rule) => !isGlobalRule(rule)) // Skip global rules as we've already handled them
224-
.filter((rule) => shouldApplyRule(rule, allFilePaths));
223+
// Apply shouldApplyRule to all rules - this will handle global rules, rule policies,
224+
// and path matching in a consistent way
225+
const applicableRules = rules.filter((rule) =>
226+
shouldApplyRule(rule, allFilePaths, rulePolicies),
227+
);
225228

226-
// Combine global rules with matching rules, ensuring no duplicates
227-
return [...globalRules, ...matchingRules];
229+
return applicableRules;
228230
};
229231

230-
/**
231-
* Creates a system message string with applicable rules appended
232-
*
233-
* @param baseSystemMessage - The base system message to start with
234-
* @param userMessage - The user message to check for file paths
235-
* @param rules - The list of rules to filter
236-
* @param contextItems - Context items to check for file paths
237-
* @returns System message with applicable rules appended
238-
*/
239232
export const getSystemMessageWithRules = ({
240233
baseSystemMessage,
241234
userMessage,
242235
rules,
243236
contextItems,
237+
rulePolicies = {},
244238
}: {
245239
baseSystemMessage?: string;
246240
userMessage: UserChatMessage | ToolResultChatMessage | undefined;
247241
rules: RuleWithSource[];
248242
contextItems: ContextItemWithId[];
243+
rulePolicies?: RulePolicies;
249244
}) => {
250-
const applicableRules = getApplicableRules(userMessage, rules, contextItems);
245+
const applicableRules = getApplicableRules(
246+
userMessage,
247+
rules,
248+
contextItems,
249+
rulePolicies,
250+
);
251251
let systemMessage = baseSystemMessage ?? "";
252252

253253
for (const rule of applicableRules) {

core/llm/rules/getSystemMessageWithRules.vitest.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { describe, expect, it } from "vitest";
22
import { RuleWithSource } from "../..";
33
import { shouldApplyRule } from "./getSystemMessageWithRules";
4+
import { RulePolicies } from "./types";
45

56
describe("Rule colocation glob matching", () => {
67
// This test file demonstrates the expected behavior after our fix
@@ -132,3 +133,114 @@ describe("Rule colocation glob matching", () => {
132133
);
133134
});
134135
});
136+
137+
describe("Rule policies", () => {
138+
const componentRule: RuleWithSource = {
139+
name: "Components Rule",
140+
rule: "Use functional components with hooks",
141+
source: "rules-block",
142+
ruleFile: "src/components/rules.md",
143+
};
144+
145+
const testFiles = ["src/components/Button.tsx"];
146+
const nonMatchingFiles = ["src/utils/helpers.ts"];
147+
148+
it("should never apply rules with 'off' policy regardless of file paths", () => {
149+
const rulePolicies: RulePolicies = {
150+
"Components Rule": "off",
151+
};
152+
153+
// Should not apply even to matching files
154+
expect(shouldApplyRule(componentRule, testFiles, rulePolicies)).toBe(false);
155+
156+
// Rule with alwaysApply: true should still be overridden by 'off' policy
157+
const alwaysApplyRule: RuleWithSource = {
158+
name: "Always Apply Rule",
159+
rule: "This rule would normally always apply",
160+
alwaysApply: true,
161+
source: "rules-block",
162+
ruleFile: "src/components/rules.md",
163+
};
164+
165+
const offPolicies: RulePolicies = {
166+
"Always Apply Rule": "off",
167+
};
168+
169+
expect(shouldApplyRule(alwaysApplyRule, testFiles, offPolicies)).toBe(
170+
false,
171+
);
172+
});
173+
174+
it("should apply 'on' policy rules based on normal matching logic", () => {
175+
const rulePolicies: RulePolicies = {
176+
"Components Rule": "on",
177+
};
178+
179+
// Should apply to matching files
180+
expect(shouldApplyRule(componentRule, testFiles, rulePolicies)).toBe(true);
181+
182+
// Should not apply to non-matching files
183+
expect(shouldApplyRule(componentRule, nonMatchingFiles, rulePolicies)).toBe(
184+
false,
185+
);
186+
});
187+
188+
it("should use default behavior when no policy is specified", () => {
189+
// Empty policies object should use default behavior
190+
const rulePolicies: RulePolicies = {};
191+
192+
// Should apply to matching files
193+
expect(shouldApplyRule(componentRule, testFiles, rulePolicies)).toBe(true);
194+
195+
// Should not apply to non-matching files
196+
expect(shouldApplyRule(componentRule, nonMatchingFiles, rulePolicies)).toBe(
197+
false,
198+
);
199+
});
200+
201+
it("should handle policy interaction with global rules", () => {
202+
// Root level rule which would normally apply globally
203+
const rootRule: RuleWithSource = {
204+
name: "Root Rule",
205+
rule: "Follow project standards",
206+
source: "rules-block",
207+
ruleFile: ".continue/rules.md",
208+
};
209+
210+
// Off policy should override even global rules
211+
const offPolicies: RulePolicies = {
212+
"Root Rule": "off",
213+
};
214+
215+
expect(shouldApplyRule(rootRule, testFiles, offPolicies)).toBe(false);
216+
217+
// On policy should maintain global rule behavior
218+
const onPolicies: RulePolicies = {
219+
"Root Rule": "on",
220+
};
221+
222+
expect(shouldApplyRule(rootRule, testFiles, onPolicies)).toBe(true);
223+
});
224+
225+
it("should prioritize 'off' policy over alwaysApply and directory restrictions", () => {
226+
// Create rule with multiple matching criteria
227+
const complexRule: RuleWithSource = {
228+
name: "Complex Rule",
229+
rule: "This rule has complex matching logic",
230+
alwaysApply: true,
231+
globs: "**/*.ts",
232+
source: "rules-block",
233+
ruleFile: "src/utils/rules.md",
234+
};
235+
236+
// Off policy should win over everything
237+
const offPolicies: RulePolicies = {
238+
"Complex Rule": "off",
239+
};
240+
241+
// Even with matching files and alwaysApply: true, off policy wins
242+
expect(
243+
shouldApplyRule(complexRule, ["src/utils/test.ts"], offPolicies),
244+
).toBe(false);
245+
});
246+
});

core/llm/rules/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export type RulePolicy = "on" | "off";
2+
export type RulePolicies = { [ruleName: string]: RulePolicy };

0 commit comments

Comments
 (0)