@zebralabs/context-cli 0.1.2 → 0.1.4

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.
@@ -0,0 +1,133 @@
1
+ import { FileWriter } from "../../infrastructure/file-system/file-writer.js";
2
+ import path from "node:path";
3
+
4
+ /**
5
+ * Stage 6: Validation & Reporting
6
+ * Validates compilation and generates comprehensive reports
7
+ */
8
+ export class Stage6Validation {
9
+ constructor() {
10
+ this.fileWriter = new FileWriter();
11
+ }
12
+
13
+ /**
14
+ * Validate and generate reports
15
+ * @param {Compilation} compilation - The compilation
16
+ * @param {Object} stageReports - Reports from all stages
17
+ * @param {string} outputDir - Output directory
18
+ * @returns {Object} Validation result
19
+ */
20
+ execute(compilation, stageReports, outputDir) {
21
+ const reportsDir = path.join(outputDir, "reports");
22
+ this.fileWriter.ensureDir(reportsDir);
23
+
24
+ // Validation checks
25
+ const validation = {
26
+ rulesValid: true,
27
+ noDuplicateIds: true,
28
+ allRulesHaveIds: true,
29
+ errors: []
30
+ };
31
+
32
+ // Check for duplicate rule IDs
33
+ const ruleIds = new Set();
34
+ for (const rule of compilation.rules) {
35
+ if (ruleIds.has(rule.id)) {
36
+ validation.noDuplicateIds = false;
37
+ validation.errors.push(`Duplicate rule ID: ${rule.id}`);
38
+ }
39
+ ruleIds.add(rule.id);
40
+ }
41
+
42
+ // Check all rules have required fields
43
+ for (const rule of compilation.rules) {
44
+ if (!rule.id || !rule.level || !rule.rule) {
45
+ validation.allRulesHaveIds = false;
46
+ validation.errors.push(`Rule missing required fields: ${rule.id || "unknown"}`);
47
+ }
48
+ }
49
+
50
+ validation.rulesValid = validation.noDuplicateIds && validation.allRulesHaveIds;
51
+
52
+ // Generate compilation report
53
+ const compilationReport = this.generateCompilationReport(compilation, stageReports, validation);
54
+ this.fileWriter.writeFile(
55
+ path.join(reportsDir, "COMPILATION-REPORT.md"),
56
+ compilationReport
57
+ );
58
+
59
+ return {
60
+ validation,
61
+ reportsGenerated: ["COMPILATION-REPORT.md"]
62
+ };
63
+ }
64
+
65
+ /**
66
+ * Generate compilation report
67
+ */
68
+ generateCompilationReport(compilation, stageReports, validation) {
69
+ const lines = [];
70
+ lines.push("# Compilation Report");
71
+ lines.push("");
72
+ lines.push(`Generated: ${compilation.metadata.timestamp}`);
73
+ lines.push("");
74
+
75
+ // Summary
76
+ lines.push("## Summary");
77
+ lines.push("");
78
+ lines.push(`- Files Processed: ${stageReports.stage2?.filesProcessed || 0}`);
79
+ lines.push(`- Rules Extracted: ${compilation.rules.length}`);
80
+ lines.push(`- Preferences Extracted: ${compilation.preferences.length}`);
81
+ lines.push(`- Scopes Extracted: ${compilation.scopes.length}`);
82
+ lines.push(`- Categories: ${compilation.getCategories().length}`);
83
+ lines.push("");
84
+
85
+ // Validation
86
+ lines.push("## Validation");
87
+ lines.push("");
88
+ if (validation.rulesValid) {
89
+ lines.push("✅ All rules are valid");
90
+ } else {
91
+ lines.push("❌ Validation errors found:");
92
+ for (const error of validation.errors) {
93
+ lines.push(` - ${error}`);
94
+ }
95
+ }
96
+ lines.push("");
97
+
98
+ // Rules by category
99
+ lines.push("## Rules by Category");
100
+ lines.push("");
101
+ const categories = compilation.getCategories();
102
+ for (const category of categories) {
103
+ const rules = compilation.getRulesByCategory(category);
104
+ lines.push(`- **${category}**: ${rules.length} rules`);
105
+ }
106
+ lines.push("");
107
+
108
+ // Rules by level
109
+ lines.push("## Rules by Level");
110
+ lines.push("");
111
+ const levels = ["must", "should", "prefer", "avoid"];
112
+ for (const level of levels) {
113
+ const rules = compilation.getRulesByLevel(level);
114
+ if (rules.length > 0) {
115
+ lines.push(`- **${level}**: ${rules.length} rules`);
116
+ }
117
+ }
118
+ lines.push("");
119
+
120
+ // Errors and warnings
121
+ if (stageReports.stage2?.errors?.length > 0) {
122
+ lines.push("## Errors");
123
+ lines.push("");
124
+ for (const error of stageReports.stage2.errors) {
125
+ lines.push(`- ${error.file}: ${error.error}`);
126
+ }
127
+ lines.push("");
128
+ }
129
+
130
+ return lines.join("\n");
131
+ }
132
+ }
133
+
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Asset Generator Port (Interface)
3
+ * Defines the contract for tool-specific asset generators
4
+ */
5
+ export class IAssetGenerator {
6
+ /**
7
+ * Generate tool-specific assets from consolidated rules
8
+ * @param {Compilation} compilation - The compiled context
9
+ * @param {string} outputPath - Where to write assets
10
+ * @returns {Promise<Object>} Generation result
11
+ */
12
+ async generate(compilation, outputPath) {
13
+ throw new Error("Not implemented");
14
+ }
15
+
16
+ /**
17
+ * Merge pre-generated assets from packs
18
+ * @param {Object[]} assets - Assets from packs
19
+ * @returns {Object[]} Merged assets
20
+ */
21
+ merge(assets) {
22
+ throw new Error("Not implemented");
23
+ }
24
+
25
+ /**
26
+ * Get the tool name this generator is for
27
+ * @returns {string} Tool name (e.g., "cursor", "claude")
28
+ */
29
+ getToolName() {
30
+ throw new Error("Not implemented");
31
+ }
32
+ }
33
+
package/src/context.js CHANGED
@@ -17,9 +17,9 @@ Context-as-Code CLI
17
17
 
