@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,219 @@
1
+ import { Rule } from "../../domain/rule.js";
2
+ import { Preference } from "../../domain/preference.js";
3
+ import { Scope } from "../../domain/scope.js";
4
+ import { MarkdownParser } from "./markdown-parser.js";
5
+
6
+ /**
7
+ * Rule extractor adapter
8
+ * Extracts rules, preferences, and scope from markdown files
9
+ */
10
+ export class RuleExtractor {
11
+ constructor() {
12
+ this.parser = new MarkdownParser();
13
+ }
14
+
15
+ /**
16
+ * Extract rules from markdown content
17
+ * @param {string} content - Markdown content
18
+ * @param {Object} source - Source information
19
+ * @returns {Rule[]} Extracted rules
20
+ */
21
+ extractRules(content, source) {
22
+ const rules = [];
23
+ const rulesSection = this.parser.findSection(content, "## @context.rules");
24
+
25
+ if (!rulesSection) return rules;
26
+
27
+ const lines = rulesSection.split("\n");
28
+ let currentRule = null;
29
+
30
+ for (let i = 0; i < lines.length; i++) {
31
+ const line = lines[i].trim();
32
+
33
+ // Rule header: ### rule-id: Title
34
+ const ruleHeaderMatch = line.match(/^###\s+([a-z0-9-]+):\s*(.+)$/);
35
+ if (ruleHeaderMatch) {
36
+ // Save previous rule if exists
37
+ if (currentRule) {
38
+ if (currentRule.rule) {
39
+ rules.push(this.createRule(currentRule, source, content));
40
+ }
41
+ }
42
+
43
+ // Start new rule
44
+ currentRule = {
45
+ id: ruleHeaderMatch[1],
46
+ title: ruleHeaderMatch[2],
47
+ level: null,
48
+ appliesTo: null,
49
+ rule: null,
50
+ rationale: null
51
+ };
52
+ continue;
53
+ }
54
+
55
+ if (!currentRule) continue;
56
+
57
+ // Parse rule fields
58
+ const levelMatch = line.match(/^-\s*\*\*Level:\*\*\s*(.+)$/);
59
+ if (levelMatch) {
60
+ currentRule.level = levelMatch[1].trim();
61
+ continue;
62
+ }
63
+
64
+ const appliesToMatch = line.match(/^-\s*\*\*Applies to:\*\*\s*(.+)$/);
65
+ if (appliesToMatch) {
66
+ const appliesToStr = appliesToMatch[1].trim();
67
+ currentRule.appliesTo = appliesToStr === "all"
68
+ ? ["all"]
69
+ : appliesToStr.split(",").map(s => s.trim());
70
+ continue;
71
+ }
72
+
73
+ const ruleMatch = line.match(/^-\s*\*\*Rule:\*\*\s*(.+)$/);
74
+ if (ruleMatch) {
75
+ currentRule.rule = ruleMatch[1].trim();
76
+ continue;
77
+ }
78
+
79
+ const rationaleMatch = line.match(/^-\s*\*\*Rationale:\*\*\s*(.+)$/);
80
+ if (rationaleMatch) {
81
+ currentRule.rationale = rationaleMatch[1].trim();
82
+ continue;
83
+ }
84
+ }
85
+
86
+ // Don't forget the last rule
87
+ if (currentRule && currentRule.rule) {
88
+ rules.push(this.createRule(currentRule, source, content));
89
+ }
90
+
91
+ return rules;
92
+ }
93
+
94
+ /**
95
+ * Create a Rule object from extracted data
96
+ * @param {Object} data - Extracted rule data
97
+ * @param {Object} source - Source information
98
+ * @param {string} content - Full content for line number calculation
99
+ * @returns {Rule} Rule object
100
+ */
101
+ createRule(data, source, content) {
102
+ // Validate level
103
+ if (!Rule.isValidLevel(data.level)) {
104
+ console.warn(`Invalid level "${data.level}" for rule ${data.id}, defaulting to "should"`);
105
+ data.level = "should";
106
+ }
107
+
108
+ // Validate rule ID
109
+ if (!Rule.isValidId(data.id)) {
110
+ throw new Error(`Invalid rule ID format: ${data.id} (must be lowercase, numbers, hyphens only)`);
111
+ }
112
+
113
+ // Get line number
114
+ const lineNumber = this.parser.getLineNumber(content, `### ${data.id}:`);
115
+
116
+ return new Rule({
117
+ id: data.id,
118
+ level: data.level,
119
+ category: Rule.extractCategory(data.id),
120
+ appliesTo: data.appliesTo || [],
121
+ rule: data.rule,
122
+ rationale: data.rationale,
123
+ source: {
124
+ pack: source.pack,
125
+ packVersion: source.packVersion,
126
+ file: source.file,
127
+ line: lineNumber,
128
+ precedence: source.precedence
129
+ }
130
+ });
131
+ }
132
+
133
+ /**
134
+ * Extract preferences from markdown content
135
+ * @param {string} content - Markdown content
136
+ * @param {Object} source - Source information
137
+ * @returns {Preference[]} Extracted preferences
138
+ */
139
+ extractPreferences(content, source) {
140
+ const preferences = [];
141
+ const prefsSection = this.parser.findSection(content, "## @context.preferences");
142
+
143
+ if (!prefsSection) return preferences;
144
+
145
+ const lines = prefsSection.split("\n");
146
+ for (const line of lines) {
147
+ const match = line.match(/^-\s*\*\*prefer:\*\*\s*(.+?)(?:\s*-\s*(.+))?$/);
148
+ if (match) {
149
+ const name = match[1].trim();
150
+ const description = match[2] ? match[2].trim() : null;
151
+ const lineNumber = this.parser.getLineNumber(content, line.trim());
152
+
153
+ preferences.push(new Preference({
154
+ name,
155
+ description,
156
+ source: {
157
+ pack: source.pack,
158
+ packVersion: source.packVersion,
159
+ file: source.file,
160
+ line: lineNumber,
161
+ precedence: source.precedence
162
+ }
163
+ }));
164
+ }
165
+ }
166
+
167
+ return preferences;
168
+ }
169
+
170
+ /**
171
+ * Extract scope from markdown content
172
+ * @param {string} content - Markdown content
173
+ * @param {Object} source - Source information
174
+ * @returns {Scope|null} Extracted scope or null
175
+ */
176
+ extractScope(content, source) {
177
+ const scopeSection = this.parser.findSection(content, "## @context.scope");
178
+
179
+ if (!scopeSection) return null;
180
+
181
+ const appliesTo = [];
182
+ const doesNotApplyTo = [];
183
+ let inAppliesTo = false;
184
+ let inDoesNotApplyTo = false;
185
+
186
+ const lines = scopeSection.split("\n");
187
+ for (const line of lines) {
188
+ if (line.includes("**Applies to:**")) {
189
+ inAppliesTo = true;
190
+ inDoesNotApplyTo = false;
191
+ continue;
192
+ }
193
+ if (line.includes("**Does not apply to:**")) {
194
+ inAppliesTo = false;
195
+ inDoesNotApplyTo = true;
196
+ continue;
197
+ }
198
+
199
+ const listMatch = line.match(/^-\s*(.+)$/);
200
+ if (listMatch) {
201
+ const item = listMatch[1].trim();
202
+ if (inAppliesTo) {
203
+ appliesTo.push(item);
204
+ } else if (inDoesNotApplyTo) {
205
+ doesNotApplyTo.push(item);
206
+ }
207
+ }
208
+ }
209
+
210
+ return new Scope({
211
+ appliesTo,
212
+ doesNotApplyTo,
213
+ source: {
214
+ file: source.file
215
+ }
216
+ });
217
+ }
218
+ }
219
+
@@ -0,0 +1,74 @@
1
+ import { MarkdownParser } from "./markdown-parser.js";
2
+
3
+ /**
4
+ * Skill extractor adapter
5
+ * Extracts skill definitions from @context.skills sections
6
+ */
7
+ export class SkillExtractor {
8
+ constructor() {
9
+ this.parser = new MarkdownParser();
10
+ }
11
+
12
+ /**
13
+ * Extracts skill definitions from markdown content.
14
+ * @param {string} markdown - The markdown content.
15
+ * @param {Object} sourceMetadata - Metadata about the source file (packId, file, etc.).
16
+ * @returns {Object[]} Extracted skill definitions.
17
+ */
18
+ extractSkills(markdown, sourceMetadata) {
19
+ const skills = [];
20
+ const skillsSection = this.parser.findSection(markdown, "## @context.skills");
21
+
22
+ if (!skillsSection) return skills;
23
+
24
+ const skillBlocks = skillsSection.split(/\r?\n### /).filter(Boolean);
25
+ for (const skillText of skillBlocks) {
26
+ try {
27
+ const skill = this._parseSkill(skillText, sourceMetadata);
28
+ if (skill) skills.push(skill);
29
+ } catch (e) {
30
+ console.warn(`Warning: Malformed skill in ${sourceMetadata.file}: ${e.message}`);
31
+ }
32
+ }
33
+ return skills;
34
+ }
35
+
36
+ _parseSkill(skillText, sourceMetadata) {
37
+ const lines = skillText.split(/\r?\n/);
38
+ // Handle first line - might have "### " prefix or not
39
+ const firstLine = lines[0].trim();
40
+ const nameMatch = firstLine.match(/^(?:###\s+)?skill-([a-z0-9-]+)$/i);
41
+ if (!nameMatch) throw new Error(`Invalid skill ID format: ${firstLine}`);
42
+
43
+ const id = nameMatch[1].toLowerCase();
44
+ const skill = {
45
+ id: id,
46
+ name: id, // Default name to id, can be overridden by explicit 'Name' field
47
+ description: '',
48
+ rules: [],
49
+ whenToUse: [],
50
+ processReference: '',
51
+ source: sourceMetadata
52
+ };
53
+
54
+ for (let i = 1; i < lines.length; i++) {
55
+ const line = lines[i].trim();
56
+ if (line.startsWith('- **Name:**')) {
57
+ skill.name = line.replace('- **Name:**', '').trim();
58
+ } else if (line.startsWith('- **Description:**')) {
59
+ skill.description = line.replace('- **Description:**', '').trim();
60
+ } else if (line.startsWith('- **Rules:**')) {
61
+ skill.rules = line.replace('- **Rules:**', '').split(',').map(s => s.trim()).filter(Boolean);
62
+ } else if (line.startsWith('- **When to use:**')) {
63
+ skill.whenToUse = line.replace('- **When to use:**', '').split(',').map(s => s.trim()).filter(Boolean);
64
+ } else if (line.startsWith('- **Process reference:**')) {
65
+ skill.processReference = line.replace('- **Process reference:**', '').trim();
66
+ }
67
+ }
68
+
69
+ if (!skill.description) throw new Error(`Skill '${skill.id}' is missing a description.`);
70
+
71
+ return skill;
72
+ }
73
+ }
74
+