rulix 0.1.1 → 0.2.0
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/README.md +2 -3
- package/dist/{chunk-I6MHHT6P.cjs → chunk-DUQAOCSV.cjs} +117 -157
- package/dist/chunk-DUQAOCSV.cjs.map +1 -0
- package/dist/{chunk-IX4ZOAKV.js → chunk-OBURSZGS.js} +117 -157
- package/dist/chunk-OBURSZGS.js.map +1 -0
- package/dist/cli/index.cjs +33 -21
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +13 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +9 -9
- package/dist/index.d.cts +8 -11
- package/dist/index.d.ts +8 -11
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-I6MHHT6P.cjs.map +0 -1
- package/dist/chunk-IX4ZOAKV.js.map +0 -1
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ Every AI coding tool has its own rules format:
|
|
|
34
34
|
| Tool | Format | Location |
|
|
35
35
|
|---|---|---|
|
|
36
36
|
| Cursor | `.mdc` with YAML frontmatter | `.cursor/rules/` |
|
|
37
|
-
| Claude Code |
|
|
37
|
+
| Claude Code | Markdown with optional frontmatter | `.claude/rules/` |
|
|
38
38
|
| AGENTS.md | Plain markdown | `AGENTS.md` |
|
|
39
39
|
| Windsurf | Markdown | `.windsurf/rules/` |
|
|
40
40
|
| Copilot | Markdown | `.github/copilot-instructions.md` |
|
|
@@ -43,7 +43,7 @@ If you use more than one tool, you're maintaining duplicate rules that drift apa
|
|
|
43
43
|
|
|
44
44
|
```
|
|
45
45
|
.rulix/rules/ .cursor/rules/*.mdc
|
|
46
|
-
├── typescript.md →
|
|
46
|
+
├── typescript.md → .claude/rules/*.md
|
|
47
47
|
├── testing.md → AGENTS.md
|
|
48
48
|
└── security.md → (more targets coming)
|
|
49
49
|
```
|
|
@@ -190,7 +190,6 @@ Rulix is configured via `.rulix/config.json`:
|
|
|
190
190
|
"options": {
|
|
191
191
|
"tokenEstimation": "heuristic",
|
|
192
192
|
"agentsMdHeader": true,
|
|
193
|
-
"claudeMdStrategy": "concatenate",
|
|
194
193
|
"syncOnSave": false
|
|
195
194
|
}
|
|
196
195
|
}
|
|
@@ -50,11 +50,8 @@ ${r.content}`);
|
|
|
50
50
|
${sections.join("\n\n")}`;
|
|
51
51
|
}
|
|
52
52
|
function buildAgentsMdContent(rules, includeHeader) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
);
|
|
56
|
-
if (exportable.length === 0) return "";
|
|
57
|
-
const groups = groupByCategory(exportable);
|
|
53
|
+
if (rules.length === 0) return "";
|
|
54
|
+
const groups = groupByCategory(rules);
|
|
58
55
|
const sections = [];
|
|
59
56
|
for (const category of CATEGORY_ORDER) {
|
|
60
57
|
const categoryRules = groups.get(category);
|
|
@@ -95,7 +92,6 @@ var agentsMdAdapter = {
|
|
|
95
92
|
getTokenBudget() {
|
|
96
93
|
return {
|
|
97
94
|
maxTokens: 32768,
|
|
98
|
-
maxInstructions: 0,
|
|
99
95
|
warningThreshold: 0.8,
|
|
100
96
|
source: "AGENTS.md convention"
|
|
101
97
|
};
|
|
@@ -139,9 +135,7 @@ function computeBudgetUsage(used, max) {
|
|
|
139
135
|
|
|
140
136
|
|
|
141
137
|
|
|
142
|
-
var CLAUDE_MD = "CLAUDE.md";
|
|
143
138
|
var RULES_DIR = ".claude/rules";
|
|
144
|
-
var CONTEXT_PREFIX = "Context: ";
|
|
145
139
|
async function pathExists2(filePath) {
|
|
146
140
|
try {
|
|
147
141
|
await _promises.access.call(void 0, filePath);
|
|
@@ -158,9 +152,6 @@ async function listMdFiles(dir) {
|
|
|
158
152
|
return [];
|
|
159
153
|
}
|
|
160
154
|
}
|
|
161
|
-
function toKebabCase(text) {
|
|
162
|
-
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
163
|
-
}
|
|
164
155
|
function stripQuotes(s) {
|
|
165
156
|
const t = s.trim();
|
|
166
157
|
if (t.startsWith('"') && t.endsWith('"') || t.startsWith("'") && t.endsWith("'")) {
|
|
@@ -181,7 +172,9 @@ function splitFrontmatter(raw) {
|
|
|
181
172
|
}
|
|
182
173
|
return null;
|
|
183
174
|
}
|
|
184
|
-
function
|
|
175
|
+
function parseClaudeRuleFields(yaml) {
|
|
176
|
+
let paths;
|
|
177
|
+
let description;
|
|
185
178
|
for (const line of yaml.split("\n")) {
|
|
186
179
|
const trimmed = line.trim();
|
|
187
180
|
const colonIdx = trimmed.indexOf(":");
|
|
@@ -191,33 +184,15 @@ function parsePaths(yaml) {
|
|
|
191
184
|
if (key === "paths" && value !== "") {
|
|
192
185
|
if (value.startsWith("[") && value.endsWith("]")) {
|
|
193
186
|
const inner = value.slice(1, -1).trim();
|
|
194
|
-
|
|
187
|
+
paths = inner === "" ? [] : inner.split(",").map(stripQuotes);
|
|
188
|
+
} else {
|
|
189
|
+
paths = [stripQuotes(value)];
|
|
195
190
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
return void 0;
|
|
200
|
-
}
|
|
201
|
-
function splitByH2(raw) {
|
|
202
|
-
const parts = raw.split(/^(?=## )/m);
|
|
203
|
-
let preamble = "";
|
|
204
|
-
const sections = [];
|
|
205
|
-
for (const part of parts) {
|
|
206
|
-
if (!part.startsWith("## ")) {
|
|
207
|
-
preamble = part.trim();
|
|
208
|
-
continue;
|
|
209
|
-
}
|
|
210
|
-
const newlineIdx = part.indexOf("\n");
|
|
211
|
-
if (newlineIdx === -1) {
|
|
212
|
-
sections.push({ heading: part.slice(3).trim(), content: "" });
|
|
213
|
-
} else {
|
|
214
|
-
sections.push({
|
|
215
|
-
heading: part.slice(3, newlineIdx).trim(),
|
|
216
|
-
content: part.slice(newlineIdx + 1).trim()
|
|
217
|
-
});
|
|
191
|
+
} else if (key === "description" && value !== "") {
|
|
192
|
+
description = stripQuotes(value);
|
|
218
193
|
}
|
|
219
194
|
}
|
|
220
|
-
return {
|
|
195
|
+
return { paths, description };
|
|
221
196
|
}
|
|
222
197
|
function createImportedRule(id, scope, description, content, filePath, globs) {
|
|
223
198
|
return {
|
|
@@ -236,70 +211,45 @@ function createImportedRule(id, scope, description, content, filePath, globs) {
|
|
|
236
211
|
}
|
|
237
212
|
};
|
|
238
213
|
}
|
|
239
|
-
function importClaudeMdContent(raw) {
|
|
240
|
-
const content = raw.replace(/\r\n/g, "\n").trim();
|
|
241
|
-
if (content === "") return [];
|
|
242
|
-
const { preamble, sections } = splitByH2(content);
|
|
243
|
-
if (sections.length === 0) {
|
|
244
|
-
if (!preamble) return [];
|
|
245
|
-
return [
|
|
246
|
-
createImportedRule(
|
|
247
|
-
"claude-md",
|
|
248
|
-
"always",
|
|
249
|
-
"CLAUDE.md contents",
|
|
250
|
-
preamble,
|
|
251
|
-
CLAUDE_MD
|
|
252
|
-
)
|
|
253
|
-
];
|
|
254
|
-
}
|
|
255
|
-
const rules = [];
|
|
256
|
-
if (preamble) {
|
|
257
|
-
rules.push(
|
|
258
|
-
createImportedRule(
|
|
259
|
-
"claude-md-preamble",
|
|
260
|
-
"always",
|
|
261
|
-
"CLAUDE.md preamble",
|
|
262
|
-
preamble,
|
|
263
|
-
CLAUDE_MD
|
|
264
|
-
)
|
|
265
|
-
);
|
|
266
|
-
}
|
|
267
|
-
for (const section of sections) {
|
|
268
|
-
const isContext = section.heading.startsWith(CONTEXT_PREFIX);
|
|
269
|
-
const description = isContext ? section.heading.slice(CONTEXT_PREFIX.length) : section.heading;
|
|
270
|
-
const scope = isContext ? "agent-selected" : "always";
|
|
271
|
-
const id = toKebabCase(description);
|
|
272
|
-
if (id && section.content) {
|
|
273
|
-
rules.push(
|
|
274
|
-
createImportedRule(id, scope, description, section.content, CLAUDE_MD)
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
return rules;
|
|
279
|
-
}
|
|
280
|
-
async function importClaudeMd(projectRoot) {
|
|
281
|
-
const filePath = _path.join.call(void 0, projectRoot, CLAUDE_MD);
|
|
282
|
-
if (!await pathExists2(filePath)) return [];
|
|
283
|
-
const raw = await _promises.readFile.call(void 0, filePath, "utf-8");
|
|
284
|
-
return importClaudeMdContent(raw);
|
|
285
|
-
}
|
|
286
214
|
function importClaudeRuleFile(raw, filePath, id) {
|
|
287
|
-
const
|
|
215
|
+
const fallbackDescription = id.replace(/-/g, " ");
|
|
288
216
|
const parts = splitFrontmatter(raw);
|
|
289
217
|
if (!parts) {
|
|
290
218
|
const content = raw.replace(/\r\n/g, "\n").trim();
|
|
291
|
-
return createImportedRule(
|
|
219
|
+
return createImportedRule(
|
|
220
|
+
id,
|
|
221
|
+
"always",
|
|
222
|
+
fallbackDescription,
|
|
223
|
+
content,
|
|
224
|
+
filePath
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
const fields = parseClaudeRuleFields(parts.yaml);
|
|
228
|
+
if (fields.paths && fields.paths.length > 0) {
|
|
229
|
+
return createImportedRule(
|
|
230
|
+
id,
|
|
231
|
+
"file-scoped",
|
|
232
|
+
_nullishCoalesce(fields.description, () => ( fallbackDescription)),
|
|
233
|
+
parts.content,
|
|
234
|
+
filePath,
|
|
235
|
+
fields.paths
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
if (fields.description) {
|
|
239
|
+
return createImportedRule(
|
|
240
|
+
id,
|
|
241
|
+
"agent-selected",
|
|
242
|
+
fields.description,
|
|
243
|
+
parts.content,
|
|
244
|
+
filePath
|
|
245
|
+
);
|
|
292
246
|
}
|
|
293
|
-
const paths = parsePaths(parts.yaml);
|
|
294
|
-
const scope = paths && paths.length > 0 ? "file-scoped" : "always";
|
|
295
|
-
const globs = scope === "file-scoped" ? paths : void 0;
|
|
296
247
|
return createImportedRule(
|
|
297
248
|
id,
|
|
298
|
-
|
|
299
|
-
|
|
249
|
+
"always",
|
|
250
|
+
fallbackDescription,
|
|
300
251
|
parts.content,
|
|
301
|
-
filePath
|
|
302
|
-
globs
|
|
252
|
+
filePath
|
|
303
253
|
);
|
|
304
254
|
}
|
|
305
255
|
async function importClaudeRuleFiles(projectRoot) {
|
|
@@ -313,55 +263,40 @@ async function importClaudeRuleFiles(projectRoot) {
|
|
|
313
263
|
}
|
|
314
264
|
return rules;
|
|
315
265
|
}
|
|
316
|
-
function buildClaudeMdContent(rules) {
|
|
317
|
-
const sorted = [...rules].sort((a, b) => a.priority - b.priority);
|
|
318
|
-
const sections = [];
|
|
319
|
-
for (const rule of sorted) {
|
|
320
|
-
const prefix = rule.scope === "agent-selected" ? CONTEXT_PREFIX : "";
|
|
321
|
-
sections.push(`## ${prefix}${rule.description}
|
|
322
|
-
|
|
323
|
-
${rule.content}`);
|
|
324
|
-
}
|
|
325
|
-
return `${sections.join("\n\n")}
|
|
326
|
-
`;
|
|
327
|
-
}
|
|
328
266
|
function buildClaudeRuleFile(rule) {
|
|
329
|
-
if (
|
|
330
|
-
|
|
267
|
+
if (rule.scope === "file-scoped" && rule.globs && rule.globs.length > 0) {
|
|
268
|
+
const lines = ["---"];
|
|
269
|
+
if (rule.globs.length === 1) {
|
|
270
|
+
const first = rule.globs[0];
|
|
271
|
+
if (first !== void 0) lines.push(`paths: "${first}"`);
|
|
272
|
+
} else {
|
|
273
|
+
const items = rule.globs.map((g) => `"${g}"`).join(", ");
|
|
274
|
+
lines.push(`paths: [${items}]`);
|
|
275
|
+
}
|
|
276
|
+
lines.push("---");
|
|
277
|
+
lines.push("");
|
|
278
|
+
lines.push(rule.content);
|
|
279
|
+
lines.push("");
|
|
280
|
+
return lines.join("\n");
|
|
281
|
+
}
|
|
282
|
+
if (rule.scope === "agent-selected") {
|
|
283
|
+
const lines = ["---"];
|
|
284
|
+
lines.push(`description: "${rule.description}"`);
|
|
285
|
+
lines.push("---");
|
|
286
|
+
lines.push("");
|
|
287
|
+
lines.push(rule.content);
|
|
288
|
+
lines.push("");
|
|
289
|
+
return lines.join("\n");
|
|
290
|
+
}
|
|
291
|
+
return `${rule.content}
|
|
331
292
|
`;
|
|
332
|
-
}
|
|
333
|
-
const lines = ["---"];
|
|
334
|
-
if (rule.globs.length === 1) {
|
|
335
|
-
const first = rule.globs[0];
|
|
336
|
-
if (first !== void 0) lines.push(`paths: "${first}"`);
|
|
337
|
-
} else {
|
|
338
|
-
const items = rule.globs.map((g) => `"${g}"`).join(", ");
|
|
339
|
-
lines.push(`paths: [${items}]`);
|
|
340
|
-
}
|
|
341
|
-
lines.push("---");
|
|
342
|
-
lines.push("");
|
|
343
|
-
lines.push(rule.content);
|
|
344
|
-
lines.push("");
|
|
345
|
-
return lines.join("\n");
|
|
346
|
-
}
|
|
347
|
-
async function exportClaudeMd(rules, projectRoot, dryRun) {
|
|
348
|
-
const mdRules = rules.filter(
|
|
349
|
-
(r) => r.scope === "always" || r.scope === "agent-selected"
|
|
350
|
-
);
|
|
351
|
-
if (mdRules.length === 0) return [];
|
|
352
|
-
if (!dryRun) {
|
|
353
|
-
const content = buildClaudeMdContent(mdRules);
|
|
354
|
-
await _promises.writeFile.call(void 0, _path.join.call(void 0, projectRoot, CLAUDE_MD), content, "utf-8");
|
|
355
|
-
}
|
|
356
|
-
return [CLAUDE_MD];
|
|
357
293
|
}
|
|
358
294
|
async function exportClaudeRules(rules, projectRoot, dryRun) {
|
|
359
|
-
|
|
360
|
-
if (fileScoped.length === 0) return [];
|
|
295
|
+
if (rules.length === 0) return [];
|
|
361
296
|
const dir = _path.join.call(void 0, projectRoot, RULES_DIR);
|
|
362
297
|
if (!dryRun) await _promises.mkdir.call(void 0, dir, { recursive: true });
|
|
363
298
|
const written = [];
|
|
364
|
-
for (const rule of
|
|
299
|
+
for (const rule of rules) {
|
|
365
300
|
const filePath = _path.join.call(void 0, RULES_DIR, `${rule.id}.md`);
|
|
366
301
|
if (!dryRun) {
|
|
367
302
|
await _promises.writeFile.call(void 0,
|
|
@@ -377,9 +312,7 @@ async function exportClaudeRules(rules, projectRoot, dryRun) {
|
|
|
377
312
|
async function deleteStaleRuleFiles(rules, projectRoot, dryRun) {
|
|
378
313
|
const dir = _path.join.call(void 0, projectRoot, RULES_DIR);
|
|
379
314
|
const existing = await listMdFiles(dir);
|
|
380
|
-
const exportedIds = new Set(
|
|
381
|
-
rules.filter((r) => r.scope === "file-scoped").map((r) => `${r.id}.md`)
|
|
382
|
-
);
|
|
315
|
+
const exportedIds = new Set(rules.map((r) => `${r.id}.md`));
|
|
383
316
|
const deleted = [];
|
|
384
317
|
for (const file of existing) {
|
|
385
318
|
if (!exportedIds.has(file)) {
|
|
@@ -394,35 +327,32 @@ var claudeCodeAdapter = {
|
|
|
394
327
|
name: "claude-code",
|
|
395
328
|
displayName: "Claude Code",
|
|
396
329
|
async detect(projectRoot) {
|
|
397
|
-
const claudeMd = _path.join.call(void 0, projectRoot,
|
|
330
|
+
const claudeMd = _path.join.call(void 0, projectRoot, "CLAUDE.md");
|
|
398
331
|
const claudeDir = _path.join.call(void 0, projectRoot, ".claude");
|
|
399
332
|
return await pathExists2(claudeMd) || await pathExists2(claudeDir);
|
|
400
333
|
},
|
|
401
334
|
async import(projectRoot) {
|
|
402
|
-
const mdRules = await importClaudeMd(projectRoot);
|
|
403
335
|
const ruleFiles = await importClaudeRuleFiles(projectRoot);
|
|
404
336
|
return {
|
|
405
|
-
rules:
|
|
337
|
+
rules: ruleFiles,
|
|
406
338
|
warnings: [],
|
|
407
|
-
source:
|
|
339
|
+
source: RULES_DIR
|
|
408
340
|
};
|
|
409
341
|
},
|
|
410
342
|
async export(rules, projectRoot, options) {
|
|
411
343
|
const dryRun = _optionalChain([options, 'optionalAccess', _8 => _8.dryRun]) === true;
|
|
412
344
|
const strategy = _nullishCoalesce(_optionalChain([options, 'optionalAccess', _9 => _9.strategy]), () => ( "overwrite"));
|
|
413
|
-
const
|
|
414
|
-
const rulesWritten = await exportClaudeRules(rules, projectRoot, dryRun);
|
|
345
|
+
const filesWritten = await exportClaudeRules(rules, projectRoot, dryRun);
|
|
415
346
|
const filesDeleted = strategy === "overwrite" ? await deleteStaleRuleFiles(rules, projectRoot, dryRun) : [];
|
|
416
347
|
return {
|
|
417
|
-
filesWritten
|
|
348
|
+
filesWritten,
|
|
418
349
|
filesDeleted,
|
|
419
350
|
warnings: []
|
|
420
351
|
};
|
|
421
352
|
},
|
|
422
353
|
getTokenBudget() {
|
|
423
354
|
return {
|
|
424
|
-
maxTokens:
|
|
425
|
-
maxInstructions: 150,
|
|
355
|
+
maxTokens: 4e3,
|
|
426
356
|
warningThreshold: 0.8,
|
|
427
357
|
source: "Claude Code documentation"
|
|
428
358
|
};
|
|
@@ -676,8 +606,7 @@ var cursorAdapter = {
|
|
|
676
606
|
},
|
|
677
607
|
getTokenBudget() {
|
|
678
608
|
return {
|
|
679
|
-
maxTokens:
|
|
680
|
-
maxInstructions: 500,
|
|
609
|
+
maxTokens: 5e3,
|
|
681
610
|
warningThreshold: 0.8,
|
|
682
611
|
source: "Cursor documentation"
|
|
683
612
|
};
|
|
@@ -731,7 +660,6 @@ var RULES_DIR3 = "rules";
|
|
|
731
660
|
var DEFAULT_OPTIONS = {
|
|
732
661
|
tokenEstimation: "heuristic",
|
|
733
662
|
agentsMdHeader: true,
|
|
734
|
-
claudeMdStrategy: "concatenate",
|
|
735
663
|
syncOnSave: false
|
|
736
664
|
};
|
|
737
665
|
var DEFAULT_CONFIG = {
|
|
@@ -766,11 +694,6 @@ function resolveOptions(raw) {
|
|
|
766
694
|
'"options.tokenEstimation" must be "heuristic" or "tiktoken"'
|
|
767
695
|
);
|
|
768
696
|
}
|
|
769
|
-
if (raw.claudeMdStrategy !== void 0 && raw.claudeMdStrategy !== "concatenate" && raw.claudeMdStrategy !== "reference") {
|
|
770
|
-
return configError(
|
|
771
|
-
'"options.claudeMdStrategy" must be "concatenate" or "reference"'
|
|
772
|
-
);
|
|
773
|
-
}
|
|
774
697
|
if (raw.agentsMdHeader !== void 0 && typeof raw.agentsMdHeader !== "boolean") {
|
|
775
698
|
return configError('"options.agentsMdHeader" must be a boolean');
|
|
776
699
|
}
|
|
@@ -782,7 +705,6 @@ function resolveOptions(raw) {
|
|
|
782
705
|
value: {
|
|
783
706
|
tokenEstimation: _nullishCoalesce(raw.tokenEstimation, () => ( DEFAULT_OPTIONS.tokenEstimation)),
|
|
784
707
|
agentsMdHeader: _nullishCoalesce(raw.agentsMdHeader, () => ( DEFAULT_OPTIONS.agentsMdHeader)),
|
|
785
|
-
claudeMdStrategy: _nullishCoalesce(raw.claudeMdStrategy, () => ( DEFAULT_OPTIONS.claudeMdStrategy)),
|
|
786
708
|
syncOnSave: _nullishCoalesce(raw.syncOnSave, () => ( DEFAULT_OPTIONS.syncOnSave))
|
|
787
709
|
}
|
|
788
710
|
};
|
|
@@ -1024,6 +946,8 @@ async function writeRule(projectRoot, rule) {
|
|
|
1024
946
|
// src/core/validator.ts
|
|
1025
947
|
var MIN_CONTENT_LENGTH = 20;
|
|
1026
948
|
var MAX_CONTENT_LINES = 50;
|
|
949
|
+
var CURSOR_MAX_LINES = 50;
|
|
950
|
+
var CLAUDE_CODE_TOKEN_BUDGET = 4e3;
|
|
1027
951
|
var VAGUE_PATTERNS = [
|
|
1028
952
|
/handle .+ properly/i,
|
|
1029
953
|
/do .+ correctly/i,
|
|
@@ -1182,6 +1106,37 @@ function checkLongContent(rule) {
|
|
|
1182
1106
|
}
|
|
1183
1107
|
return [];
|
|
1184
1108
|
}
|
|
1109
|
+
function checkCursorLineLimit(rule) {
|
|
1110
|
+
const lineCount = rule.content.split("\n").length;
|
|
1111
|
+
if (lineCount > CURSOR_MAX_LINES) {
|
|
1112
|
+
return [
|
|
1113
|
+
createIssue(
|
|
1114
|
+
"V011",
|
|
1115
|
+
"warning",
|
|
1116
|
+
`Rule "${rule.id}" has ${lineCount} lines \u2014 Cursor recommends <${CURSOR_MAX_LINES} lines per .mdc file`,
|
|
1117
|
+
rule.id,
|
|
1118
|
+
"Split into smaller, focused rules"
|
|
1119
|
+
)
|
|
1120
|
+
];
|
|
1121
|
+
}
|
|
1122
|
+
return [];
|
|
1123
|
+
}
|
|
1124
|
+
function checkTotalTokenBudget(rules) {
|
|
1125
|
+
let total = 0;
|
|
1126
|
+
for (const rule of rules) {
|
|
1127
|
+
total += rule.estimatedTokens;
|
|
1128
|
+
}
|
|
1129
|
+
if (total > CLAUDE_CODE_TOKEN_BUDGET) {
|
|
1130
|
+
return [
|
|
1131
|
+
createIssue(
|
|
1132
|
+
"V012",
|
|
1133
|
+
"info",
|
|
1134
|
+
`Total rule tokens (${total}) exceed ${CLAUDE_CODE_TOKEN_BUDGET} \u2014 consider reducing to keep Claude responsive`
|
|
1135
|
+
)
|
|
1136
|
+
];
|
|
1137
|
+
}
|
|
1138
|
+
return [];
|
|
1139
|
+
}
|
|
1185
1140
|
function buildResult(issues) {
|
|
1186
1141
|
return {
|
|
1187
1142
|
passed: issues.every((i) => i.severity !== "error"),
|
|
@@ -1190,8 +1145,9 @@ function buildResult(issues) {
|
|
|
1190
1145
|
info: issues.filter((i) => i.severity === "info")
|
|
1191
1146
|
};
|
|
1192
1147
|
}
|
|
1193
|
-
function validateRules(rules) {
|
|
1148
|
+
function validateRules(rules, targets) {
|
|
1194
1149
|
const issues = [];
|
|
1150
|
+
const hasCursor = _optionalChain([targets, 'optionalAccess', _24 => _24.includes, 'call', _25 => _25("cursor")]) === true;
|
|
1195
1151
|
issues.push(...checkDuplicateIds(rules));
|
|
1196
1152
|
for (const rule of rules) {
|
|
1197
1153
|
issues.push(...checkRequiredFields(rule));
|
|
@@ -1201,7 +1157,11 @@ function validateRules(rules) {
|
|
|
1201
1157
|
issues.push(...checkDefaultCategory(rule));
|
|
1202
1158
|
issues.push(...checkGlobSyntax(rule));
|
|
1203
1159
|
issues.push(...checkLongContent(rule));
|
|
1160
|
+
if (hasCursor) {
|
|
1161
|
+
issues.push(...checkCursorLineLimit(rule));
|
|
1162
|
+
}
|
|
1204
1163
|
}
|
|
1164
|
+
issues.push(...checkTotalTokenBudget(rules));
|
|
1205
1165
|
return buildResult(issues);
|
|
1206
1166
|
}
|
|
1207
1167
|
|
|
@@ -1232,4 +1192,4 @@ function validateRules(rules) {
|
|
|
1232
1192
|
|
|
1233
1193
|
|
|
1234
1194
|
exports.agentsMdAdapter = agentsMdAdapter; exports.estimateTokens = estimateTokens; exports.estimateRuleTokens = estimateRuleTokens; exports.sumTokens = sumTokens; exports.computeBudgetUsage = computeBudgetUsage; exports.claudeCodeAdapter = claudeCodeAdapter; exports.cursorAdapter = cursorAdapter; exports.getAdapters = getAdapters; exports.getAdapter = getAdapter; exports.getAdapterNames = getAdapterNames; exports.detectAdapters = detectAdapters; exports.RulixError = RulixError; exports.RULIX_DIR = RULIX_DIR; exports.CONFIG_FILENAME = CONFIG_FILENAME; exports.RULES_DIR = RULES_DIR3; exports.configPath = configPath; exports.rulesPath = rulesPath; exports.createDefaultConfig = createDefaultConfig; exports.resolveConfig = resolveConfig; exports.loadConfig = loadConfig; exports.parseRule = parseRule; exports.serializeRule = serializeRule; exports.loadRules = loadRules; exports.writeRule = writeRule; exports.validateRules = validateRules;
|
|
1235
|
-
//# sourceMappingURL=chunk-
|
|
1195
|
+
//# sourceMappingURL=chunk-DUQAOCSV.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/rulix/rulix/dist/chunk-DUQAOCSV.cjs","../src/adapters/agents-md.ts","../src/core/tokenizer.ts","../src/adapters/claude-code.ts","../src/adapters/cursor.ts","../src/adapters/registry.ts","../src/core/ir.ts","../src/core/config.ts","../src/core/parser.ts","../src/core/validator.ts"],"names":["access","writeFile","join","mkdir","rm","RULES_DIR","stripQuotes","parseInlineArray","splitFrontmatter"],"mappings":"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IS;AACJ,EAAA;AACA,EAAA;AACA,EAAA;AACG,EAAA;AACR;AAIS;AAKF,EAAA;AACA,EAAA;AACC,EAAA;AACA,IAAA;AACL,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACC,QAAA;AACA,QAAA;AACA,QAAA;AACD,MAAA;AACD,IAAA;AACA,IAAA;AACC,MAAA;AACA,MAAA;AACD,IAAA;AACD,EAAA;AACD;AAES;AAKF,EAAA;AACA,EAAA;AACA,EAAA;AAEC,EAAA;AACN,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACI,IAAA;AACI,IAAA;AACP,MAAA;AACA,MAAA;AACA,MAAA;AACD,IAAA;AACD,EAAA;AACD;AAEA;AAGO,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AAEN,EAAA;AACO,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AAED,IAAA;AACE,MAAA;AACA,MAAA;AACN,MAAA;AACM,IAAA;AACA,MAAA;AACP,IAAA;AACD,EAAA;AAES,EAAA;AACV;AAEA;AAGO,EAAA;AACA,EAAA;AAEA,EAAA;AACA,EAAA;AACF,EAAA;AAEG,EAAA;AACC,IAAA;AACN,MAAA;AACK,QAAA;AACJ,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACC,UAAA;AACA,UAAA;AACA,UAAA;AACD,QAAA;AACD,MAAA;AACD,IAAA;AACA,IAAA;AACC,MAAA;AACC,QAAA;AACA,QAAA;AACD,MAAA;AACD,IAAA;AACD,EAAA;AACD;AAIS;AACE,EAAA;AACH,IAAA;AACF,IAAA;AACL,EAAA;AACM,EAAA;AACC,EAAA;AACR;AAES;AACF,EAAA;AACA,EAAA;AACG,EAAA;AACF,IAAA;AACP,EAAA;AACM,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACC,EAAA;AACR;AAEA;AAKO,EAAA;AACA,EAAA;AACA,EAAA;AAEN,EAAA;AACM,IA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,EAAA;AACD;AAIS;AACF,EAAA;AACA,EAAA;AAEN,EAAA;AACO,IAAA;AACD,IAAA;AACD,IAAA;AACH,MAAA;AACC,QAAA;AACC,UAAA;AACA,UAAA;AACA,UAAA;AACA,UAAA;AACA,UAAA;AACD,QAAA;AACD,MAAA;AACD,IAAA;AACD,EAAA;AAEO,EAAA;AACR;AAIS;AACF,EAAA;AACG,EAAA;AACD,IAAA;AACR,EAAA;AACS,EAAA;AACD,IAAA;AACN,MAAA;AACC,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACD,MAAA;AACD,IAAA;AACD,EAAA;AACO,EAAA;AACR;AAES;AACC,EAAA;AACC,EAAA;AACF,IAAA;AACN,MAAA;AACC,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACD,MAAA;AACD,IAAA;AACD,EAAA;AACQ,EAAA;AACT;AAES;AACC,EAAA;AACD,IAAA;AACN,MAAA;AACC,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACD,MAAA;AACD,IAAA;AACD,EAAA;AACQ,EAAA;AACT;AAES;AACR,EAAA;AACK,IAAA;AACH,MAAA;AACC,QAAA;AACC,UAAA;AACA,UAAA;AACA,UAAA;AACA,UAAA;AACA,UAAA;AACD,QAAA;AACD,MAAA;AACD,IAAA;AACD,EAAA;AACQ,EAAA;AACT;AAES;AACC,EAAA;AACD,IAAA;AACN,MAAA;AACC,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACD,MAAA;AACD,IAAA;AACD,EAAA;AACQ,EAAA;AACT;AAES;AACJ,EAAA;AACA,EAAA;AACA,EAAA;AACJ,EAAA;AACK,IAAA;AAAY,IAAA;AACK,IAAA;AACA,IAAA;AAEjB,IAAA;AACL,EAAA;AACO,EAAA;AACR;AAES;AACE,EAAA;AACJ,EAAA;AACN,EAAA;AACM,IAAA;AACJ,MAAA;AACC,QAAA;AACC,UAAA;AACA,UAAA;AACA,UAAA;AACA,UAAA;AACD,QAAA;AACD,MAAA;AACD,IAAA;AACD,EAAA;AACO,EAAA;AACR;AAES;AACF,EAAA;AACF,EAAA;AACI,IAAA;AACN,MAAA;AACC,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACD,MAAA;AACD,IAAA;AACD,EAAA;AACQ,EAAA;AACT;AAIS;AACF,EAAA;AACF,EAAA;AACI,IAAA;AACN,MAAA;AACC,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACD,MAAA;AACD,IAAA;AACD,EAAA;AACQ,EAAA;AACT;AAES;AACJ,EAAA;AACJ,EAAA;AACC,IAAA;AACD,EAAA;AACI,EAAA;AACI,IAAA;AACN,MAAA;AACC,QAAA;AACA,QAAA;AACA,QAAA;AACD,MAAA;AACD,IAAA;AACD,EAAA;AACQ,EAAA;AACT;AAIS;AACD,EAAA;AACE,IAAA;AACA,IAAA;AACR,IAAA;AACM,IAAA;AACP,EAAA;AACD;AAKgB;AAIT,EAAA;AACA,EAAA;AAEC,EAAA;AAEP,EAAA;AACQ,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACH,IAAA;AACH,MAAA;AACD,IAAA;AACD,EAAA;AAEO,EAAA;AAEA,EAAA;AACR;ATy3BY;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/rulix/rulix/dist/chunk-DUQAOCSV.cjs","sourcesContent":[null,"/**\n * AGENTS.md adapter: export-only.\n *\n * Import is not supported in v0.1 because AGENTS.md is too\n * unstructured to parse reliably into discrete rules.\n */\n\nimport { access, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type {\n\tExportOptions,\n\tExportResult,\n\tImportResult,\n\tRule,\n\tRuleCategory,\n\tRulixAdapter,\n\tTokenBudget,\n} from \"../core/ir.js\";\n\nconst AGENTS_MD = \"AGENTS.md\";\nconst GENERATED_HEADER =\n\t\"<!-- Generated by Rulix. Do not edit directly. -->\\n\\n\";\n\nconst CATEGORY_LABELS: Record<RuleCategory, string> = {\n\tstyle: \"Style\",\n\tsecurity: \"Security\",\n\ttesting: \"Testing\",\n\tarchitecture: \"Architecture\",\n\tworkflow: \"Workflow\",\n\tgeneral: \"General\",\n};\n\nconst CATEGORY_ORDER: RuleCategory[] = [\n\t\"architecture\",\n\t\"style\",\n\t\"security\",\n\t\"testing\",\n\t\"workflow\",\n\t\"general\",\n];\n\n// ─── Filesystem Helpers ──────────────────────────────────────────\n\nasync function pathExists(filePath: string): Promise<boolean> {\n\ttry {\n\t\tawait access(filePath);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n// ─── Export Helpers ──────────────────────────────────────────────\n\nfunction groupByCategory(rules: Rule[]): Map<RuleCategory, Rule[]> {\n\tconst groups = new Map<RuleCategory, Rule[]>();\n\n\tfor (const rule of rules) {\n\t\tconst existing = groups.get(rule.category);\n\t\tif (existing) {\n\t\t\texisting.push(rule);\n\t\t} else {\n\t\t\tgroups.set(rule.category, [rule]);\n\t\t}\n\t}\n\n\treturn groups;\n}\n\nfunction buildCategorySection(category: RuleCategory, rules: Rule[]): string {\n\tconst sorted = [...rules].sort((a, b) => a.priority - b.priority);\n\tconst label = CATEGORY_LABELS[category];\n\tconst sections = sorted.map((r) => `### ${r.description}\\n\\n${r.content}`);\n\treturn `## ${label}\\n\\n${sections.join(\"\\n\\n\")}`;\n}\n\nfunction buildAgentsMdContent(rules: Rule[], includeHeader: boolean): string {\n\tif (rules.length === 0) return \"\";\n\n\tconst groups = groupByCategory(rules);\n\tconst sections: string[] = [];\n\n\tfor (const category of CATEGORY_ORDER) {\n\t\tconst categoryRules = groups.get(category);\n\t\tif (categoryRules && categoryRules.length > 0) {\n\t\t\tsections.push(buildCategorySection(category, categoryRules));\n\t\t}\n\t}\n\n\tconst header = includeHeader ? GENERATED_HEADER : \"\";\n\treturn `${header}# AGENTS.md\\n\\n${sections.join(\"\\n\\n\")}\\n`;\n}\n\n// ─── Adapter ─────────────────────────────────────────────────────\n\nexport const agentsMdAdapter: RulixAdapter = {\n\tname: \"agents-md\",\n\tdisplayName: \"AGENTS.md\",\n\n\tasync detect(projectRoot: string): Promise<boolean> {\n\t\treturn pathExists(join(projectRoot, AGENTS_MD));\n\t},\n\n\tasync import(_projectRoot: string): Promise<ImportResult> {\n\t\treturn { rules: [], warnings: [], source: AGENTS_MD };\n\t},\n\n\tasync export(\n\t\trules: Rule[],\n\t\tprojectRoot: string,\n\t\toptions?: ExportOptions,\n\t): Promise<ExportResult> {\n\t\tconst dryRun = options?.dryRun === true;\n\t\tconst content = buildAgentsMdContent(rules, true);\n\n\t\tif (content === \"\") {\n\t\t\treturn { filesWritten: [], filesDeleted: [], warnings: [] };\n\t\t}\n\n\t\tif (!dryRun) {\n\t\t\tawait writeFile(join(projectRoot, AGENTS_MD), content, \"utf-8\");\n\t\t}\n\n\t\treturn {\n\t\t\tfilesWritten: [AGENTS_MD],\n\t\t\tfilesDeleted: [],\n\t\t\twarnings: [],\n\t\t};\n\t},\n\n\tgetTokenBudget(): TokenBudget {\n\t\treturn {\n\t\t\tmaxTokens: 32_768,\n\t\t\twarningThreshold: 0.8,\n\t\t\tsource: \"AGENTS.md convention\",\n\t\t};\n\t},\n};\n","/**\n * Lightweight token estimation without external dependencies.\n *\n * Uses a character-based heuristic (~4 chars per token) that's accurate\n * enough for budget warnings. Exact counting via tiktoken is planned for v0.2.\n */\n\nconst CHARS_PER_TOKEN = 4;\n\n/** Estimates token count using the ~4 chars/token heuristic for English/code. */\nexport function estimateTokens(text: string): number {\n\tif (text.length === 0) return 0;\n\treturn Math.ceil(text.length / CHARS_PER_TOKEN);\n}\n\n/**\n * Builds the full text that a rule contributes to a tool's context window:\n * frontmatter metadata + markdown content.\n */\nexport function estimateRuleTokens(\n\tcontent: string,\n\tdescription: string,\n): number {\n\treturn estimateTokens(`${description}\\n${content}`);\n}\n\n/** Sums estimated tokens across multiple content strings. */\nexport function sumTokens(tokenCounts: number[]): number {\n\tlet total = 0;\n\tfor (const count of tokenCounts) {\n\t\ttotal += count;\n\t}\n\treturn total;\n}\n\nexport interface TokenBudgetUsage {\n\treadonly used: number;\n\treadonly max: number;\n\treadonly percentage: number;\n\treadonly exceeded: boolean;\n}\n\n/** Computes usage against a budget, returning percentage and exceeded flag. */\nexport function computeBudgetUsage(\n\tused: number,\n\tmax: number,\n): TokenBudgetUsage {\n\tconst percentage = max === 0 ? 100 : (used / max) * 100;\n\treturn {\n\t\tused,\n\t\tmax,\n\t\tpercentage,\n\t\texceeded: used > max,\n\t};\n}\n","/**\n * Claude Code adapter: imports from `.claude/rules/*.md`,\n * exports IR rules to `.claude/rules/` individual files.\n *\n * CLAUDE.md is never generated, modified, or overwritten by Rulix.\n * Never touches `.claude/skills/`, `.claude/commands/`,\n * `.claude/agents/`, or `.claude/settings.json`.\n */\n\nimport {\n\taccess,\n\tmkdir,\n\treaddir,\n\treadFile,\n\trm,\n\twriteFile,\n} from \"node:fs/promises\";\nimport { basename, join } from \"node:path\";\nimport type {\n\tExportOptions,\n\tExportResult,\n\tImportResult,\n\tRule,\n\tRuleScope,\n\tRulixAdapter,\n\tTokenBudget,\n} from \"../core/ir.js\";\nimport { estimateRuleTokens } from \"../core/tokenizer.js\";\n\nconst RULES_DIR = \".claude/rules\";\n\n// ─── Filesystem Helpers ──────────────────────────────────────────\n\nasync function pathExists(filePath: string): Promise<boolean> {\n\ttry {\n\t\tawait access(filePath);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nasync function listMdFiles(dir: string): Promise<string[]> {\n\ttry {\n\t\tconst entries = await readdir(dir);\n\t\treturn entries.filter((f) => f.endsWith(\".md\")).sort();\n\t} catch {\n\t\treturn [];\n\t}\n}\n\n// ─── Text Helpers ────────────────────────────────────────────────\n\nfunction stripQuotes(s: string): string {\n\tconst t = s.trim();\n\tif (\n\t\t(t.startsWith('\"') && t.endsWith('\"')) ||\n\t\t(t.startsWith(\"'\") && t.endsWith(\"'\"))\n\t) {\n\t\treturn t.slice(1, -1);\n\t}\n\treturn t;\n}\n\n// ─── Frontmatter Parsing ─────────────────────────────────────────\n\ninterface FmParts {\n\treadonly yaml: string;\n\treadonly content: string;\n}\n\nfunction splitFrontmatter(raw: string): FmParts | null {\n\tconst lines = raw.replace(/\\r\\n/g, \"\\n\").split(\"\\n\");\n\tif (lines[0]?.trim() !== \"---\") return null;\n\n\tfor (let i = 1; i < lines.length; i++) {\n\t\tif (lines[i]?.trim() === \"---\") {\n\t\t\treturn {\n\t\t\t\tyaml: lines.slice(1, i).join(\"\\n\"),\n\t\t\t\tcontent: lines\n\t\t\t\t\t.slice(i + 1)\n\t\t\t\t\t.join(\"\\n\")\n\t\t\t\t\t.trim(),\n\t\t\t};\n\t\t}\n\t}\n\treturn null;\n}\n\ninterface ClaudeRuleFields {\n\treadonly paths: string[] | undefined;\n\treadonly description: string | undefined;\n}\n\nfunction parseClaudeRuleFields(yaml: string): ClaudeRuleFields {\n\tlet paths: string[] | undefined;\n\tlet description: string | undefined;\n\n\tfor (const line of yaml.split(\"\\n\")) {\n\t\tconst trimmed = line.trim();\n\t\tconst colonIdx = trimmed.indexOf(\":\");\n\t\tif (colonIdx === -1) continue;\n\n\t\tconst key = trimmed.slice(0, colonIdx).trim();\n\t\tconst value = trimmed.slice(colonIdx + 1).trim();\n\n\t\tif (key === \"paths\" && value !== \"\") {\n\t\t\tif (value.startsWith(\"[\") && value.endsWith(\"]\")) {\n\t\t\t\tconst inner = value.slice(1, -1).trim();\n\t\t\t\tpaths = inner === \"\" ? [] : inner.split(\",\").map(stripQuotes);\n\t\t\t} else {\n\t\t\t\tpaths = [stripQuotes(value)];\n\t\t\t}\n\t\t} else if (key === \"description\" && value !== \"\") {\n\t\t\tdescription = stripQuotes(value);\n\t\t}\n\t}\n\n\treturn { paths, description };\n}\n\n// ─── Import Helpers ──────────────────────────────────────────────\n\nfunction createImportedRule(\n\tid: string,\n\tscope: RuleScope,\n\tdescription: string,\n\tcontent: string,\n\tfilePath: string,\n\tglobs?: string[],\n): Rule {\n\treturn {\n\t\tid,\n\t\tscope,\n\t\tdescription,\n\t\tcontent,\n\t\tcategory: \"general\",\n\t\tpriority: 3,\n\t\testimatedTokens: estimateRuleTokens(content, description),\n\t\t...(globs ? { globs } : {}),\n\t\tsource: {\n\t\t\tadapter: \"claude-code\",\n\t\t\tfilePath,\n\t\t\timportedAt: new Date().toISOString(),\n\t\t},\n\t};\n}\n\nfunction importClaudeRuleFile(raw: string, filePath: string, id: string): Rule {\n\tconst fallbackDescription = id.replace(/-/g, \" \");\n\tconst parts = splitFrontmatter(raw);\n\n\tif (!parts) {\n\t\tconst content = raw.replace(/\\r\\n/g, \"\\n\").trim();\n\t\treturn createImportedRule(\n\t\t\tid,\n\t\t\t\"always\",\n\t\t\tfallbackDescription,\n\t\t\tcontent,\n\t\t\tfilePath,\n\t\t);\n\t}\n\n\tconst fields = parseClaudeRuleFields(parts.yaml);\n\n\tif (fields.paths && fields.paths.length > 0) {\n\t\treturn createImportedRule(\n\t\t\tid,\n\t\t\t\"file-scoped\",\n\t\t\tfields.description ?? fallbackDescription,\n\t\t\tparts.content,\n\t\t\tfilePath,\n\t\t\tfields.paths,\n\t\t);\n\t}\n\n\tif (fields.description) {\n\t\treturn createImportedRule(\n\t\t\tid,\n\t\t\t\"agent-selected\",\n\t\t\tfields.description,\n\t\t\tparts.content,\n\t\t\tfilePath,\n\t\t);\n\t}\n\n\treturn createImportedRule(\n\t\tid,\n\t\t\"always\",\n\t\tfallbackDescription,\n\t\tparts.content,\n\t\tfilePath,\n\t);\n}\n\nasync function importClaudeRuleFiles(projectRoot: string): Promise<Rule[]> {\n\tconst dir = join(projectRoot, RULES_DIR);\n\tconst files = await listMdFiles(dir);\n\tconst rules: Rule[] = [];\n\n\tfor (const file of files) {\n\t\tconst filePath = join(RULES_DIR, file);\n\t\tconst raw = await readFile(join(projectRoot, filePath), \"utf-8\");\n\t\trules.push(importClaudeRuleFile(raw, filePath, basename(file, \".md\")));\n\t}\n\n\treturn rules;\n}\n\n// ─── Export Helpers ──────────────────────────────────────────────\n\nfunction buildClaudeRuleFile(rule: Rule): string {\n\tif (rule.scope === \"file-scoped\" && rule.globs && rule.globs.length > 0) {\n\t\tconst lines: string[] = [\"---\"];\n\t\tif (rule.globs.length === 1) {\n\t\t\tconst first = rule.globs[0];\n\t\t\tif (first !== undefined) lines.push(`paths: \"${first}\"`);\n\t\t} else {\n\t\t\tconst items = rule.globs.map((g) => `\"${g}\"`).join(\", \");\n\t\t\tlines.push(`paths: [${items}]`);\n\t\t}\n\t\tlines.push(\"---\");\n\t\tlines.push(\"\");\n\t\tlines.push(rule.content);\n\t\tlines.push(\"\");\n\t\treturn lines.join(\"\\n\");\n\t}\n\n\tif (rule.scope === \"agent-selected\") {\n\t\tconst lines: string[] = [\"---\"];\n\t\tlines.push(`description: \"${rule.description}\"`);\n\t\tlines.push(\"---\");\n\t\tlines.push(\"\");\n\t\tlines.push(rule.content);\n\t\tlines.push(\"\");\n\t\treturn lines.join(\"\\n\");\n\t}\n\n\treturn `${rule.content}\\n`;\n}\n\nasync function exportClaudeRules(\n\trules: Rule[],\n\tprojectRoot: string,\n\tdryRun: boolean,\n): Promise<string[]> {\n\tif (rules.length === 0) return [];\n\n\tconst dir = join(projectRoot, RULES_DIR);\n\tif (!dryRun) await mkdir(dir, { recursive: true });\n\n\tconst written: string[] = [];\n\tfor (const rule of rules) {\n\t\tconst filePath = join(RULES_DIR, `${rule.id}.md`);\n\t\tif (!dryRun) {\n\t\t\tawait writeFile(\n\t\t\t\tjoin(projectRoot, filePath),\n\t\t\t\tbuildClaudeRuleFile(rule),\n\t\t\t\t\"utf-8\",\n\t\t\t);\n\t\t}\n\t\twritten.push(filePath);\n\t}\n\treturn written;\n}\n\nasync function deleteStaleRuleFiles(\n\trules: Rule[],\n\tprojectRoot: string,\n\tdryRun: boolean,\n): Promise<string[]> {\n\tconst dir = join(projectRoot, RULES_DIR);\n\tconst existing = await listMdFiles(dir);\n\tconst exportedIds = new Set(rules.map((r) => `${r.id}.md`));\n\tconst deleted: string[] = [];\n\n\tfor (const file of existing) {\n\t\tif (!exportedIds.has(file)) {\n\t\t\tconst filePath = join(RULES_DIR, file);\n\t\t\tif (!dryRun) await rm(join(projectRoot, filePath));\n\t\t\tdeleted.push(filePath);\n\t\t}\n\t}\n\treturn deleted;\n}\n\n// ─── Adapter ─────────────────────────────────────────────────────\n\nexport const claudeCodeAdapter: RulixAdapter = {\n\tname: \"claude-code\",\n\tdisplayName: \"Claude Code\",\n\n\tasync detect(projectRoot: string): Promise<boolean> {\n\t\tconst claudeMd = join(projectRoot, \"CLAUDE.md\");\n\t\tconst claudeDir = join(projectRoot, \".claude\");\n\t\treturn (await pathExists(claudeMd)) || (await pathExists(claudeDir));\n\t},\n\n\tasync import(projectRoot: string): Promise<ImportResult> {\n\t\tconst ruleFiles = await importClaudeRuleFiles(projectRoot);\n\n\t\treturn {\n\t\t\trules: ruleFiles,\n\t\t\twarnings: [],\n\t\t\tsource: RULES_DIR,\n\t\t};\n\t},\n\n\tasync export(\n\t\trules: Rule[],\n\t\tprojectRoot: string,\n\t\toptions?: ExportOptions,\n\t): Promise<ExportResult> {\n\t\tconst dryRun = options?.dryRun === true;\n\t\tconst strategy = options?.strategy ?? \"overwrite\";\n\n\t\tconst filesWritten = await exportClaudeRules(rules, projectRoot, dryRun);\n\t\tconst filesDeleted =\n\t\t\tstrategy === \"overwrite\"\n\t\t\t\t? await deleteStaleRuleFiles(rules, projectRoot, dryRun)\n\t\t\t\t: [];\n\n\t\treturn {\n\t\t\tfilesWritten,\n\t\t\tfilesDeleted,\n\t\t\twarnings: [],\n\t\t};\n\t},\n\n\tgetTokenBudget(): TokenBudget {\n\t\treturn {\n\t\t\tmaxTokens: 4_000,\n\t\t\twarningThreshold: 0.8,\n\t\t\tsource: \"Claude Code documentation\",\n\t\t};\n\t},\n};\n","/**\n * Cursor adapter: imports from `.cursor/rules/*.mdc` and `.cursorrules`,\n * exports IR rules to `.cursor/rules/*.mdc`.\n */\n\nimport {\n\taccess,\n\tmkdir,\n\treaddir,\n\treadFile,\n\trm,\n\twriteFile,\n} from \"node:fs/promises\";\nimport { basename, join } from \"node:path\";\nimport type {\n\tExportOptions,\n\tExportResult,\n\tExportWarning,\n\tImportResult,\n\tImportWarning,\n\tRule,\n\tRuleScope,\n\tRulixAdapter,\n\tTokenBudget,\n} from \"../core/ir.js\";\nimport { estimateRuleTokens } from \"../core/tokenizer.js\";\n\nconst RULES_DIR = \".cursor/rules\";\nconst LEGACY_FILE = \".cursorrules\";\nconst MDC_EXT = \".mdc\";\n\n// ─── Filesystem Helpers ──────────────────────────────────────────\n\nasync function pathExists(filePath: string): Promise<boolean> {\n\ttry {\n\t\tawait access(filePath);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nasync function listMdcFiles(dir: string): Promise<string[]> {\n\ttry {\n\t\tconst entries = await readdir(dir);\n\t\treturn entries.filter((f) => f.endsWith(MDC_EXT)).sort();\n\t} catch {\n\t\treturn [];\n\t}\n}\n\n// ─── Frontmatter Parsing ─────────────────────────────────────────\n\nfunction stripQuotes(s: string): string {\n\tconst t = s.trim();\n\tif (\n\t\t(t.startsWith('\"') && t.endsWith('\"')) ||\n\t\t(t.startsWith(\"'\") && t.endsWith(\"'\"))\n\t) {\n\t\treturn t.slice(1, -1);\n\t}\n\treturn t;\n}\n\nfunction parseInlineArray(raw: string): string[] {\n\tconst inner = raw.slice(1, -1).trim();\n\tif (inner === \"\") return [];\n\treturn inner.split(\",\").map(stripQuotes);\n}\n\ninterface MdcParts {\n\treadonly yaml: string;\n\treadonly content: string;\n}\n\nfunction splitMdcFrontmatter(raw: string): MdcParts | null {\n\tconst lines = raw.replace(/\\r\\n/g, \"\\n\").split(\"\\n\");\n\tif (lines[0]?.trim() !== \"---\") return null;\n\n\tfor (let i = 1; i < lines.length; i++) {\n\t\tif (lines[i]?.trim() === \"---\") {\n\t\t\treturn {\n\t\t\t\tyaml: lines.slice(1, i).join(\"\\n\"),\n\t\t\t\tcontent: lines\n\t\t\t\t\t.slice(i + 1)\n\t\t\t\t\t.join(\"\\n\")\n\t\t\t\t\t.trim(),\n\t\t\t};\n\t\t}\n\t}\n\treturn null;\n}\n\ninterface MdcFields {\n\treadonly description: string | undefined;\n\treadonly globs: string[] | undefined;\n\treadonly alwaysApply: boolean;\n}\n\nfunction parseMdcFields(yaml: string): MdcFields {\n\tlet description: string | undefined;\n\tlet globs: string[] | undefined;\n\tlet alwaysApply = false;\n\n\tfor (const line of yaml.split(\"\\n\")) {\n\t\tconst trimmed = line.trim();\n\t\tif (trimmed === \"\") continue;\n\t\tconst colonIdx = trimmed.indexOf(\":\");\n\t\tif (colonIdx === -1) continue;\n\n\t\tconst key = trimmed.slice(0, colonIdx).trim();\n\t\tconst value = trimmed.slice(colonIdx + 1).trim();\n\n\t\tif (key === \"description\" && value !== \"\") {\n\t\t\tdescription = stripQuotes(value);\n\t\t} else if (key === \"globs\" && value !== \"\") {\n\t\t\tglobs =\n\t\t\t\tvalue.startsWith(\"[\") && value.endsWith(\"]\")\n\t\t\t\t\t? parseInlineArray(value)\n\t\t\t\t\t: [stripQuotes(value)];\n\t\t} else if (key === \"alwaysApply\") {\n\t\t\talwaysApply = value === \"true\";\n\t\t}\n\t}\n\n\treturn { description, globs, alwaysApply };\n}\n\n// ─── Scope Mapping ───────────────────────────────────────────────\n\nfunction determineScope(fields: MdcFields): RuleScope {\n\tif (fields.alwaysApply) return \"always\";\n\tif (fields.globs && fields.globs.length > 0) return \"file-scoped\";\n\tif (fields.description) return \"agent-selected\";\n\treturn \"always\";\n}\n\n// ─── Import Helpers ──────────────────────────────────────────────\n\nfunction importRuleWithoutFrontmatter(\n\traw: string,\n\tfilePath: string,\n\tid: string,\n): { rule: Rule; warning: ImportWarning } {\n\tconst content = raw.replace(/\\r\\n/g, \"\\n\").trim();\n\tconst description = id.replace(/-/g, \" \");\n\treturn {\n\t\trule: {\n\t\t\tid,\n\t\t\tscope: \"always\",\n\t\t\tdescription,\n\t\t\tcontent,\n\t\t\tcategory: \"general\",\n\t\t\tpriority: 3,\n\t\t\testimatedTokens: estimateRuleTokens(content, description),\n\t\t\tsource: {\n\t\t\t\tadapter: \"cursor\",\n\t\t\t\tfilePath,\n\t\t\t\timportedAt: new Date().toISOString(),\n\t\t\t},\n\t\t},\n\t\twarning: {\n\t\t\tfilePath,\n\t\t\tmessage: \"No frontmatter found, treating as always-on rule\",\n\t\t},\n\t};\n}\n\nfunction importRuleWithFrontmatter(\n\tparts: MdcParts,\n\tfilePath: string,\n\tid: string,\n): Rule {\n\tconst fields = parseMdcFields(parts.yaml);\n\tconst scope = determineScope(fields);\n\tconst description = fields.description ?? id.replace(/-/g, \" \");\n\n\treturn {\n\t\tid,\n\t\tscope,\n\t\tdescription,\n\t\tcontent: parts.content,\n\t\tcategory: \"general\",\n\t\tpriority: 3,\n\t\testimatedTokens: estimateRuleTokens(parts.content, description),\n\t\t...(scope === \"file-scoped\" && fields.globs ? { globs: fields.globs } : {}),\n\t\tsource: {\n\t\t\tadapter: \"cursor\",\n\t\t\tfilePath,\n\t\t\timportedAt: new Date().toISOString(),\n\t\t},\n\t};\n}\n\nasync function importMdcFiles(\n\tprojectRoot: string,\n): Promise<{ rules: Rule[]; warnings: ImportWarning[] }> {\n\tconst rules: Rule[] = [];\n\tconst warnings: ImportWarning[] = [];\n\tconst dir = join(projectRoot, RULES_DIR);\n\tconst files = await listMdcFiles(dir);\n\n\tfor (const file of files) {\n\t\tconst filePath = join(RULES_DIR, file);\n\t\tconst raw = await readFile(join(projectRoot, filePath), \"utf-8\");\n\t\tconst id = basename(file, MDC_EXT);\n\t\tconst parts = splitMdcFrontmatter(raw);\n\n\t\tif (!parts) {\n\t\t\tconst result = importRuleWithoutFrontmatter(raw, filePath, id);\n\t\t\trules.push(result.rule);\n\t\t\twarnings.push(result.warning);\n\t\t} else {\n\t\t\trules.push(importRuleWithFrontmatter(parts, filePath, id));\n\t\t}\n\t}\n\n\treturn { rules, warnings };\n}\n\nasync function importLegacyFile(\n\tprojectRoot: string,\n): Promise<{ rules: Rule[]; warnings: ImportWarning[] }> {\n\tconst legacyPath = join(projectRoot, LEGACY_FILE);\n\tif (!(await pathExists(legacyPath))) return { rules: [], warnings: [] };\n\n\tconst raw = await readFile(legacyPath, \"utf-8\");\n\tconst content = raw.trim();\n\tif (content === \"\") return { rules: [], warnings: [] };\n\n\treturn {\n\t\trules: [\n\t\t\t{\n\t\t\t\tid: \"cursorrules-legacy\",\n\t\t\t\tscope: \"always\",\n\t\t\t\tdescription: \"Legacy .cursorrules file\",\n\t\t\t\tcontent,\n\t\t\t\tcategory: \"general\",\n\t\t\t\tpriority: 3,\n\t\t\t\testimatedTokens: estimateRuleTokens(content, \"Legacy .cursorrules\"),\n\t\t\t\tsource: {\n\t\t\t\t\tadapter: \"cursor\",\n\t\t\t\t\tfilePath: LEGACY_FILE,\n\t\t\t\t\timportedAt: new Date().toISOString(),\n\t\t\t\t},\n\t\t\t},\n\t\t],\n\t\twarnings: [\n\t\t\t{\n\t\t\t\tfilePath: LEGACY_FILE,\n\t\t\t\tmessage: \".cursorrules is deprecated. Migrate to .cursor/rules/*.mdc\",\n\t\t\t},\n\t\t],\n\t};\n}\n\n// ─── Export Helpers ──────────────────────────────────────────────\n\nfunction serializeMdcGlobs(globs: string[]): string {\n\tif (globs.length === 1) {\n\t\tconst first = globs[0];\n\t\tif (first !== undefined) return `globs: \"${first}\"`;\n\t}\n\tconst items = globs.map((g) => `\"${g}\"`).join(\", \");\n\treturn `globs: [${items}]`;\n}\n\nfunction ruleToMdc(rule: Rule): string {\n\tconst lines: string[] = [\"---\"];\n\tlines.push(`description: \"${rule.description}\"`);\n\tif (rule.scope === \"file-scoped\" && rule.globs && rule.globs.length > 0) {\n\t\tlines.push(serializeMdcGlobs(rule.globs));\n\t}\n\tlines.push(`alwaysApply: ${rule.scope === \"always\"}`);\n\tlines.push(\"---\");\n\tlines.push(\"\");\n\tlines.push(rule.content);\n\tlines.push(\"\");\n\treturn lines.join(\"\\n\");\n}\n\nasync function deleteStaleFiles(\n\tprojectRoot: string,\n\texportedIds: Set<string>,\n\tdryRun: boolean,\n): Promise<string[]> {\n\tconst dir = join(projectRoot, RULES_DIR);\n\tconst existing = await listMdcFiles(dir);\n\tconst deleted: string[] = [];\n\n\tfor (const file of existing) {\n\t\tif (!exportedIds.has(file)) {\n\t\t\tconst filePath = join(RULES_DIR, file);\n\t\t\tif (!dryRun) await rm(join(projectRoot, filePath));\n\t\t\tdeleted.push(filePath);\n\t\t}\n\t}\n\n\treturn deleted;\n}\n\n// ─── Adapter ─────────────────────────────────────────────────────\n\nexport const cursorAdapter: RulixAdapter = {\n\tname: \"cursor\",\n\tdisplayName: \"Cursor\",\n\n\tasync detect(projectRoot: string): Promise<boolean> {\n\t\tconst rulesDir = join(projectRoot, RULES_DIR);\n\t\tconst legacyFile = join(projectRoot, LEGACY_FILE);\n\t\treturn (await pathExists(rulesDir)) || (await pathExists(legacyFile));\n\t},\n\n\tasync import(projectRoot: string): Promise<ImportResult> {\n\t\tconst mdc = await importMdcFiles(projectRoot);\n\t\tconst legacy = await importLegacyFile(projectRoot);\n\n\t\treturn {\n\t\t\trules: [...mdc.rules, ...legacy.rules],\n\t\t\twarnings: [...mdc.warnings, ...legacy.warnings],\n\t\t\tsource: RULES_DIR,\n\t\t};\n\t},\n\n\tasync export(\n\t\trules: Rule[],\n\t\tprojectRoot: string,\n\t\toptions?: ExportOptions,\n\t): Promise<ExportResult> {\n\t\tconst dir = join(projectRoot, RULES_DIR);\n\t\tconst dryRun = options?.dryRun === true;\n\t\tconst strategy = options?.strategy ?? \"overwrite\";\n\t\tconst filesWritten: string[] = [];\n\t\tconst warnings: ExportWarning[] = [];\n\n\t\tif (!dryRun) await mkdir(dir, { recursive: true });\n\n\t\tfor (const rule of rules) {\n\t\t\tconst filePath = join(RULES_DIR, `${rule.id}${MDC_EXT}`);\n\t\t\tif (!dryRun) {\n\t\t\t\tawait writeFile(join(projectRoot, filePath), ruleToMdc(rule), \"utf-8\");\n\t\t\t}\n\t\t\tfilesWritten.push(filePath);\n\t\t}\n\n\t\tconst exportedIds = new Set(rules.map((r) => `${r.id}${MDC_EXT}`));\n\t\tconst filesDeleted =\n\t\t\tstrategy === \"overwrite\"\n\t\t\t\t? await deleteStaleFiles(projectRoot, exportedIds, dryRun)\n\t\t\t\t: [];\n\n\t\treturn { filesWritten, filesDeleted, warnings };\n\t},\n\n\tgetTokenBudget(): TokenBudget {\n\t\treturn {\n\t\t\tmaxTokens: 5_000,\n\t\t\twarningThreshold: 0.8,\n\t\t\tsource: \"Cursor documentation\",\n\t\t};\n\t},\n};\n","/**\n * Adapter registry: lookup by name and auto-detection.\n */\n\nimport type { RulixAdapter } from \"../core/ir.js\";\nimport { agentsMdAdapter } from \"./agents-md.js\";\nimport { claudeCodeAdapter } from \"./claude-code.js\";\nimport { cursorAdapter } from \"./cursor.js\";\n\nconst BUILTIN_ADAPTERS: RulixAdapter[] = [\n\tcursorAdapter,\n\tclaudeCodeAdapter,\n\tagentsMdAdapter,\n];\n\nconst adapterMap = new Map<string, RulixAdapter>(\n\tBUILTIN_ADAPTERS.map((a) => [a.name, a]),\n);\n\n/** Returns all registered adapters. */\nexport function getAdapters(): RulixAdapter[] {\n\treturn [...adapterMap.values()];\n}\n\n/** Returns an adapter by name, or `undefined` if not found. */\nexport function getAdapter(name: string): RulixAdapter | undefined {\n\treturn adapterMap.get(name);\n}\n\n/** Returns all adapter names. */\nexport function getAdapterNames(): string[] {\n\treturn [...adapterMap.keys()];\n}\n\n/** Detects which adapters have existing rules in a project. */\nexport async function detectAdapters(\n\tprojectRoot: string,\n): Promise<RulixAdapter[]> {\n\tconst results = await Promise.all(\n\t\tBUILTIN_ADAPTERS.map(async (adapter) => ({\n\t\t\tadapter,\n\t\t\tdetected: await adapter.detect(projectRoot),\n\t\t})),\n\t);\n\treturn results.filter((r) => r.detected).map((r) => r.adapter);\n}\n","/**\n * Intermediate Representation (IR) types for Rulix.\n *\n * All logic operates on these types, never on raw tool-specific formats.\n * This module defines the core contract between the engine and adapters.\n */\n\n// ─── Scalar Types ────────────────────────────────────────────────\n\nexport type RuleScope = \"always\" | \"file-scoped\" | \"agent-selected\";\n\nexport type RuleCategory =\n\t| \"style\"\n\t| \"security\"\n\t| \"testing\"\n\t| \"architecture\"\n\t| \"workflow\"\n\t| \"general\";\n\nexport type ValidationSeverity = \"error\" | \"warning\" | \"info\";\n\nexport type ExportStrategy = \"overwrite\" | \"merge\";\n\nexport type TokenEstimation = \"heuristic\" | \"tiktoken\";\n\n// ─── Result Pattern ──────────────────────────────────────────────\n\n/**\n * Use for operations that can fail in predictable ways (parsing, validation).\n * Reserve exceptions for unexpected errors (filesystem, permissions).\n */\nexport type Result<T, E = RulixError> =\n\t| { readonly ok: true; readonly value: T }\n\t| { readonly ok: false; readonly error: E };\n\n/**\n * Every error carries a machine-readable `code` so callers can handle\n * specific failure modes programmatically without parsing message strings.\n */\nexport class RulixError extends Error {\n\tpublic override readonly name = \"RulixError\";\n\n\tconstructor(\n\t\tpublic readonly code: string,\n\t\tmessage: string,\n\t\tpublic override readonly cause?: unknown,\n\t) {\n\t\tsuper(message);\n\t}\n}\n\n// ─── Core IR ─────────────────────────────────────────────────────\n\n/**\n * Tracks where a rule was imported from so users can trace back\n * to the original source file when debugging conflicts.\n */\nexport interface RuleSource {\n\t/** E.g. `\"cursor\"`, `\"claude-code\"`, `\"rulix\"`. */\n\treadonly adapter: string;\n\t/** Relative to the project root. */\n\treadonly filePath: string;\n\t/** ISO 8601 timestamp. */\n\treadonly importedAt: string;\n}\n\n/**\n * The fundamental unit of the Rulix IR. All adapters convert\n * their tool-specific formats to and from this representation.\n */\nexport interface Rule {\n\t/** Kebab-case. Used for deduplication and preset composition. */\n\treadonly id: string;\n\treadonly scope: RuleScope;\n\t/** Used by agent-selected rules to let the AI decide relevance. */\n\treadonly description: string;\n\treadonly content: string;\n\t/** Required when `scope` is `\"file-scoped\"`. */\n\treadonly globs?: string[];\n\treadonly category: RuleCategory;\n\t/** 1 (critical) to 5 (nice-to-have). Drives token budget optimization. */\n\treadonly priority: number;\n\t/** E.g. `\"@rulix/typescript/strict\"`. */\n\treadonly extends?: string;\n\t/** Auto-computed by the tokenizer. */\n\treadonly estimatedTokens: number;\n\treadonly source?: RuleSource;\n}\n\nexport interface RulixConfigOptions {\n\treadonly tokenEstimation: TokenEstimation;\n\t/** Controls the \"Generated by Rulix\" header in AGENTS.md output. */\n\treadonly agentsMdHeader: boolean;\n\t/** Used by the `watch` command. */\n\treadonly syncOnSave: boolean;\n}\n\n/**\n * Fully-resolved project configuration with all defaults applied.\n */\nexport interface RulixConfig {\n\t/** Adapter names, e.g. `[\"cursor\", \"claude-code\", \"agents-md\"]`. */\n\treadonly targets: string[];\n\t/** npm packages or local paths to preset rule collections. */\n\treadonly presets: string[];\n\t/** Keyed by rule ID. */\n\treadonly overrides: Record<string, Partial<Rule>>;\n\treadonly options: RulixConfigOptions;\n}\n\nexport interface Ruleset {\n\t/** Local + preset rules, after overrides are applied. */\n\treadonly rules: Rule[];\n\treadonly config: RulixConfig;\n}\n\n// ─── Adapter Interface ───────────────────────────────────────────\n\n/**\n * Each adapter provides its own budget based on the tool's known token limits.\n */\nexport interface TokenBudget {\n\treadonly maxTokens: number;\n\t/** Fraction of max (e.g. 0.8 for 80%). */\n\treadonly warningThreshold: number;\n\t/** Where the limit comes from (e.g. documentation URL). */\n\treadonly source: string;\n}\n\nexport interface ExportOptions {\n\treadonly strategy: ExportStrategy;\n\t/** Preview changes without writing to disk. */\n\treadonly dryRun?: boolean | undefined;\n}\n\nexport interface ImportWarning {\n\treadonly ruleId?: string | undefined;\n\t/** Source file that produced the warning. */\n\treadonly filePath: string;\n\treadonly message: string;\n}\n\nexport interface ExportWarning {\n\treadonly ruleId?: string | undefined;\n\t/** Target file that produced the warning. */\n\treadonly filePath: string;\n\treadonly message: string;\n}\n\nexport interface ImportResult {\n\treadonly rules: Rule[];\n\treadonly warnings: ImportWarning[];\n\t/** E.g. `\".cursor/rules/\"`. */\n\treadonly source: string;\n}\n\nexport interface ExportResult {\n\treadonly filesWritten: string[];\n\t/** Files removed because the corresponding rule was deleted from source. */\n\treadonly filesDeleted: string[];\n\treadonly warnings: ExportWarning[];\n}\n\n/**\n * Contract that every tool-specific adapter must implement.\n *\n * Import reads tool-specific files into `Rule[]`; export writes `Rule[]`\n * back to the tool's native format.\n */\nexport interface RulixAdapter {\n\t/** E.g. `\"cursor\"`, `\"claude-code\"`. */\n\treadonly name: string;\n\t/** E.g. `\"Cursor\"`, `\"Claude Code\"`. */\n\treadonly displayName: string;\n\n\tdetect(projectRoot: string): Promise<boolean>;\n\timport(projectRoot: string): Promise<ImportResult>;\n\texport(\n\t\trules: Rule[],\n\t\tprojectRoot: string,\n\t\toptions?: ExportOptions,\n\t): Promise<ExportResult>;\n\tgetTokenBudget(): TokenBudget;\n}\n\n// ─── Validation ──────────────────────────────────────────────────\n\n/**\n * Codes follow the pattern `V001`–`V999` as defined in PRD section 11.1.\n */\nexport interface ValidationIssue {\n\t/** E.g. `\"V001\"`. */\n\treadonly code: string;\n\treadonly severity: ValidationSeverity;\n\treadonly message: string;\n\treadonly ruleId?: string | undefined;\n\treadonly filePath?: string | undefined;\n\t/** Actionable fix suggestion. */\n\treadonly suggestion?: string | undefined;\n}\n\nexport interface ValidationResult {\n\t/** `true` when no errors were found (warnings and info are allowed). */\n\treadonly passed: boolean;\n\treadonly errors: ValidationIssue[];\n\treadonly warnings: ValidationIssue[];\n\treadonly info: ValidationIssue[];\n}\n","/**\n * Schema, defaults, and loader for `.rulix/config.json`.\n *\n * Pure validation lives in `resolveConfig`; filesystem I/O lives in `loadConfig`.\n */\n\nimport { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type {\n\tResult,\n\tRule,\n\tRulixConfig,\n\tRulixConfigOptions,\n\tTokenEstimation,\n} from \"./ir.js\";\nimport { RulixError } from \"./ir.js\";\n\nexport const RULIX_DIR = \".rulix\";\nexport const CONFIG_FILENAME = \"config.json\";\nexport const RULES_DIR = \"rules\";\n\nconst DEFAULT_OPTIONS: RulixConfigOptions = {\n\ttokenEstimation: \"heuristic\",\n\tagentsMdHeader: true,\n\tsyncOnSave: false,\n};\n\nconst DEFAULT_CONFIG: RulixConfig = {\n\ttargets: [],\n\tpresets: [],\n\toverrides: {},\n\toptions: DEFAULT_OPTIONS,\n};\n\nexport function configPath(projectRoot: string): string {\n\treturn join(projectRoot, RULIX_DIR, CONFIG_FILENAME);\n}\n\nexport function rulesPath(projectRoot: string): string {\n\treturn join(projectRoot, RULIX_DIR, RULES_DIR);\n}\n\nexport function createDefaultConfig(): RulixConfig {\n\treturn DEFAULT_CONFIG;\n}\n\n// ─── Validation ──────────────────────────────────────────────────\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isStringArray(value: unknown): value is string[] {\n\treturn Array.isArray(value) && value.every((v) => typeof v === \"string\");\n}\n\nfunction configError(message: string): Result<never> {\n\treturn { ok: false, error: new RulixError(\"CONFIG_INVALID\", message) };\n}\n\nfunction resolveOptions(raw: unknown): Result<RulixConfigOptions> {\n\tif (raw === undefined) return { ok: true, value: DEFAULT_OPTIONS };\n\tif (!isRecord(raw)) return configError('\"options\" must be an object');\n\n\tif (\n\t\traw.tokenEstimation !== undefined &&\n\t\traw.tokenEstimation !== \"heuristic\" &&\n\t\traw.tokenEstimation !== \"tiktoken\"\n\t) {\n\t\treturn configError(\n\t\t\t'\"options.tokenEstimation\" must be \"heuristic\" or \"tiktoken\"',\n\t\t);\n\t}\n\tif (\n\t\traw.agentsMdHeader !== undefined &&\n\t\ttypeof raw.agentsMdHeader !== \"boolean\"\n\t) {\n\t\treturn configError('\"options.agentsMdHeader\" must be a boolean');\n\t}\n\tif (raw.syncOnSave !== undefined && typeof raw.syncOnSave !== \"boolean\") {\n\t\treturn configError('\"options.syncOnSave\" must be a boolean');\n\t}\n\n\treturn {\n\t\tok: true,\n\t\tvalue: {\n\t\t\ttokenEstimation:\n\t\t\t\t(raw.tokenEstimation as TokenEstimation | undefined) ??\n\t\t\t\tDEFAULT_OPTIONS.tokenEstimation,\n\t\t\tagentsMdHeader:\n\t\t\t\t(raw.agentsMdHeader as boolean | undefined) ??\n\t\t\t\tDEFAULT_OPTIONS.agentsMdHeader,\n\t\t\tsyncOnSave:\n\t\t\t\t(raw.syncOnSave as boolean | undefined) ?? DEFAULT_OPTIONS.syncOnSave,\n\t\t},\n\t};\n}\n\n/** Validates raw JSON and merges with defaults to produce a fully-resolved config. */\nexport function resolveConfig(raw: unknown): Result<RulixConfig> {\n\tif (!isRecord(raw)) return configError(\"Config must be a JSON object\");\n\n\tif (raw.targets !== undefined && !isStringArray(raw.targets)) {\n\t\treturn configError('\"targets\" must be an array of strings');\n\t}\n\tif (raw.presets !== undefined && !isStringArray(raw.presets)) {\n\t\treturn configError('\"presets\" must be an array of strings');\n\t}\n\tif (raw.overrides !== undefined && !isRecord(raw.overrides)) {\n\t\treturn configError('\"overrides\" must be an object');\n\t}\n\n\tconst options = resolveOptions(raw.options);\n\tif (!options.ok) return options;\n\n\treturn {\n\t\tok: true,\n\t\tvalue: {\n\t\t\ttargets: (raw.targets as string[] | undefined) ?? DEFAULT_CONFIG.targets,\n\t\t\tpresets: (raw.presets as string[] | undefined) ?? DEFAULT_CONFIG.presets,\n\t\t\toverrides:\n\t\t\t\t(raw.overrides as Record<string, Partial<Rule>> | undefined) ??\n\t\t\t\tDEFAULT_CONFIG.overrides,\n\t\t\toptions: options.value,\n\t\t},\n\t};\n}\n\n// ─── I/O ─────────────────────────────────────────────────────────\n\n/** Reads `.rulix/config.json` from disk. Returns defaults if file is missing. */\nexport async function loadConfig(\n\tprojectRoot: string,\n): Promise<Result<RulixConfig>> {\n\tconst filePath = configPath(projectRoot);\n\n\tlet content: string;\n\ttry {\n\t\tcontent = await readFile(filePath, \"utf-8\");\n\t} catch (error: unknown) {\n\t\tif (error instanceof Error && \"code\" in error && error.code === \"ENOENT\") {\n\t\t\treturn { ok: true, value: createDefaultConfig() };\n\t\t}\n\t\tthrow error;\n\t}\n\n\tlet raw: unknown;\n\ttry {\n\t\traw = JSON.parse(content);\n\t} catch {\n\t\treturn configError(`Invalid JSON in ${filePath}`);\n\t}\n\n\treturn resolveConfig(raw);\n}\n","/**\n * Hand-rolled frontmatter parser for `.rulix/rules/*.md`.\n *\n * Supports the subset of YAML needed by Rulix frontmatter:\n * simple key-value pairs, quoted strings, numbers, and arrays\n * (both inline `[a, b]` and multi-line `- a`).\n */\n\nimport { mkdir, readdir, readFile, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { rulesPath } from \"./config.js\";\nimport type { Result, Rule, RuleCategory, RuleScope } from \"./ir.js\";\nimport { RulixError } from \"./ir.js\";\nimport { estimateRuleTokens } from \"./tokenizer.js\";\n\nconst DEFAULT_CATEGORY: RuleCategory = \"general\";\nconst DEFAULT_PRIORITY = 3;\n\n// ─── Error Helper ────────────────────────────────────────────────\n\nfunction parseError(message: string, filePath?: string): Result<never> {\n\tconst suffix = filePath ? ` in ${filePath}` : \"\";\n\treturn {\n\t\tok: false,\n\t\terror: new RulixError(\"PARSE_ERROR\", `${message}${suffix}`),\n\t};\n}\n\n// ─── YAML Helpers ────────────────────────────────────────────────\n\nfunction stripQuotes(value: string): string {\n\tconst t = value.trim();\n\tif (\n\t\t(t.startsWith('\"') && t.endsWith('\"')) ||\n\t\t(t.startsWith(\"'\") && t.endsWith(\"'\"))\n\t) {\n\t\treturn t.slice(1, -1);\n\t}\n\treturn t;\n}\n\nfunction parseInlineArray(raw: string): string[] {\n\tconst inner = raw.slice(1, -1).trim();\n\tif (inner === \"\") return [];\n\treturn inner.split(\",\").map(stripQuotes);\n}\n\nfunction parseYamlValue(raw: string): unknown {\n\tconst trimmed = raw.trim();\n\tif (trimmed === \"\") return trimmed;\n\tif (trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\")) {\n\t\treturn parseInlineArray(trimmed);\n\t}\n\tif (/^-?\\d+$/.test(trimmed)) return Number(trimmed);\n\tif (trimmed === \"true\") return true;\n\tif (trimmed === \"false\") return false;\n\treturn stripQuotes(trimmed);\n}\n\n// ─── Frontmatter Splitting ───────────────────────────────────────\n\ninterface FrontmatterParts {\n\treadonly yaml: string;\n\treadonly content: string;\n}\n\nfunction splitFrontmatter(raw: string): Result<FrontmatterParts> {\n\tconst lines = raw.replace(/\\r\\n/g, \"\\n\").split(\"\\n\");\n\n\tif (lines[0]?.trim() !== \"---\") {\n\t\treturn parseError(\"File must start with frontmatter (---)\");\n\t}\n\n\tlet closingIndex = -1;\n\tfor (let i = 1; i < lines.length; i++) {\n\t\tif (lines[i]?.trim() === \"---\") {\n\t\t\tclosingIndex = i;\n\t\t\tbreak;\n\t\t}\n\t}\n\n\tif (closingIndex === -1) {\n\t\treturn parseError(\"Unterminated frontmatter (missing closing ---)\");\n\t}\n\n\treturn {\n\t\tok: true,\n\t\tvalue: {\n\t\t\tyaml: lines.slice(1, closingIndex).join(\"\\n\"),\n\t\t\tcontent: lines.slice(closingIndex + 1).join(\"\\n\"),\n\t\t},\n\t};\n}\n\n// ─── Frontmatter Field Parsing ───────────────────────────────────\n\nfunction parseFrontmatterFields(yaml: string): Record<string, unknown> {\n\tconst fields: Record<string, unknown> = {};\n\tconst lines = yaml.split(\"\\n\");\n\tlet arrayKey: string | undefined;\n\tlet arrayItems: string[] = [];\n\n\tfor (const line of lines) {\n\t\tconst trimmed = line.trim();\n\t\tif (trimmed === \"\") continue;\n\n\t\tif (trimmed.startsWith(\"- \")) {\n\t\t\tif (arrayKey !== undefined) {\n\t\t\t\tarrayItems.push(stripQuotes(trimmed.slice(2)));\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (arrayKey !== undefined) {\n\t\t\tfields[arrayKey] = arrayItems;\n\t\t\tarrayKey = undefined;\n\t\t\tarrayItems = [];\n\t\t}\n\n\t\tconst colonIndex = trimmed.indexOf(\":\");\n\t\tif (colonIndex === -1) continue;\n\n\t\tconst key = trimmed.slice(0, colonIndex).trim();\n\t\tconst rawValue = trimmed.slice(colonIndex + 1).trim();\n\n\t\tif (rawValue === \"\") {\n\t\t\tarrayKey = key;\n\t\t\tarrayItems = [];\n\t\t} else {\n\t\t\tfields[key] = parseYamlValue(rawValue);\n\t\t}\n\t}\n\n\tif (arrayKey !== undefined) fields[arrayKey] = arrayItems;\n\n\treturn fields;\n}\n\n// ─── Type Guards ─────────────────────────────────────────────────\n\nfunction isValidScope(value: unknown): value is RuleScope {\n\treturn (\n\t\tvalue === \"always\" || value === \"file-scoped\" || value === \"agent-selected\"\n\t);\n}\n\nfunction isValidCategory(value: unknown): value is RuleCategory {\n\treturn (\n\t\tvalue === \"style\" ||\n\t\tvalue === \"security\" ||\n\t\tvalue === \"testing\" ||\n\t\tvalue === \"architecture\" ||\n\t\tvalue === \"workflow\" ||\n\t\tvalue === \"general\"\n\t);\n}\n\nfunction normalizeGlobs(value: unknown): string[] | undefined {\n\tif (value === undefined) return undefined;\n\tif (typeof value === \"string\") return [value];\n\tif (Array.isArray(value) && value.every((v) => typeof v === \"string\")) {\n\t\treturn value as string[];\n\t}\n\treturn undefined;\n}\n\n// ─── Public API: Parse & Serialize ───────────────────────────────\n\n/** Parses a markdown file with YAML frontmatter into a Rule. */\nexport function parseRule(raw: string, filePath: string): Result<Rule> {\n\tconst split = splitFrontmatter(raw);\n\tif (!split.ok) return parseError(split.error.message, filePath);\n\n\tconst fields = parseFrontmatterFields(split.value.yaml);\n\tconst content = split.value.content.trim();\n\tconst { id, scope, description } = fields;\n\n\tif (typeof id !== \"string\" || id === \"\") {\n\t\treturn parseError('Missing required field \"id\"', filePath);\n\t}\n\tif (!isValidScope(scope)) {\n\t\treturn parseError('Invalid or missing \"scope\"', filePath);\n\t}\n\tif (typeof description !== \"string\" || description === \"\") {\n\t\treturn parseError('Missing required field \"description\"', filePath);\n\t}\n\n\tconst globs = normalizeGlobs(fields.globs);\n\tconst extendsVal =\n\t\ttypeof fields.extends === \"string\" ? fields.extends : undefined;\n\n\treturn {\n\t\tok: true,\n\t\tvalue: {\n\t\t\tid,\n\t\t\tscope,\n\t\t\tdescription,\n\t\t\tcontent,\n\t\t\tcategory: isValidCategory(fields.category)\n\t\t\t\t? fields.category\n\t\t\t\t: DEFAULT_CATEGORY,\n\t\t\tpriority:\n\t\t\t\ttypeof fields.priority === \"number\"\n\t\t\t\t\t? fields.priority\n\t\t\t\t\t: DEFAULT_PRIORITY,\n\t\t\testimatedTokens: estimateRuleTokens(content, description),\n\t\t\t...(globs !== undefined ? { globs } : {}),\n\t\t\t...(extendsVal !== undefined ? { extends: extendsVal } : {}),\n\t\t},\n\t};\n}\n\nfunction serializeGlobs(globs: string[], lines: string[]): void {\n\tif (globs.length === 1) {\n\t\tconst first = globs[0];\n\t\tif (first !== undefined) lines.push(`globs: \"${first}\"`);\n\t\treturn;\n\t}\n\tlines.push(\"globs:\");\n\tfor (const glob of globs) {\n\t\tlines.push(` - \"${glob}\"`);\n\t}\n}\n\n/** Serializes a Rule back to markdown with YAML frontmatter. */\nexport function serializeRule(rule: Rule): string {\n\tconst lines: string[] = [\"---\"];\n\tlines.push(`id: ${rule.id}`);\n\tlines.push(`scope: ${rule.scope}`);\n\tlines.push(`description: \"${rule.description}\"`);\n\tif (rule.globs && rule.globs.length > 0) serializeGlobs(rule.globs, lines);\n\tlines.push(`category: ${rule.category}`);\n\tlines.push(`priority: ${rule.priority}`);\n\tif (rule.extends) lines.push(`extends: ${rule.extends}`);\n\tlines.push(\"---\");\n\tlines.push(\"\");\n\tlines.push(rule.content);\n\tlines.push(\"\");\n\treturn lines.join(\"\\n\");\n}\n\n// ─── Public API: I/O ─────────────────────────────────────────────\n\n/** Reads and parses all `.md` rule files from `.rulix/rules/`. */\nexport async function loadRules(projectRoot: string): Promise<Result<Rule[]>> {\n\tconst dir = rulesPath(projectRoot);\n\n\tlet entries: string[];\n\ttry {\n\t\tentries = await readdir(dir);\n\t} catch (error: unknown) {\n\t\tif (error instanceof Error && \"code\" in error && error.code === \"ENOENT\") {\n\t\t\treturn { ok: true, value: [] };\n\t\t}\n\t\tthrow error;\n\t}\n\n\tconst mdFiles = entries.filter((f) => f.endsWith(\".md\")).sort();\n\tconst rules: Rule[] = [];\n\n\tfor (const file of mdFiles) {\n\t\tconst filePath = join(dir, file);\n\t\tconst raw = await readFile(filePath, \"utf-8\");\n\t\tconst result = parseRule(raw, filePath);\n\t\tif (!result.ok) return result;\n\t\trules.push(result.value);\n\t}\n\n\treturn { ok: true, value: rules };\n}\n\n/** Writes a single rule to `.rulix/rules/{id}.md`. Creates the directory if needed. */\nexport async function writeRule(\n\tprojectRoot: string,\n\trule: Rule,\n): Promise<void> {\n\tconst dir = rulesPath(projectRoot);\n\tawait mkdir(dir, { recursive: true });\n\tawait writeFile(join(dir, `${rule.id}.md`), serializeRule(rule), \"utf-8\");\n}\n","/**\n * Validation engine for Rulix rules (V001–V012).\n *\n * Each check is a pure function that inspects rules structurally.\n * V006 (token budgets) and V008 (agent-selected support) are deferred\n * to the adapter layer where tool-specific limits are known.\n */\n\nimport type {\n\tRule,\n\tValidationIssue,\n\tValidationResult,\n\tValidationSeverity,\n} from \"./ir.js\";\n\nconst MIN_CONTENT_LENGTH = 20;\nconst MAX_CONTENT_LINES = 50;\nconst CURSOR_MAX_LINES = 50;\nconst CLAUDE_CODE_TOKEN_BUDGET = 4_000;\n\nconst VAGUE_PATTERNS: RegExp[] = [\n\t/handle .+ properly/i,\n\t/do .+ correctly/i,\n\t/implement .+ properly/i,\n\t/make sure .+ works/i,\n\t/ensure .+ is correct/i,\n\t/follow best practices/i,\n];\n\n// ─── Issue Factory ───────────────────────────────────────────────\n\nfunction createIssue(\n\tcode: string,\n\tseverity: ValidationSeverity,\n\tmessage: string,\n\truleId?: string,\n\tsuggestion?: string,\n): ValidationIssue {\n\treturn {\n\t\tcode,\n\t\tseverity,\n\t\tmessage,\n\t\t...(ruleId !== undefined ? { ruleId } : {}),\n\t\t...(suggestion !== undefined ? { suggestion } : {}),\n\t};\n}\n\n// ─── Cross-Rule Checks ──────────────────────────────────────────\n\nfunction checkDuplicateIds(rules: Rule[]): ValidationIssue[] {\n\tconst seen = new Map<string, number>();\n\tconst issues: ValidationIssue[] = [];\n\n\tfor (const rule of rules) {\n\t\tconst count = (seen.get(rule.id) ?? 0) + 1;\n\t\tseen.set(rule.id, count);\n\t\tif (count === 2) {\n\t\t\tissues.push(\n\t\t\t\tcreateIssue(\n\t\t\t\t\t\"V001\",\n\t\t\t\t\t\"error\",\n\t\t\t\t\t`Duplicate rule ID \"${rule.id}\"`,\n\t\t\t\t\trule.id,\n\t\t\t\t\t\"Rename one of the duplicate rules\",\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\t}\n\n\treturn issues;\n}\n\n// ─── Per-Rule Checks ─────────────────────────────────────────────\n\nfunction checkRequiredFields(rule: Rule): ValidationIssue[] {\n\tconst issues: ValidationIssue[] = [];\n\tif (rule.id === \"\") {\n\t\tissues.push(createIssue(\"V002\", \"error\", \"Rule has empty ID\", rule.id));\n\t}\n\tif (rule.description === \"\") {\n\t\tissues.push(\n\t\t\tcreateIssue(\n\t\t\t\t\"V002\",\n\t\t\t\t\"error\",\n\t\t\t\t`Rule \"${rule.id}\" has empty description`,\n\t\t\t\trule.id,\n\t\t\t),\n\t\t);\n\t}\n\treturn issues;\n}\n\nfunction checkFileScopedGlobs(rule: Rule): ValidationIssue[] {\n\tif (rule.scope !== \"file-scoped\") return [];\n\tif (!rule.globs || rule.globs.length === 0) {\n\t\treturn [\n\t\t\tcreateIssue(\n\t\t\t\t\"V003\",\n\t\t\t\t\"error\",\n\t\t\t\t`Rule \"${rule.id}\" is file-scoped but has no globs`,\n\t\t\t\trule.id,\n\t\t\t\t'Add globs or change scope to \"always\"',\n\t\t\t),\n\t\t];\n\t}\n\treturn [];\n}\n\nfunction checkShortContent(rule: Rule): ValidationIssue[] {\n\tif (rule.content.length < MIN_CONTENT_LENGTH) {\n\t\treturn [\n\t\t\tcreateIssue(\n\t\t\t\t\"V004\",\n\t\t\t\t\"warning\",\n\t\t\t\t`Rule \"${rule.id}\" has very short content (${rule.content.length} chars)`,\n\t\t\t\trule.id,\n\t\t\t\t\"Consider adding more detail\",\n\t\t\t),\n\t\t];\n\t}\n\treturn [];\n}\n\nfunction checkVagueDescription(rule: Rule): ValidationIssue[] {\n\tfor (const pattern of VAGUE_PATTERNS) {\n\t\tif (pattern.test(rule.description)) {\n\t\t\treturn [\n\t\t\t\tcreateIssue(\n\t\t\t\t\t\"V005\",\n\t\t\t\t\t\"warning\",\n\t\t\t\t\t`Rule \"${rule.id}\" has a vague description`,\n\t\t\t\t\trule.id,\n\t\t\t\t\t\"Be more specific about what conventions to follow\",\n\t\t\t\t),\n\t\t\t];\n\t\t}\n\t}\n\treturn [];\n}\n\nfunction checkDefaultCategory(rule: Rule): ValidationIssue[] {\n\tif (rule.category === \"general\") {\n\t\treturn [\n\t\t\tcreateIssue(\n\t\t\t\t\"V007\",\n\t\t\t\t\"info\",\n\t\t\t\t`Rule \"${rule.id}\" has no specific category`,\n\t\t\t\trule.id,\n\t\t\t\t\"Consider assigning a category (style, security, testing, architecture, workflow)\",\n\t\t\t),\n\t\t];\n\t}\n\treturn [];\n}\n\nfunction isValidGlobSyntax(pattern: string): boolean {\n\tif (pattern.trim() === \"\") return false;\n\tlet brackets = 0;\n\tlet braces = 0;\n\tfor (const ch of pattern) {\n\t\tif (ch === \"[\") brackets++;\n\t\telse if (ch === \"]\") brackets--;\n\t\telse if (ch === \"{\") braces++;\n\t\telse if (ch === \"}\") braces--;\n\t\tif (brackets < 0 || braces < 0) return false;\n\t}\n\treturn brackets === 0 && braces === 0;\n}\n\nfunction checkGlobSyntax(rule: Rule): ValidationIssue[] {\n\tif (!rule.globs) return [];\n\tconst issues: ValidationIssue[] = [];\n\tfor (const glob of rule.globs) {\n\t\tif (!isValidGlobSyntax(glob)) {\n\t\t\tissues.push(\n\t\t\t\tcreateIssue(\n\t\t\t\t\t\"V009\",\n\t\t\t\t\t\"error\",\n\t\t\t\t\t`Rule \"${rule.id}\" has invalid glob pattern: \"${glob}\"`,\n\t\t\t\t\trule.id,\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\t}\n\treturn issues;\n}\n\nfunction checkLongContent(rule: Rule): ValidationIssue[] {\n\tconst lineCount = rule.content.split(\"\\n\").length;\n\tif (lineCount > MAX_CONTENT_LINES) {\n\t\treturn [\n\t\t\tcreateIssue(\n\t\t\t\t\"V010\",\n\t\t\t\t\"warning\",\n\t\t\t\t`Rule \"${rule.id}\" has ${lineCount} lines (>${MAX_CONTENT_LINES})`,\n\t\t\t\trule.id,\n\t\t\t\t\"Consider splitting into smaller rules\",\n\t\t\t),\n\t\t];\n\t}\n\treturn [];\n}\n\n// ─── Target-Aware Checks ─────────────────────────────────────────\n\nfunction checkCursorLineLimit(rule: Rule): ValidationIssue[] {\n\tconst lineCount = rule.content.split(\"\\n\").length;\n\tif (lineCount > CURSOR_MAX_LINES) {\n\t\treturn [\n\t\t\tcreateIssue(\n\t\t\t\t\"V011\",\n\t\t\t\t\"warning\",\n\t\t\t\t`Rule \"${rule.id}\" has ${lineCount} lines — Cursor recommends <${CURSOR_MAX_LINES} lines per .mdc file`,\n\t\t\t\trule.id,\n\t\t\t\t\"Split into smaller, focused rules\",\n\t\t\t),\n\t\t];\n\t}\n\treturn [];\n}\n\nfunction checkTotalTokenBudget(rules: Rule[]): ValidationIssue[] {\n\tlet total = 0;\n\tfor (const rule of rules) {\n\t\ttotal += rule.estimatedTokens;\n\t}\n\tif (total > CLAUDE_CODE_TOKEN_BUDGET) {\n\t\treturn [\n\t\t\tcreateIssue(\n\t\t\t\t\"V012\",\n\t\t\t\t\"info\",\n\t\t\t\t`Total rule tokens (${total}) exceed ${CLAUDE_CODE_TOKEN_BUDGET} — consider reducing to keep Claude responsive`,\n\t\t\t),\n\t\t];\n\t}\n\treturn [];\n}\n\n// ─── Result Builder ──────────────────────────────────────────────\n\nfunction buildResult(issues: ValidationIssue[]): ValidationResult {\n\treturn {\n\t\tpassed: issues.every((i) => i.severity !== \"error\"),\n\t\terrors: issues.filter((i) => i.severity === \"error\"),\n\t\twarnings: issues.filter((i) => i.severity === \"warning\"),\n\t\tinfo: issues.filter((i) => i.severity === \"info\"),\n\t};\n}\n\n// ─── Public API ──────────────────────────────────────────────────\n\n/** Validates rules for structural issues (V001–V005, V007, V009–V012). */\nexport function validateRules(\n\trules: Rule[],\n\ttargets?: string[] | undefined,\n): ValidationResult {\n\tconst issues: ValidationIssue[] = [];\n\tconst hasCursor = targets?.includes(\"cursor\") === true;\n\n\tissues.push(...checkDuplicateIds(rules));\n\n\tfor (const rule of rules) {\n\t\tissues.push(...checkRequiredFields(rule));\n\t\tissues.push(...checkFileScopedGlobs(rule));\n\t\tissues.push(...checkShortContent(rule));\n\t\tissues.push(...checkVagueDescription(rule));\n\t\tissues.push(...checkDefaultCategory(rule));\n\t\tissues.push(...checkGlobSyntax(rule));\n\t\tissues.push(...checkLongContent(rule));\n\t\tif (hasCursor) {\n\t\t\tissues.push(...checkCursorLineLimit(rule));\n\t\t}\n\t}\n\n\tissues.push(...checkTotalTokenBudget(rules));\n\n\treturn buildResult(issues);\n}\n"]}
|