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 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 | Plain markdown | `CLAUDE.md` + `.claude/rules/` |
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 → CLAUDE.md + .claude/rules/
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
- const exportable = rules.filter(
54
- (r) => r.scope === "always" || r.scope === "file-scoped"
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 parsePaths(yaml) {
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
- return inner === "" ? [] : inner.split(",").map(stripQuotes);
187
+ paths = inner === "" ? [] : inner.split(",").map(stripQuotes);
188
+ } else {
189
+ paths = [stripQuotes(value)];
195
190
  }
196
- return [stripQuotes(value)];
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 { preamble, sections };
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 description = id.replace(/-/g, " ");
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(id, "always", description, content, filePath);
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
- scope,
299
- description,
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 (!rule.globs || rule.globs.length === 0) {
330
- return `${rule.content}
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
- const fileScoped = rules.filter((r) => r.scope === "file-scoped");
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 fileScoped) {
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, CLAUDE_MD);
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: [...mdRules, ...ruleFiles],
337
+ rules: ruleFiles,
406
338
  warnings: [],
407
- source: CLAUDE_MD
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 mdWritten = await exportClaudeMd(rules, projectRoot, dryRun);
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: [...mdWritten, ...rulesWritten],
348
+ filesWritten,
418
349
  filesDeleted,
419
350
  warnings: []
420
351
  };
421
352
  },
422
353
  getTokenBudget() {
423
354
  return {
424
- maxTokens: 2e3,
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: 1e4,
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-I6MHHT6P.cjs.map
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"]}