@zebralabs/context-cli 0.1.3 → 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.
- package/package.json +1 -1
- package/src/application/compile/compile-context.js +119 -0
- package/src/application/compile/stage1-discovery.js +125 -0
- package/src/application/compile/stage2-extraction.js +97 -0
- package/src/application/compile/stage4-consolidation.js +235 -0
- package/src/application/compile/stage5-assets.js +61 -0
- package/src/application/compile/stage6-validation.js +133 -0
- package/src/application/ports/asset-generator.js +33 -0
- package/src/context.js +274 -10
- package/src/domain/compilation.js +77 -0
- package/src/domain/preference.js +23 -0
- package/src/domain/rule.js +71 -0
- package/src/domain/scope.js +30 -0
- package/src/infrastructure/assets/claude/claude-generator.js +95 -0
- package/src/infrastructure/assets/cursor/cursor-rules-generator.js +119 -0
- package/src/infrastructure/assets/cursor/cursor-skills-generator.js +115 -0
- package/src/infrastructure/file-system/file-reader.js +67 -0
- package/src/infrastructure/file-system/file-writer.js +40 -0
- package/src/infrastructure/parsing/markdown-parser.js +95 -0
- package/src/infrastructure/parsing/rule-extractor.js +219 -0
- package/src/infrastructure/parsing/skill-extractor.js +74 -0
|
@@ -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,6 +32,9 @@ 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
40
|
- Installer merges into <repo-root>/docs/practices-and-standards/
|
|
@@ -41,14 +44,62 @@ Notes:
|
|
|
41
44
|
function findRepoContextRoot(startDir) {
|
|
42
45
|
let dir = startDir;
|
|
43
46
|
while (true) {
|
|
44
|
-
|
|
45
|
-
|
|
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,6 +248,188 @@ 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
435
|
const psRoot = path.join(repoRoot, "docs", "practices-and-standards");
|
|
@@ -539,7 +772,7 @@ async function main() {
|
|
|
539
772
|
|
|
540
773
|
if (cmd === "--version" || cmd === "-v" || cmd === "version") {
|
|
541
774
|
// Make sure package.json has version (or hardcode a constant)
|
|
542
|
-
console.log("0.1.
|
|
775
|
+
console.log("0.1.4");
|
|
543
776
|
return;
|
|
544
777
|
}
|
|
545
778
|
|
|
@@ -565,10 +798,35 @@ async function main() {
|
|
|
565
798
|
}
|
|
566
799
|
|
|
567
800
|
// Existing behavior: these require an existing context.yaml
|
|
568
|
-
const
|
|
569
|
-
|
|
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
|
+
}
|
|
570
829
|
|
|
571
|
-
const contextPath = path.join(repoRoot, "docs", "practices-and-standards", "context.yaml");
|
|
572
830
|
const ctx = readYamlFile(contextPath);
|
|
573
831
|
|
|
574
832
|
if (!ctx?.schema || ctx.schema !== "context-install/v1") {
|
|
@@ -577,7 +835,13 @@ async function main() {
|
|
|
577
835
|
|
|
578
836
|
switch (cmd) {
|
|
579
837
|
case "list": return cmdList(repoRoot, ctx);
|
|
580
|
-
case "compile":
|
|
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);
|
|
581
845
|
case "validate": return cmdValidate(repoRoot, ctx);
|
|
582
846
|
default:
|
|
583
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
|
+
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule value object
|
|
3
|
+
* Represents a single rule extracted from documentation
|
|
4
|
+
*/
|
|
5
|
+
export class Rule {
|
|
6
|
+
/**
|
|
7
|
+
* @param {Object} data
|
|
8
|
+
* @param {string} data.id - Rule ID (e.g., "doc-structure-001")
|
|
9
|
+
* @param {string} data.level - Rule level: "must" | "should" | "prefer" | "avoid"
|
|
10
|
+
* @param {string} data.category - Category extracted from rule ID prefix
|
|
11
|
+
* @param {string[]} data.appliesTo - What this rule applies to
|
|
12
|
+
* @param {string} data.rule - The rule statement
|
|
13
|
+
* @param {string} [data.rationale] - Why this rule exists
|
|
14
|
+
* @param {Object} data.source - Source information
|
|
15
|
+
* @param {string} data.source.pack - Pack ID
|
|
16
|
+
* @param {string} data.source.packVersion - Pack version
|
|
17
|
+
* @param {string} data.source.file - Source file path
|
|
18
|
+
* @param {number} data.source.line - Line number where rule starts
|
|
19
|
+
* @param {number} data.source.precedence - Precedence index (lower = higher priority)
|
|
20
|
+
*/
|
|
21
|
+
constructor(data) {
|
|
22
|
+
this.id = data.id;
|
|
23
|
+
this.level = data.level;
|
|
24
|
+
this.category = data.category;
|
|
25
|
+
this.appliesTo = data.appliesTo;
|
|
26
|
+
this.rule = data.rule;
|
|
27
|
+
this.rationale = data.rationale;
|
|
28
|
+
this.source = data.source;
|
|
29
|
+
this.conflicts = []; // Populated during conflict resolution
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Extract category from rule ID
|
|
34
|
+
* @param {string} ruleId - Rule ID (e.g., "doc-structure-001")
|
|
35
|
+
* @returns {string} Category (e.g., "doc-structure")
|
|
36
|
+
*/
|
|
37
|
+
static extractCategory(ruleId) {
|
|
38
|
+
const match = ruleId.match(/^([a-z0-9-]+)-\d+$/);
|
|
39
|
+
return match ? match[1] : "unknown";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Validate rule level
|
|
44
|
+
* @param {string} level - Level to validate
|
|
45
|
+
* @returns {boolean} True if valid
|
|
46
|
+
*/
|
|
47
|
+
static isValidLevel(level) {
|
|
48
|
+
return ["must", "should", "prefer", "avoid"].includes(level);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Validate rule ID format
|
|
53
|
+
* @param {string} ruleId - Rule ID to validate
|
|
54
|
+
* @returns {boolean} True if valid
|
|
55
|
+
*/
|
|
56
|
+
static isValidId(ruleId) {
|
|
57
|
+
return /^[a-z0-9-]+$/.test(ruleId);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Check if this rule applies to the given scope
|
|
62
|
+
* @param {string|string[]} scope - Scope to check
|
|
63
|
+
* @returns {boolean} True if rule applies
|
|
64
|
+
*/
|
|
65
|
+
appliesToScope(scope) {
|
|
66
|
+
if (this.appliesTo.includes("all")) return true;
|
|
67
|
+
const scopes = Array.isArray(scope) ? scope : [scope];
|
|
68
|
+
return scopes.some(s => this.appliesTo.includes(s));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|