18
18
  Usage:
19
19
  ctx help
20
- ctx list
21
- ctx compile
22
- ctx validate
20
+ ctx list [--context-yaml <path>]
21
+ ctx compile [--pack <packId>] [--context-yaml <path>]
22
+ ctx validate [--context-yaml <path>]
23
23
 
24
24
  Pack install:
25
25
  ctx pack install <packId> [--repo-root <path>] [--mode SkipExisting|Overwrite]
@@ -32,23 +32,74 @@ Examples (registry download):
32
32
  ctx pack install pack-01-documentation-management --registry https://example.com --token YOURTOKEN
33
33
  ctx pack install pack-01-documentation-management --registry https://example.com --token YOURTOKEN --version 0.1.0
34
34
 
35
+ Examples (compile):
36
+ ctx compile --pack pack-01-documentation-management
37
+
35
38
  Notes:
36
39
  - Zip must contain: practices-and-standards/install.ps1
37
- - Installer merges into <repo-root>/practices-and-standards/
40
+ - Installer merges into <repo-root>/docs/practices-and-standards/
38
41
  `.trim() + "\n");
39
42
  }
40
43
 
41
44
  function findRepoContextRoot(startDir) {
42
45
  let dir = startDir;
43
46
  while (true) {
44
- const candidate = path.join(dir, "practices-and-standards", "context.yaml");
45
- if (fs.existsSync(candidate)) return dir;
47
+ // Check installed location
48
+ const candidate1 = path.join(dir, "docs", "practices-and-standards", "context.yaml");
49
+ if (fs.existsSync(candidate1)) return dir;
50
+
51
+ // Check source/development location
52
+ const candidate2 = path.join(dir, "practices-and-standards", "context.yaml");
53
+ if (fs.existsSync(candidate2)) return dir;
54
+
46
55
  const parent = path.dirname(dir);
47
56
  if (parent === dir) return null;
48
57
  dir = parent;
49
58
  }
50
59
  }
51
60
 
61
+ function deriveRepoRootFromContextPath(contextPath) {
62
+ const normalized = path.normalize(contextPath);
63
+
64
+ // Check if path ends with docs/practices-and-standards/context.yaml
65
+ const docsSuffix = path.join("docs", "practices-and-standards", "context.yaml");
66
+ if (normalized.endsWith(docsSuffix)) {
67
+ return normalized.substring(0, normalized.length - docsSuffix.length);
68
+ }
69
+
70
+ // Check if path ends with practices-and-standards/context.yaml
71
+ const psSuffix = path.join("practices-and-standards", "context.yaml");
72
+ if (normalized.endsWith(psSuffix)) {
73
+ return normalized.substring(0, normalized.length - psSuffix.length);
74
+ }
75
+
76
+ // Fallback: go up 3 levels from context.yaml
77
+ // context.yaml -> practices-and-standards -> (repo root)
78
+ let current = path.dirname(contextPath);
79
+ if (path.basename(current) === "practices-and-standards") {
80
+ return path.dirname(current);
81
+ }
82
+ // Or if in docs/practices-and-standards
83
+ if (path.basename(current) === "practices-and-standards" && path.basename(path.dirname(current)) === "docs") {
84
+ return path.dirname(path.dirname(current));
85
+ }
86
+
87
+ // Last resort: assume parent of practices-and-standards or docs
88
+ return path.dirname(path.dirname(contextPath));
89
+ }
90
+
91
+ function findContextYamlPath(repoRoot) {
92
+ // Try installed location first
93
+ const installedPath = path.join(repoRoot, "docs", "practices-and-standards", "context.yaml");
94
+ if (fs.existsSync(installedPath)) return installedPath;
95
+
96
+ // Try source location
97
+ const sourcePath = path.join(repoRoot, "practices-and-standards", "context.yaml");
98
+ if (fs.existsSync(sourcePath)) return sourcePath;
99
+
100
+ return null;
101
+ }
102
+
52
103
  function readYamlFile(filePath) {
53
104
  const raw = fs.readFileSync(filePath, "utf8");
54
105
  return YAML.parse(raw);
@@ -197,9 +248,192 @@ function cmdValidate(repoRoot, ctx) {
197
248
  process.exit(errors.length > 0 ? 1 : 0);
198
249
  }
199
250
 
251
+ // Helper function to test Stage5Assets dependencies individually
252
+ async function testStage5Dependencies() {
253
+ const dependencies = [
254
+ { name: "CursorRulesGenerator", path: "./infrastructure/assets/cursor/cursor-rules-generator.js" },
255
+ { name: "CursorSkillsGenerator", path: "./infrastructure/assets/cursor/cursor-skills-generator.js" },
256
+ { name: "ClaudeGenerator", path: "./infrastructure/assets/claude/claude-generator.js" },
257
+ { name: "SkillExtractor", path: "./infrastructure/parsing/skill-extractor.js" },
258
+ { name: "FileReader", path: "./infrastructure/file-system/file-reader.js" },
259
+ { name: "IAssetGenerator", path: "./application/ports/asset-generator.js" },
260
+ { name: "FileWriter", path: "./infrastructure/file-system/file-writer.js" },
261
+ { name: "MarkdownParser", path: "./infrastructure/parsing/markdown-parser.js" },
262
+ ];
263
+
264
+ console.log(" Testing Stage5Assets dependencies...");
265
+ for (const dep of dependencies) {
266
+ try {
267
+ console.log(` Testing dependency: ${dep.name} from ${dep.path}`);
268
+ const module = await import(dep.path);
269
+ const exportNames = Object.keys(module);
270
+ console.log(` ✓ Success - exports: ${exportNames.join(", ")}`);
271
+ if (!exportNames.includes(dep.name)) {
272
+ console.warn(` ⚠ Warning: Expected export '${dep.name}' not found`);
273
+ }
274
+ } catch (error) {
275
+ console.error(` ✗ FAILED: ${error.message}`);
276
+ console.error(` Error type: ${error.constructor.name}`);
277
+ if (error.code) {
278
+ console.error(` Error code: ${error.code}`);
279
+ }
280
+ if (error.stack) {
281
+ const stackLines = error.stack.split("\n").slice(0, 8);
282
+ console.error(` Stack trace:\n${stackLines.map(l => ` ${l}`).join("\n")}`);
283
+ }
284
+ throw new Error(`Failed to import ${dep.name} from ${dep.path}: ${error.message}`);
285
+ }
286
+ }
287
+ console.log(" ✓ All Stage5Assets dependencies successful");
288
+ }
289
+
290
+ // Helper function to test imports individually
291
+ async function testImports() {
292
+ const imports = [
293
+ { name: "Stage1Discovery", path: "./application/compile/stage1-discovery.js" },
294
+ { name: "Stage2Extraction", path: "./application/compile/stage2-extraction.js" },
295
+ { name: "Stage4Consolidation", path: "./application/compile/stage4-consolidation.js" },
296
+ { name: "Stage5Assets", path: "./application/compile/stage5-assets.js", testDependencies: true },
297
+ { name: "Stage6Validation", path: "./application/compile/stage6-validation.js" },
298
+ { name: "CompileContext", path: "./application/compile/compile-context.js" },
299
+ ];
300
+
301
+ console.log("Testing module imports...");
302
+ for (const imp of imports) {
303
+ try {
304
+ console.log(` Testing: ${imp.name} from ${imp.path}`);
305
+
306
+ // For Stage5Assets, test dependencies first
307
+ if (imp.testDependencies) {
308
+ try {
309
+ await testStage5Dependencies();
310
+ } catch (depError) {
311
+ console.error(` ✗ Dependency test failed before importing ${imp.name}`);
312
+ throw depError;
313
+ }
314
+ }
315
+
316
+ const module = await import(imp.path);
317
+ const exportNames = Object.keys(module);
318
+ console.log(` ✓ Success - exports: ${exportNames.join(", ")}`);
319
+ if (!exportNames.includes(imp.name)) {
320
+ console.warn(` ⚠ Warning: Expected export '${imp.name}' not found`);
321
+ }
322
+ } catch (error) {
323
+ console.error(` ✗ FAILED: ${error.message}`);
324
+ console.error(` Error type: ${error.constructor.name}`);
325
+ if (error.code) {
326
+ console.error(` Error code: ${error.code}`);
327
+ }
328
+ if (error.stack) {
329
+ const stackLines = error.stack.split("\n").slice(0, 10);
330
+ console.error(` Stack trace:\n${stackLines.map(l => ` ${l}`).join("\n")}`);
331
+ }
332
+ throw new Error(`Failed to import ${imp.name} from ${imp.path}: ${error.message}`);
333
+ }
334
+ }
335
+ console.log(" ✓ All imports successful\n");
336
+ }
337
+
338
+ async function cmdCompilePack(repoRoot, packId) {
339
+ // Test all imports first to isolate the failing module
340
+ try {
341
+ await testImports();
342
+ } catch (error) {
343
+ console.error("\n❌ Import test failed - this indicates a syntax or module resolution error");
344
+ die(`Import test failed: ${error.message}`);
345
+ }
346
+
347
+ // Now try to import the main module with detailed error handling
348
+ let CompileContext;
349
+ try {
350
+ console.log("Loading compilation module...");
351
+ const modulePath = "./application/compile/compile-context.js";
352
+ console.log(` Import path: ${modulePath}`);
353
+ console.log(` Current working directory: ${process.cwd()}`);
354
+
355
+ const module = await import(modulePath);
356
+ CompileContext = module.CompileContext;
357
+
358
+ if (!CompileContext) {
359
+ const availableExports = Object.keys(module).join(", ");
360
+ throw new Error(`CompileContext not exported from ${modulePath}. Available exports: ${availableExports || "(none)"}`);
361
+ }
362
+ console.log(" ✓ Module loaded successfully");
363
+ } catch (error) {
364
+ console.error("\n❌ Failed to load compilation module:");
365
+ console.error(` Error type: ${error.constructor.name}`);
366
+ console.error(` Error message: ${error.message}`);
367
+ if (error.code) {
368
+ console.error(` Error code: ${error.code}`);
369
+ if (error.code === "ERR_MODULE_NOT_FOUND") {
370
+ console.error(` This indicates the module file was not found at the specified path.`);
371
+ console.error(` Check if the file exists and the relative path is correct.`);
372
+ }
373
+ }
374
+ if (error.stack) {
375
+ const stackLines = error.stack.split("\n").slice(0, 15);
376
+ console.error(` Stack trace:\n${stackLines.map(l => ` ${l}`).join("\n")}`);
377
+ }
378
+ die(`Failed to load compilation module: ${error.message}`);
379
+ }
380
+
381
+ // Try to create compiler instance
382
+ let compiler;
383
+ try {
384
+ compiler = new CompileContext();
385
+ console.log(" ✓ Compiler instance created\n");
386
+ } catch (error) {
387
+ console.error("\n❌ Failed to create compiler instance:");
388
+ console.error(` Error: ${error.message}`);
389
+ if (error.stack) {
390
+ const stackLines = error.stack.split("\n").slice(0, 10);
391
+ console.error(` Stack trace:\n${stackLines.map(l => ` ${l}`).join("\n")}`);
392
+ }
393
+ die(`Failed to create compiler: ${error.message}`);
394
+ }
395
+
396
+ // Execute compilation
397
+ try {
398
+ const result = await compiler.compilePack(repoRoot, packId);
399
+
400
+ console.log("\n✅ Compilation successful!");
401
+ console.log(`\nOutput: ${result.outputPath}`);
402
+ console.log(`\nRules by category:`);
403
+ const categories = result.compilation.getCategories();
404
+ for (const category of categories) {
405
+ const rules = result.compilation.getRulesByCategory(category);
406
+ console.log(` ${category}: ${rules.length} rules`);
407
+ }
408
+
409
+ console.log(`\nTotal: ${result.compilation.rules.length} rules`);
410
+ console.log(` ${result.compilation.preferences.length} preferences`);
411
+ console.log(` ${result.compilation.scopes.length} scopes`);
412
+
413
+ console.log(`\nGenerated files:`);
414
+ console.log(` Consolidated: ${result.stageReports.stage4.filesGenerated.length} files`);
415
+ console.log(` Cursor rules: ${result.stageReports.stage5.results.cursorRules.filesGenerated.length} files`);
416
+ if (result.stageReports.stage5.skillsExtracted > 0) {
417
+ console.log(` Cursor skills: ${result.stageReports.stage5.results.cursorSkills.filesGenerated.length} files`);
418
+ }
419
+ console.log(` Claude.md: 1 file`);
420
+ console.log(` Reports: ${result.stageReports.stage6.reportsGenerated.length} files`);
421
+
422
+ } catch (error) {
423
+ console.error("\n❌ Compilation execution failed:");
424
+ console.error(` Error: ${error.message}`);
425
+ if (error.stack) {
426
+ const stackLines = error.stack.split("\n").slice(0, 15);
427
+ console.error(` Stack trace:\n${stackLines.map(l => ` ${l}`).join("\n")}`);
428
+ }
429
+ die(error.message);
430
+ }
431
+ }
432
+
200
433
  function cmdCompile(repoRoot, ctx) {
201
434
  const installed = orderInstalledPacks(ctx.installed_packs ?? [], ctx.precedence ?? []);
202
- const outDir = path.join(repoRoot, "practices-and-standards", ".compiled");
435
+ const psRoot = path.join(repoRoot, "docs", "practices-and-standards");
436
+ const outDir = path.join(psRoot, ".compiled");
203
437
  fs.mkdirSync(outDir, { recursive: true });
204
438
 
205
439
  const promptPath = path.join(outDir, "system-prompt.md");
@@ -351,7 +585,7 @@ async function fetchJson(url, token) {
351
585
  }
352
586
 
353
587
  function ensureContextInitialized(repoRoot, registryUrlMaybe) {
354
- const psRoot = path.join(repoRoot, "practices-and-standards");
588
+ const psRoot = path.join(repoRoot, "docs", "practices-and-standards");
355
589
  ensureDir(psRoot);
356
590
 
357
591
  const ctxPath = path.join(psRoot, "context.yaml");
@@ -395,7 +629,7 @@ function upsertInstalledPack(ctxObj, packId, version) {
395
629
  if (!Array.isArray(ctxObj.installed_packs)) ctxObj.installed_packs = [];
396
630
  if (!Array.isArray(ctxObj.precedence)) ctxObj.precedence = [];
397
631
 
398
- const manifest = `practices-and-standards/packs/${packId}/pack.yaml`;
632
+ const manifest = `docs/practices-and-standards/packs/${packId}/pack.yaml`;
399
633
  const existing = ctxObj.installed_packs.find(p => p.id === packId);
400
634
 
401
635
  if (existing) {
@@ -436,7 +670,7 @@ async function cmdPackInstall(repoRoot, packId, opts) {
436
670
  if (!token) die("Missing required --token for registry install.");
437
671
 
438
672
  const registry = opts.registry || ctx.registry;
439
- if (!registry) die("No registry configured. Provide --registry <url> or set registry: in practices-and-standards/context.yaml");
673
+ if (!registry) die("No registry configured. Provide --registry <url> or set registry: in docs/practices-and-standards/context.yaml");
440
674
 
441
675
  if (!version) {
442
676
  const latestUrl = `${registry.replace(/\/$/, "")}/packs/${encodeURIComponent(packId)}/latest`;
@@ -538,7 +772,7 @@ async function main() {
538
772
 
539
773
  if (cmd === "--version" || cmd === "-v" || cmd === "version") {
540
774
  // Make sure package.json has version (or hardcode a constant)
541
- console.log("0.1.2");
775
+ console.log("0.1.4");
542
776
  return;
543
777
  }
544
778
 
@@ -564,10 +798,35 @@ async function main() {
564
798
  }
565
799
 
566
800
  // Existing behavior: these require an existing context.yaml
567
- const repoRoot = findRepoContextRoot(process.cwd());
568
- if (!repoRoot) die("Could not find practices-and-standards/context.yaml in this directory or any parent.");
801
+ const contextYamlIndex = process.argv.indexOf("--context-yaml");
802
+
803
+ let repoRoot;
804
+ let contextPath;
805
+
806
+ if (contextYamlIndex !== -1 && process.argv[contextYamlIndex + 1]) {
807
+ // Explicit context.yaml path provided
808
+ contextPath = path.isAbsolute(process.argv[contextYamlIndex + 1])
809
+ ? process.argv[contextYamlIndex + 1]
810
+ : path.join(process.cwd(), process.argv[contextYamlIndex + 1]);
811
+
812
+ if (!fs.existsSync(contextPath)) {
813
+ die(`context.yaml not found at ${contextPath}`);
814
+ }
815
+
816
+ repoRoot = deriveRepoRootFromContextPath(contextPath);
817
+ } else {
818
+ // Auto-detect
819
+ repoRoot = findRepoContextRoot(process.cwd());
820
+ if (!repoRoot) {
821
+ die("Could not find context.yaml. Use --context-yaml to specify the path.");
822
+ }
823
+
824
+ contextPath = findContextYamlPath(repoRoot);
825
+ if (!contextPath) {
826
+ die("Could not find context.yaml in expected locations.");
827
+ }
828
+ }
569
829
 
570
- const contextPath = path.join(repoRoot, "practices-and-standards", "context.yaml");
571
830
  const ctx = readYamlFile(contextPath);
572
831
 
573
832
  if (!ctx?.schema || ctx.schema !== "context-install/v1") {
@@ -576,7 +835,13 @@ async function main() {
576
835
 
577
836
  switch (cmd) {
578
837
  case "list": return cmdList(repoRoot, ctx);
579
- case "compile": return cmdCompile(repoRoot, ctx);
838
+ case "compile":
839
+ // Check for --pack flag
840
+ const packIndex = process.argv.indexOf("--pack");
841
+ if (packIndex !== -1 && process.argv[packIndex + 1]) {
842
+ return cmdCompilePack(repoRoot, process.argv[packIndex + 1]);
843
+ }
844
+ return cmdCompile(repoRoot, ctx);
580
845
  case "validate": return cmdValidate(repoRoot, ctx);
581
846
  default:
582
847
  console.log(`Unknown command: ${cmd}\n`);
@@ -0,0 +1,77 @@
1
+ import { Rule } from "./rule.js";
2
+ import { Preference } from "./preference.js";
3
+ import { Scope } from "./scope.js";
4
+
5
+ /**
6
+ * Compilation aggregate root
7
+ * Orchestrates the compilation process and holds all extracted data
8
+ */
9
+ export class Compilation {
10
+ constructor() {
11
+ this.rules = [];
12
+ this.preferences = [];
13
+ this.scopes = [];
14
+ this.sourceFiles = [];
15
+ this.packs = [];
16
+ this.metadata = {
17
+ timestamp: new Date().toISOString(),
18
+ version: "1.0.0"
19
+ };
20
+ }
21
+
22
+ /**
23
+ * Add a rule to the compilation
24
+ * @param {Rule} rule - Rule to add
25
+ */
26
+ addRule(rule) {
27
+ // Check for duplicate IDs
28
+ const existing = this.rules.find(r => r.id === rule.id);
29
+ if (existing) {
30
+ throw new Error(`Duplicate rule ID: ${rule.id} in ${rule.source.file} (already exists from ${existing.source.file})`);
31
+ }
32
+ this.rules.push(rule);
33
+ }
34
+
35
+ /**
36
+ * Add a preference to the compilation
37
+ * @param {Preference} preference - Preference to add
38
+ */
39
+ addPreference(preference) {
40
+ this.preferences.push(preference);
41
+ }
42
+
43
+ /**
44
+ * Add a scope to the compilation
45
+ * @param {Scope} scope - Scope to add
46
+ */
47
+ addScope(scope) {
48
+ this.scopes.push(scope);
49
+ }
50
+
51
+ /**
52
+ * Get rules by category
53
+ * @param {string} category - Category to filter by
54
+ * @returns {Rule[]} Rules in that category
55
+ */
56
+ getRulesByCategory(category) {
57
+ return this.rules.filter(r => r.category === category);
58
+ }
59
+
60
+ /**
61
+ * Get all categories
62
+ * @returns {string[]} Unique categories
63
+ */
64
+ getCategories() {
65
+ return [...new Set(this.rules.map(r => r.category))].sort();
66
+ }
67
+
68
+ /**
69
+ * Get rules by level
70
+ * @param {string} level - Level to filter by
71
+ * @returns {Rule[]} Rules with that level
72
+ */
73
+ getRulesByLevel(level) {
74
+ return this.rules.filter(r => r.level === level);
75
+ }
76
+ }
77
+
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Preference value object
3
+ * Represents a preference extracted from documentation
4
+ */
5
+ export class Preference {
6
+ /**
7
+ * @param {Object} data
8
+ * @param {string} data.name - Preference name
9
+ * @param {string} [data.description] - Preference description
10
+ * @param {Object} data.source - Source information
11
+ * @param {string} data.source.pack - Pack ID
12
+ * @param {string} data.source.packVersion - Pack version
13
+ * @param {string} data.source.file - Source file path
14
+ * @param {number} data.source.line - Line number
15
+ * @param {number} data.source.precedence - Precedence index
16
+ */
17
+ constructor(data) {
18
+ this.name = data.name;
19
+ this.description = data.description;
20
+ this.source = data.source;
21
+ }
22
+ }
23
+