universal-dev-standards 3.5.0-beta.11 → 3.5.0-beta.12

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "universal-dev-standards",
3
- "version": "3.5.0-beta.11",
3
+ "version": "3.5.0-beta.12",
4
4
  "description": "CLI tool for adopting Universal Development Standards",
5
5
  "keywords": [
6
6
  "documentation",
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Conversion Rules for Markdown to AI-YAML
3
+ *
4
+ * Defines mappings, naming conventions, and transformation rules
5
+ * for converting core standards to AI-optimized format.
6
+ */
7
+
8
+ /**
9
+ * Standard ID mappings
10
+ * Maps source filename (without .md) to target YAML id
11
+ */
12
+ export const STANDARD_ID_MAPPING = {
13
+ 'commit-message-guide': 'commit-message',
14
+ 'changelog-standards': 'changelog',
15
+ 'code-review-checklist': 'code-review',
16
+ 'testing-standards': 'testing',
17
+ 'git-workflow': 'git-workflow',
18
+ 'documentation-structure': 'documentation',
19
+ 'checkin-standards': 'checkin',
20
+ 'anti-hallucination': 'anti-hallucination',
21
+ 'versioning': 'versioning',
22
+ 'test-driven-development': 'tdd',
23
+ 'spec-driven-development': 'sdd',
24
+ 'test-completeness-dimensions': 'test-completeness',
25
+ 'logging-standards': 'logging',
26
+ 'error-code-standards': 'error-code',
27
+ 'refactoring-standards': 'refactoring',
28
+ 'graceful-failure': 'graceful-failure',
29
+ 'release-workflow': 'release-workflow'
30
+ };
31
+
32
+ /**
33
+ * Section name mappings
34
+ * Maps Markdown section headers to YAML keys
35
+ */
36
+ export const SECTION_MAPPINGS = {
37
+ 'Purpose': 'meta.description',
38
+ 'Basic Format': 'format',
39
+ 'Type Classification': 'types',
40
+ 'Scope Guidelines': 'scopes',
41
+ 'Subject Line': 'subject',
42
+ 'Body': 'body',
43
+ 'Footer': 'footer',
44
+ 'Complete Examples': 'examples',
45
+ 'Anti-Patterns': 'anti_patterns',
46
+ 'Automation and Tooling': 'tooling',
47
+ 'Project Configuration Template': 'configuration',
48
+ 'Testing Pyramid': 'pyramid',
49
+ 'Test Categories': 'categories',
50
+ 'Commit Types': 'types',
51
+ 'Branch Naming': 'branches',
52
+ 'Merge Strategy': 'merge_strategy',
53
+ 'Quick Reference': 'quick_reference'
54
+ };
55
+
56
+ /**
57
+ * Priority inference patterns
58
+ * Maps text patterns to priority levels
59
+ */
60
+ export const PRIORITY_PATTERNS = [
61
+ { pattern: /\b(MUST|CRITICAL|REQUIRED|ALWAYS)\b/i, priority: 'required' },
62
+ { pattern: /\b(SHOULD|RECOMMENDED|PREFER)\b/i, priority: 'recommended' },
63
+ { pattern: /\b(MAY|OPTIONAL|CONSIDER)\b/i, priority: 'optional' },
64
+ { pattern: /\b(NEVER|MUST NOT|DO NOT)\b/i, priority: 'required' }
65
+ ];
66
+
67
+ /**
68
+ * Infer priority from text content
69
+ * @param {string} text - Text to analyze
70
+ * @returns {string} Priority level (required, recommended, optional)
71
+ */
72
+ export function inferPriority(text) {
73
+ for (const { pattern, priority } of PRIORITY_PATTERNS) {
74
+ if (pattern.test(text)) {
75
+ return priority;
76
+ }
77
+ }
78
+ return 'recommended'; // Default
79
+ }
80
+
81
+ /**
82
+ * Trigger patterns by section type
83
+ */
84
+ export const TRIGGER_MAPPINGS = {
85
+ 'commit-message': 'writing commit message',
86
+ 'testing': 'writing tests',
87
+ 'code-review': 'reviewing code',
88
+ 'git-workflow': 'managing branches',
89
+ 'documentation': 'writing documentation',
90
+ 'changelog': 'updating changelog',
91
+ 'versioning': 'determining version number',
92
+ 'checkin': 'committing code',
93
+ 'anti-hallucination': 'collaborating with AI',
94
+ 'tdd': 'developing with TDD',
95
+ 'sdd': 'developing with specs',
96
+ 'logging': 'adding logging',
97
+ 'error-code': 'defining error codes',
98
+ 'refactoring': 'refactoring code',
99
+ 'graceful-failure': 'handling failures'
100
+ };
101
+
102
+ /**
103
+ * Get trigger text for a standard
104
+ * @param {string} standardId - Standard identifier
105
+ * @returns {string} Trigger description
106
+ */
107
+ export function getTrigger(standardId) {
108
+ return TRIGGER_MAPPINGS[standardId] || 'applying this standard';
109
+ }
110
+
111
+ /**
112
+ * Sections to skip during conversion
113
+ * These are metadata sections that don't need YAML representation
114
+ */
115
+ export const SKIP_SECTIONS = [
116
+ 'Version History',
117
+ 'References',
118
+ 'License',
119
+ 'Related Standards'
120
+ ];
121
+
122
+ /**
123
+ * Check if a section should be skipped
124
+ * @param {string} sectionName - Section name
125
+ * @returns {boolean} Whether to skip
126
+ */
127
+ export function shouldSkipSection(sectionName) {
128
+ return SKIP_SECTIONS.some(skip =>
129
+ sectionName.toLowerCase().includes(skip.toLowerCase())
130
+ );
131
+ }
132
+
133
+ /**
134
+ * Validator patterns for common rules
135
+ */
136
+ export const VALIDATORS = {
137
+ 'commit-subject': '^.{1,72}$',
138
+ 'scope': '^[a-z][a-z0-9-]*$',
139
+ 'version': '^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.]+)?$',
140
+ 'branch-name': '^(feature|fix|docs|chore|hotfix)/[a-z0-9-]+$',
141
+ 'commit-type': '^(feat|fix|docs|style|refactor|test|perf|build|ci|chore|revert|security)$'
142
+ };
143
+
144
+ /**
145
+ * Get validator for a rule type
146
+ * @param {string} ruleType - Type of rule
147
+ * @returns {string|null} Regex validator or null
148
+ */
149
+ export function getValidator(ruleType) {
150
+ return VALIDATORS[ruleType] || null;
151
+ }
152
+
153
+ /**
154
+ * Locale configurations
155
+ */
156
+ export const LOCALE_CONFIG = {
157
+ 'zh-TW': {
158
+ name: 'Traditional Chinese',
159
+ direction: 'ltr',
160
+ coreDir: 'locales/zh-TW/core',
161
+ aiDir: 'locales/zh-TW/ai/standards'
162
+ },
163
+ 'zh-CN': {
164
+ name: 'Simplified Chinese',
165
+ direction: 'ltr',
166
+ coreDir: 'locales/zh-CN/core',
167
+ aiDir: 'locales/zh-CN/ai/standards'
168
+ }
169
+ };
170
+
171
+ /**
172
+ * Get locale configuration
173
+ * @param {string} locale - Locale code
174
+ * @returns {Object|null} Locale config or null
175
+ */
176
+ export function getLocaleConfig(locale) {
177
+ return LOCALE_CONFIG[locale] || null;
178
+ }
179
+
180
+ /**
181
+ * Get all supported locales
182
+ * @returns {string[]} Array of locale codes
183
+ */
184
+ export function getSupportedLocales() {
185
+ return Object.keys(LOCALE_CONFIG);
186
+ }
187
+
188
+ /**
189
+ * Output path mappings
190
+ * Maps source patterns to output directories
191
+ */
192
+ export const OUTPUT_PATHS = {
193
+ 'core': 'ai/standards',
194
+ 'locales/zh-TW/core': 'locales/zh-TW/ai/standards',
195
+ 'locales/zh-CN/core': 'locales/zh-CN/ai/standards'
196
+ };
197
+
198
+ /**
199
+ * Get output directory for a source path
200
+ * @param {string} sourcePath - Source file path
201
+ * @returns {string} Output directory
202
+ */
203
+ export function getOutputDir(sourcePath) {
204
+ // Check longer patterns first (locales before core)
205
+ // Sort by pattern length descending to ensure more specific matches first
206
+ const sortedEntries = Object.entries(OUTPUT_PATHS)
207
+ .sort((a, b) => b[0].length - a[0].length);
208
+
209
+ for (const [pattern, outputDir] of sortedEntries) {
210
+ if (sourcePath.includes(pattern)) {
211
+ return outputDir;
212
+ }
213
+ }
214
+ return 'ai/standards';
215
+ }
216
+
217
+ /**
218
+ * Generate output filename from source filename
219
+ * @param {string} sourceFilename - Source .md filename
220
+ * @returns {string} Output .ai.yaml filename
221
+ */
222
+ export function getOutputFilename(sourceFilename) {
223
+ // Get the base name without .md
224
+ let baseName = sourceFilename.replace(/\.md$/, '');
225
+
226
+ // Map to standard ID if available
227
+ if (STANDARD_ID_MAPPING[baseName]) {
228
+ baseName = STANDARD_ID_MAPPING[baseName];
229
+ }
230
+
231
+ return `${baseName}.ai.yaml`;
232
+ }
@@ -117,6 +117,8 @@ export function detectFramework(projectPath) {
117
117
  * @returns {Object} Detected AI tools
118
118
  */
119
119
  export function detectAITools(projectPath) {
120
+ const hasAgentsMd = existsSync(join(projectPath, 'AGENTS.md'));
121
+
120
122
  const detected = {
121
123
  cursor: existsSync(join(projectPath, '.cursorrules')),
122
124
  windsurf: existsSync(join(projectPath, '.windsurfrules')),
@@ -124,7 +126,10 @@ export function detectAITools(projectPath) {
124
126
  copilot: existsSync(join(projectPath, '.github', 'copilot-instructions.md')),
125
127
  claudeCode: existsSync(join(projectPath, '.claude')) ||
126
128
  existsSync(join(projectPath, 'CLAUDE.md')),
127
- antigravity: existsSync(join(projectPath, 'INSTRUCTIONS.md'))
129
+ antigravity: existsSync(join(projectPath, 'INSTRUCTIONS.md')),
130
+ codex: hasAgentsMd,
131
+ opencode: hasAgentsMd,
132
+ geminiCli: existsSync(join(projectPath, 'GEMINI.md'))
128
133
  };
129
134
 
130
135
  return detected;
@@ -0,0 +1,429 @@
1
+ /**
2
+ * Markdown Parser for AI-YAML Conversion
3
+ *
4
+ * Parses core standard Markdown files into a structured representation
5
+ * that can be converted to AI-optimized YAML format.
6
+ */
7
+
8
+ /**
9
+ * Parse Markdown content into structured representation
10
+ * @param {string} content - Raw Markdown content
11
+ * @returns {Object} Parsed structure
12
+ */
13
+ export function parseMarkdown(content) {
14
+ const lines = content.split('\n');
15
+
16
+ return {
17
+ metadata: extractMetadata(lines),
18
+ purpose: extractPurpose(content),
19
+ sections: extractSections(content),
20
+ tables: extractTables(content),
21
+ codeBlocks: extractCodeBlocks(content),
22
+ decisionPoints: extractDecisionPoints(content),
23
+ relatedStandards: extractRelatedStandards(content),
24
+ versionHistory: extractVersionHistory(content)
25
+ };
26
+ }
27
+
28
+ /**
29
+ * Extract metadata from the header lines
30
+ * Format expected:
31
+ * Line 1: # Title
32
+ * Line 3: > **Language**: English | [繁體中文](...)
33
+ * Line 5: **Version**: X.Y.Z
34
+ * Line 6: **Last Updated**: YYYY-MM-DD
35
+ * Line 7: **Applicability**: ...
36
+ */
37
+ function extractMetadata(lines) {
38
+ const metadata = {
39
+ title: '',
40
+ version: '',
41
+ updated: '',
42
+ applicability: '',
43
+ languageLink: null
44
+ };
45
+
46
+ for (let i = 0; i < Math.min(lines.length, 15); i++) {
47
+ const line = lines[i];
48
+
49
+ // Title: # Title
50
+ if (line.startsWith('# ') && !metadata.title) {
51
+ metadata.title = line.substring(2).trim();
52
+ }
53
+
54
+ // Language link: > **Language**: English | [繁體中文](...)
55
+ const langMatch = line.match(/>\s*\*\*Language\*\*:\s*(.+)/);
56
+ if (langMatch) {
57
+ const linkMatch = langMatch[1].match(/\[(.+?)\]\((.+?)\)/);
58
+ if (linkMatch) {
59
+ metadata.languageLink = {
60
+ text: linkMatch[1],
61
+ path: linkMatch[2]
62
+ };
63
+ }
64
+ }
65
+
66
+ // Version: **Version**: X.Y.Z
67
+ const versionMatch = line.match(/\*\*Version\*\*:\s*(.+)/);
68
+ if (versionMatch) {
69
+ metadata.version = versionMatch[1].trim();
70
+ }
71
+
72
+ // Last Updated: **Last Updated**: YYYY-MM-DD
73
+ const updatedMatch = line.match(/\*\*Last Updated\*\*:\s*(.+)/);
74
+ if (updatedMatch) {
75
+ metadata.updated = updatedMatch[1].trim();
76
+ }
77
+
78
+ // Applicability: **Applicability**: ...
79
+ const applicabilityMatch = line.match(/\*\*Applicability\*\*:\s*(.+)/);
80
+ if (applicabilityMatch) {
81
+ metadata.applicability = applicabilityMatch[1].trim();
82
+ }
83
+ }
84
+
85
+ return metadata;
86
+ }
87
+
88
+ /**
89
+ * Extract the Purpose section content
90
+ */
91
+ function extractPurpose(content) {
92
+ const purposeMatch = content.match(/## Purpose\s*\n+([\s\S]*?)(?=\n---|\n## )/);
93
+ if (purposeMatch) {
94
+ return purposeMatch[1].trim();
95
+ }
96
+ return '';
97
+ }
98
+
99
+ /**
100
+ * Extract all ## sections with their content
101
+ */
102
+ function extractSections(content) {
103
+ const sections = [];
104
+ const sectionRegex = /^## (.+)$/gm;
105
+ const matches = [...content.matchAll(sectionRegex)];
106
+
107
+ for (let i = 0; i < matches.length; i++) {
108
+ const match = matches[i];
109
+ const name = match[1].trim();
110
+ const startIdx = match.index + match[0].length;
111
+ const endIdx = matches[i + 1] ? matches[i + 1].index : content.length;
112
+
113
+ let sectionContent = content.substring(startIdx, endIdx).trim();
114
+
115
+ // Remove leading --- if present
116
+ sectionContent = sectionContent.replace(/^---\s*/, '').trim();
117
+
118
+ // Classify section type
119
+ const type = classifySectionType(name, sectionContent);
120
+
121
+ sections.push({
122
+ name,
123
+ content: sectionContent,
124
+ type,
125
+ subsections: extractSubsections(sectionContent)
126
+ });
127
+ }
128
+
129
+ return sections;
130
+ }
131
+
132
+ /**
133
+ * Extract ### subsections from section content
134
+ */
135
+ function extractSubsections(content) {
136
+ const subsections = [];
137
+ const subRegex = /^### (.+)$/gm;
138
+ const matches = [...content.matchAll(subRegex)];
139
+
140
+ for (let i = 0; i < matches.length; i++) {
141
+ const match = matches[i];
142
+ const name = match[1].trim();
143
+ const startIdx = match.index + match[0].length;
144
+ const endIdx = matches[i + 1] ? matches[i + 1].index : content.length;
145
+
146
+ subsections.push({
147
+ name,
148
+ content: content.substring(startIdx, endIdx).trim()
149
+ });
150
+ }
151
+
152
+ return subsections;
153
+ }
154
+
155
+ /**
156
+ * Classify section type based on name and content
157
+ */
158
+ function classifySectionType(name, content) {
159
+ const nameLower = name.toLowerCase();
160
+
161
+ if (nameLower === 'purpose') return 'purpose';
162
+ if (nameLower.includes('version history')) return 'version-history';
163
+ if (nameLower.includes('related standards')) return 'related-standards';
164
+ if (nameLower.includes('references')) return 'references';
165
+ if (nameLower === 'license') return 'license';
166
+ if (nameLower.includes('anti-pattern')) return 'anti-patterns';
167
+ if (nameLower.includes('example')) return 'examples';
168
+ if (nameLower.includes('configuration') || nameLower.includes('template')) return 'configuration';
169
+
170
+ // Check content for table presence
171
+ if (content.includes('| ') && content.includes(' |')) {
172
+ return 'table-content';
173
+ }
174
+
175
+ // Check for decision points
176
+ if (content.includes('PROJECT MUST CHOOSE') || content.includes('MUST CHOOSE ONE')) {
177
+ return 'decision-point';
178
+ }
179
+
180
+ return 'content';
181
+ }
182
+
183
+ /**
184
+ * Extract all tables from content
185
+ */
186
+ function extractTables(content) {
187
+ const tables = [];
188
+ // Match tables with header, separator, and rows
189
+ const tableRegex = /(?:^|\n)((?:\|[^\n]+\|\n)+)/g;
190
+ const matches = [...content.matchAll(tableRegex)];
191
+
192
+ for (const match of matches) {
193
+ const tableContent = match[1].trim();
194
+ const lines = tableContent.split('\n').filter(l => l.trim());
195
+
196
+ if (lines.length < 2) continue;
197
+
198
+ // Parse header
199
+ const headerLine = lines[0];
200
+ const headers = parseTableRow(headerLine);
201
+
202
+ // Skip separator line (|---|---|)
203
+ const dataStartIdx = lines[1].includes('---') ? 2 : 1;
204
+
205
+ // Parse data rows
206
+ const rows = [];
207
+ for (let i = dataStartIdx; i < lines.length; i++) {
208
+ const row = parseTableRow(lines[i]);
209
+ if (row.length > 0) {
210
+ rows.push(row);
211
+ }
212
+ }
213
+
214
+ // Try to find table context (previous heading or text)
215
+ const tableStart = match.index;
216
+ const precedingContent = content.substring(Math.max(0, tableStart - 200), tableStart);
217
+ const contextMatch = precedingContent.match(/###?\s+([^\n]+)\s*$/);
218
+ const context = contextMatch ? contextMatch[1].trim() : '';
219
+
220
+ tables.push({
221
+ headers,
222
+ rows,
223
+ context,
224
+ raw: tableContent
225
+ });
226
+ }
227
+
228
+ return tables;
229
+ }
230
+
231
+ /**
232
+ * Parse a single table row
233
+ */
234
+ function parseTableRow(line) {
235
+ return line
236
+ .split('|')
237
+ .map(cell => cell.trim())
238
+ .filter(cell => cell && !cell.match(/^-+$/));
239
+ }
240
+
241
+ /**
242
+ * Extract code blocks from content
243
+ */
244
+ function extractCodeBlocks(content) {
245
+ const codeBlocks = [];
246
+ const codeRegex = /```(\w*)\n([\s\S]*?)```/g;
247
+ const matches = [...content.matchAll(codeRegex)];
248
+
249
+ for (const match of matches) {
250
+ const lang = match[1] || 'text';
251
+ const code = match[2].trim();
252
+
253
+ // Find context (preceding text)
254
+ const blockStart = match.index;
255
+ const precedingContent = content.substring(Math.max(0, blockStart - 300), blockStart);
256
+ const contextMatch = precedingContent.match(/(?:^|\n)([^\n]+)\s*$/);
257
+ const context = contextMatch ? contextMatch[1].trim() : '';
258
+
259
+ codeBlocks.push({
260
+ lang,
261
+ content: code,
262
+ context
263
+ });
264
+ }
265
+
266
+ return codeBlocks;
267
+ }
268
+
269
+ /**
270
+ * Extract decision points (PROJECT MUST CHOOSE patterns)
271
+ */
272
+ function extractDecisionPoints(content) {
273
+ const decisions = [];
274
+
275
+ // Pattern: PROJECT MUST CHOOSE ONE
276
+ const chooseOneRegex = /PROJECT MUST CHOOSE ONE\s+([A-Z]+)\s*/gi;
277
+ const matches = [...content.matchAll(chooseOneRegex)];
278
+
279
+ for (const match of matches) {
280
+ const topic = match[1].toLowerCase();
281
+ decisions.push({
282
+ type: 'choose-one',
283
+ topic,
284
+ context: extractContextAround(content, match.index)
285
+ });
286
+ }
287
+
288
+ // Pattern: ### Option A: / ### Option B: / ### Option C:
289
+ const optionSections = content.match(/### Option [A-C]:?\s+([^\n]+)/g);
290
+ if (optionSections && optionSections.length > 0) {
291
+ const options = optionSections.map(opt => {
292
+ const match = opt.match(/### Option ([A-C]):?\s+(.+)/);
293
+ return match ? { id: match[1], label: match[2].trim() } : null;
294
+ }).filter(Boolean);
295
+
296
+ if (options.length > 0) {
297
+ decisions.push({
298
+ type: 'options',
299
+ options
300
+ });
301
+ }
302
+ }
303
+
304
+ return decisions;
305
+ }
306
+
307
+ /**
308
+ * Extract context around a match position
309
+ */
310
+ function extractContextAround(content, position) {
311
+ const start = Math.max(0, position - 100);
312
+ const end = Math.min(content.length, position + 200);
313
+ return content.substring(start, end).trim();
314
+ }
315
+
316
+ /**
317
+ * Extract Related Standards section
318
+ */
319
+ function extractRelatedStandards(content) {
320
+ const relatedMatch = content.match(/## Related Standards\s*\n+([\s\S]*?)(?=\n---|\n## )/);
321
+ if (!relatedMatch) return [];
322
+
323
+ const links = [];
324
+ const linkRegex = /\[(.+?)\]\((.+?)\)/g;
325
+ const matches = [...relatedMatch[1].matchAll(linkRegex)];
326
+
327
+ for (const match of matches) {
328
+ links.push({
329
+ name: match[1],
330
+ path: match[2]
331
+ });
332
+ }
333
+
334
+ return links;
335
+ }
336
+
337
+ /**
338
+ * Extract Version History table
339
+ */
340
+ function extractVersionHistory(content) {
341
+ const historyMatch = content.match(/## Version History\s*\n+([\s\S]*?)(?=\n---|\n## |$)/);
342
+ if (!historyMatch) return [];
343
+
344
+ const tableContent = historyMatch[1];
345
+ const lines = tableContent.split('\n').filter(l => l.includes('|'));
346
+
347
+ if (lines.length < 3) return []; // Need header, separator, at least one row
348
+
349
+ const history = [];
350
+ // Skip header and separator
351
+ for (let i = 2; i < lines.length; i++) {
352
+ const row = parseTableRow(lines[i]);
353
+ if (row.length >= 3) {
354
+ history.push({
355
+ version: row[0],
356
+ date: row[1],
357
+ changes: row[2]
358
+ });
359
+ }
360
+ }
361
+
362
+ return history;
363
+ }
364
+
365
+ /**
366
+ * Extract rules from content based on imperative patterns
367
+ * Finds statements with MUST, SHOULD, etc.
368
+ */
369
+ export function extractRules(content) {
370
+ const rules = [];
371
+
372
+ // Pattern: statements containing MUST, SHOULD, CRITICAL, IMPORTANT
373
+ const rulePatterns = [
374
+ { regex: /\*\*CRITICAL\*\*:?\s*([^.\n]+)/gi, priority: 'required' },
375
+ { regex: /\*\*IMPORTANT\*\*:?\s*([^.\n]+)/gi, priority: 'required' },
376
+ { regex: /(?:^|\n)\s*-\s*MUST\s+([^.\n]+)/gi, priority: 'required' },
377
+ { regex: /(?:^|\n)\s*-\s*SHOULD\s+([^.\n]+)/gi, priority: 'recommended' },
378
+ { regex: /(?:^|\n)\s*-\s*Always\s+([^.\n]+)/gi, priority: 'required' },
379
+ { regex: /(?:^|\n)\s*-\s*Never\s+([^.\n]+)/gi, priority: 'required' }
380
+ ];
381
+
382
+ for (const pattern of rulePatterns) {
383
+ const matches = [...content.matchAll(pattern.regex)];
384
+ for (const match of matches) {
385
+ const instruction = match[1].trim();
386
+ if (instruction.length > 10) { // Filter out too short matches
387
+ rules.push({
388
+ instruction,
389
+ priority: pattern.priority,
390
+ source: match[0].trim().substring(0, 50)
391
+ });
392
+ }
393
+ }
394
+ }
395
+
396
+ return rules;
397
+ }
398
+
399
+ /**
400
+ * Generate a kebab-case ID from filename or title
401
+ */
402
+ export function generateId(filename) {
403
+ // Remove .md extension
404
+ let id = filename.replace(/\.md$/, '');
405
+
406
+ // Remove common suffixes
407
+ id = id.replace(/-?(guide|standards|standard|checklist)$/i, '');
408
+
409
+ // Convert to kebab-case
410
+ id = id
411
+ .toLowerCase()
412
+ .replace(/[^a-z0-9]+/g, '-')
413
+ .replace(/^-|-$/g, '');
414
+
415
+ return id;
416
+ }
417
+
418
+ /**
419
+ * Infer the source path for a standard
420
+ */
421
+ export function inferSourcePath(filename, locale = null) {
422
+ const cleanName = filename.replace(/\.md$/, '.md');
423
+
424
+ if (locale) {
425
+ return `locales/${locale}/core/${cleanName}`;
426
+ }
427
+
428
+ return `core/${cleanName}`;
429
+ }
@@ -0,0 +1,575 @@
1
+ /**
2
+ * YAML Generator for AI-YAML Conversion
3
+ *
4
+ * Generates AI-optimized YAML from parsed Markdown structure.
5
+ */
6
+
7
+ import { SECTION_MAPPINGS, STANDARD_ID_MAPPING } from './conversion-rules.js';
8
+
9
+ /**
10
+ * Generate AI-YAML structure from parsed Markdown
11
+ * @param {Object} parsed - Parsed Markdown structure from md-parser
12
+ * @param {Object} options - Generation options
13
+ * @returns {Object} AI-YAML structure
14
+ */
15
+ export function generateAiYaml(parsed, options = {}) {
16
+ const { filename = '', locale = null } = options;
17
+
18
+ // Generate ID
19
+ const id = generateStandardId(filename, parsed.metadata.title);
20
+
21
+ // Build YAML structure
22
+ const yaml = {
23
+ id,
24
+ meta: {
25
+ version: parsed.metadata.version || '1.0.0',
26
+ updated: parsed.metadata.updated || new Date().toISOString().split('T')[0],
27
+ source: inferSourcePath(filename, locale),
28
+ description: parsed.purpose || parsed.metadata.title || ''
29
+ }
30
+ };
31
+
32
+ // Add language if locale specified
33
+ if (locale) {
34
+ yaml.meta.language = locale;
35
+ }
36
+
37
+ // Process sections to extract structured content
38
+ const structuredContent = processStructuredContent(parsed);
39
+
40
+ // Merge structured content
41
+ Object.assign(yaml, structuredContent);
42
+
43
+ // Generate rules from content
44
+ const rules = generateRules(parsed);
45
+ if (rules.length > 0) {
46
+ yaml.rules = rules;
47
+ }
48
+
49
+ // Generate quick reference from tables
50
+ const quickRef = generateQuickReference(parsed);
51
+ if (Object.keys(quickRef).length > 0) {
52
+ yaml.quick_reference = quickRef;
53
+ }
54
+
55
+ // Process decision points into options structure
56
+ const decisionsAndOptions = processDecisions(parsed);
57
+ if (decisionsAndOptions.decision) {
58
+ yaml.decision = decisionsAndOptions.decision;
59
+ }
60
+ if (decisionsAndOptions.options) {
61
+ yaml.options = decisionsAndOptions.options;
62
+ }
63
+
64
+ return yaml;
65
+ }
66
+
67
+ /**
68
+ * Generate standard ID from filename or title
69
+ */
70
+ function generateStandardId(filename, _title) {
71
+ // Check mapping first
72
+ const cleanName = filename.replace(/\.md$/, '');
73
+ if (STANDARD_ID_MAPPING[cleanName]) {
74
+ return STANDARD_ID_MAPPING[cleanName];
75
+ }
76
+
77
+ // Generate from filename (title reserved for future use)
78
+ let id = cleanName
79
+ .toLowerCase()
80
+ .replace(/-?(guide|standards|standard|checklist)$/i, '')
81
+ .replace(/[^a-z0-9]+/g, '-')
82
+ .replace(/^-|-$/g, '');
83
+
84
+ return id || 'unknown';
85
+ }
86
+
87
+ /**
88
+ * Infer source path
89
+ */
90
+ function inferSourcePath(filename, locale) {
91
+ const cleanName = filename || 'unknown.md';
92
+
93
+ if (locale) {
94
+ return `locales/${locale}/core/${cleanName}`;
95
+ }
96
+
97
+ return `core/${cleanName}`;
98
+ }
99
+
100
+ /**
101
+ * Process sections into structured content
102
+ */
103
+ function processStructuredContent(parsed) {
104
+ const content = {};
105
+
106
+ for (const section of parsed.sections) {
107
+ const sectionKey = getSectionKey(section.name);
108
+
109
+ // Skip metadata sections
110
+ if (['version-history', 'references', 'license', 'related-standards'].includes(section.type)) {
111
+ continue;
112
+ }
113
+
114
+ // Skip purpose (already in meta.description)
115
+ if (section.type === 'purpose') {
116
+ continue;
117
+ }
118
+
119
+ // Process based on section type
120
+ if (section.type === 'table-content' && section.subsections.length === 0) {
121
+ // Simple table section - will be in quick_reference
122
+ continue;
123
+ }
124
+
125
+ // Handle sections with subsections
126
+ if (section.subsections.length > 0) {
127
+ const sectionData = {};
128
+
129
+ for (const sub of section.subsections) {
130
+ const subKey = toKebabCase(sub.name);
131
+ sectionData[subKey] = extractSubsectionContent(sub);
132
+ }
133
+
134
+ if (Object.keys(sectionData).length > 0) {
135
+ content[sectionKey] = sectionData;
136
+ }
137
+ }
138
+ }
139
+
140
+ return content;
141
+ }
142
+
143
+ /**
144
+ * Get YAML key for a section
145
+ */
146
+ function getSectionKey(sectionName) {
147
+ // Check custom mappings
148
+ if (SECTION_MAPPINGS[sectionName]) {
149
+ return SECTION_MAPPINGS[sectionName];
150
+ }
151
+
152
+ return toKebabCase(sectionName);
153
+ }
154
+
155
+ /**
156
+ * Convert string to kebab-case
157
+ */
158
+ function toKebabCase(str) {
159
+ return str
160
+ .toLowerCase()
161
+ .replace(/[^a-z0-9\u4e00-\u9fff]+/g, '-')
162
+ .replace(/^-|-$/g, '');
163
+ }
164
+
165
+ /**
166
+ * Extract content from a subsection
167
+ */
168
+ function extractSubsectionContent(subsection) {
169
+ const content = subsection.content;
170
+
171
+ // Check if it's primarily a list
172
+ const listItems = content.match(/^[-*]\s+(.+)$/gm);
173
+ if (listItems && listItems.length > 2) {
174
+ return listItems.map(item => item.replace(/^[-*]\s+/, '').trim());
175
+ }
176
+
177
+ // Check if it's a code block
178
+ const codeMatch = content.match(/```\w*\n([\s\S]*?)```/);
179
+ if (codeMatch) {
180
+ return codeMatch[1].trim();
181
+ }
182
+
183
+ // Return as text (truncated if too long)
184
+ const text = content.replace(/\n+/g, ' ').trim();
185
+ return text.length > 200 ? text.substring(0, 200) + '...' : text;
186
+ }
187
+
188
+ /**
189
+ * Generate rules from parsed content
190
+ */
191
+ function generateRules(parsed) {
192
+ const rules = [];
193
+ let ruleIndex = 1;
194
+
195
+ // Extract rules from sections with imperative language
196
+ for (const section of parsed.sections) {
197
+ const sectionRules = extractRulesFromSection(section, ruleIndex);
198
+ rules.push(...sectionRules);
199
+ ruleIndex += sectionRules.length;
200
+ }
201
+
202
+ // Deduplicate similar rules
203
+ const uniqueRules = deduplicateRules(rules);
204
+
205
+ return uniqueRules.slice(0, 15); // Limit to 15 rules
206
+ }
207
+
208
+ /**
209
+ * Extract rules from a section
210
+ */
211
+ function extractRulesFromSection(section, startIndex) {
212
+ const rules = [];
213
+ const content = section.content;
214
+
215
+ // Pattern: bullet points with action words
216
+ const bulletPatterns = [
217
+ { regex: /^[-*]\s+(?:\*\*)?([A-Z][^:.\n]{10,60})(?:\*\*)?[:.]/gm, priority: 'required' },
218
+ { regex: /^[-*]\s+Must\s+([^.\n]{10,60})/gim, priority: 'required' },
219
+ { regex: /^[-*]\s+Should\s+([^.\n]{10,60})/gim, priority: 'recommended' },
220
+ { regex: /^[-*]\s+Always\s+([^.\n]{10,60})/gim, priority: 'required' },
221
+ { regex: /^[-*]\s+Never\s+([^.\n]{10,60})/gim, priority: 'required' }
222
+ ];
223
+
224
+ for (const pattern of bulletPatterns) {
225
+ const matches = [...content.matchAll(pattern.regex)];
226
+ for (const match of matches) {
227
+ const instruction = match[1].trim();
228
+ if (instruction.length > 10 && instruction.length < 100) {
229
+ const ruleId = `${toKebabCase(section.name)}-${startIndex + rules.length}`;
230
+ rules.push({
231
+ id: ruleId,
232
+ trigger: inferTrigger(section.name),
233
+ instruction: cleanInstruction(instruction),
234
+ priority: pattern.priority
235
+ });
236
+ }
237
+ }
238
+ }
239
+
240
+ return rules;
241
+ }
242
+
243
+ /**
244
+ * Infer trigger from section name
245
+ */
246
+ function inferTrigger(sectionName) {
247
+ const nameLower = sectionName.toLowerCase();
248
+
249
+ if (nameLower.includes('commit')) return 'writing commit message';
250
+ if (nameLower.includes('scope')) return 'specifying scope';
251
+ if (nameLower.includes('subject')) return 'writing subject line';
252
+ if (nameLower.includes('body')) return 'writing commit body';
253
+ if (nameLower.includes('footer')) return 'adding footer';
254
+ if (nameLower.includes('type')) return 'choosing type';
255
+ if (nameLower.includes('format')) return 'formatting';
256
+ if (nameLower.includes('test')) return 'writing tests';
257
+ if (nameLower.includes('review')) return 'reviewing code';
258
+ if (nameLower.includes('branch')) return 'working with branches';
259
+
260
+ return 'applying this standard';
261
+ }
262
+
263
+ /**
264
+ * Clean instruction text
265
+ */
266
+ function cleanInstruction(text) {
267
+ return text
268
+ .replace(/\*\*/g, '')
269
+ .replace(/`/g, '')
270
+ .replace(/\s+/g, ' ')
271
+ .trim();
272
+ }
273
+
274
+ /**
275
+ * Deduplicate similar rules
276
+ */
277
+ function deduplicateRules(rules) {
278
+ const seen = new Set();
279
+ const unique = [];
280
+
281
+ for (const rule of rules) {
282
+ const normalized = rule.instruction.toLowerCase().substring(0, 30);
283
+ if (!seen.has(normalized)) {
284
+ seen.add(normalized);
285
+ unique.push(rule);
286
+ }
287
+ }
288
+
289
+ return unique;
290
+ }
291
+
292
+ /**
293
+ * Generate quick reference from tables
294
+ */
295
+ function generateQuickReference(parsed) {
296
+ const quickRef = {};
297
+
298
+ for (const table of parsed.tables) {
299
+ if (table.headers.length < 2 || table.rows.length < 1) {
300
+ continue;
301
+ }
302
+
303
+ // Generate table key from context or headers
304
+ const tableKey = generateTableKey(table);
305
+
306
+ quickRef[tableKey] = {
307
+ columns: table.headers,
308
+ rows: table.rows
309
+ };
310
+ }
311
+
312
+ return quickRef;
313
+ }
314
+
315
+ /**
316
+ * Generate a key for a table
317
+ */
318
+ function generateTableKey(table) {
319
+ // Use context if available
320
+ if (table.context) {
321
+ return toKebabCase(table.context);
322
+ }
323
+
324
+ // Use first header
325
+ if (table.headers[0]) {
326
+ return toKebabCase(table.headers[0]) + '-table';
327
+ }
328
+
329
+ return 'reference-table';
330
+ }
331
+
332
+ /**
333
+ * Process decision points into options structure
334
+ */
335
+ function processDecisions(parsed) {
336
+ const result = {};
337
+
338
+ if (parsed.decisionPoints.length === 0) {
339
+ return result;
340
+ }
341
+
342
+ // Find option sections
343
+ const optionDecision = parsed.decisionPoints.find(d => d.type === 'options');
344
+ if (optionDecision) {
345
+ result.decision = {
346
+ question: 'Which option best fits your project?',
347
+ matrix: optionDecision.options.map(opt => ({
348
+ answer: opt.label,
349
+ select: toKebabCase(opt.label)
350
+ }))
351
+ };
352
+
353
+ // Generate options structure
354
+ const optionCategory = inferOptionCategory(parsed.metadata.title);
355
+ result.options = {
356
+ [optionCategory]: {
357
+ default: toKebabCase(optionDecision.options[0]?.label || 'default'),
358
+ choices: optionDecision.options.map(opt => ({
359
+ id: toKebabCase(opt.label),
360
+ file: `options/${optionCategory}/${toKebabCase(opt.label)}.ai.yaml`
361
+ }))
362
+ }
363
+ };
364
+ }
365
+
366
+ return result;
367
+ }
368
+
369
+ /**
370
+ * Infer option category from title
371
+ */
372
+ function inferOptionCategory(title) {
373
+ const titleLower = title?.toLowerCase() || '';
374
+
375
+ if (titleLower.includes('commit')) return 'commit_language';
376
+ if (titleLower.includes('test')) return 'testing_approach';
377
+ if (titleLower.includes('git')) return 'git_workflow';
378
+ if (titleLower.includes('changelog')) return 'changelog_format';
379
+ if (titleLower.includes('review')) return 'review_style';
380
+
381
+ return 'default_option';
382
+ }
383
+
384
+ /**
385
+ * Convert YAML object to formatted string
386
+ * @param {Object} obj - YAML object
387
+ * @returns {string} Formatted YAML string
388
+ */
389
+ export function toYamlString(obj) {
390
+ const lines = [];
391
+
392
+ // Add header comment
393
+ if (obj.meta?.source) {
394
+ const title = obj.id || 'Standard';
395
+ lines.push(`# ${title.charAt(0).toUpperCase() + title.slice(1).replace(/-/g, ' ')} - AI Optimized`);
396
+ lines.push(`# Source: ${obj.meta.source}`);
397
+ lines.push('');
398
+ }
399
+
400
+ // Serialize object
401
+ serializeObject(obj, lines, 0);
402
+
403
+ return lines.join('\n');
404
+ }
405
+
406
+ /**
407
+ * Serialize a YAML value to string
408
+ */
409
+ function serializeScalar(value, indent) {
410
+ if (value === null || value === undefined) {
411
+ return 'null';
412
+ }
413
+
414
+ if (typeof value === 'boolean') {
415
+ return value ? 'true' : 'false';
416
+ }
417
+
418
+ if (typeof value === 'number') {
419
+ return String(value);
420
+ }
421
+
422
+ if (typeof value === 'string') {
423
+ // Check if multiline
424
+ if (value.includes('\n')) {
425
+ const prefix = ' '.repeat(indent + 1);
426
+ return '|\n' + value.split('\n').map(line => prefix + line).join('\n');
427
+ }
428
+
429
+ // Check if needs quoting
430
+ if (value === '' ||
431
+ value.match(/^[\s]|[\s]$/) ||
432
+ value.match(/^[#&*!|>'"%@`]/) ||
433
+ value.match(/[:#{}[\],&*?|<>=!%@`]/) ||
434
+ value.match(/^(true|false|null|yes|no|on|off)$/i) ||
435
+ value.match(/^\d/)) {
436
+ return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
437
+ }
438
+
439
+ return value;
440
+ }
441
+
442
+ return String(value);
443
+ }
444
+
445
+ /**
446
+ * Serialize an object to YAML lines
447
+ */
448
+ function serializeObject(obj, lines, indent) {
449
+ const prefix = ' '.repeat(indent);
450
+
451
+ for (const [key, value] of Object.entries(obj)) {
452
+ if (value === null || value === undefined) {
453
+ continue;
454
+ }
455
+
456
+ if (Array.isArray(value)) {
457
+ if (value.length === 0) {
458
+ lines.push(`${prefix}${key}: []`);
459
+ } else if (isSimpleArray(value)) {
460
+ // Inline simple arrays
461
+ const items = value.map(v => serializeScalar(v, indent));
462
+ lines.push(`${prefix}${key}: [${items.join(', ')}]`);
463
+ } else {
464
+ // Block array
465
+ lines.push(`${prefix}${key}:`);
466
+ for (const item of value) {
467
+ serializeArrayItem(item, lines, indent + 1);
468
+ }
469
+ }
470
+ } else if (typeof value === 'object') {
471
+ lines.push(`${prefix}${key}:`);
472
+ serializeObject(value, lines, indent + 1);
473
+ } else {
474
+ lines.push(`${prefix}${key}: ${serializeScalar(value, indent)}`);
475
+ }
476
+ }
477
+ }
478
+
479
+ /**
480
+ * Serialize an array item
481
+ */
482
+ function serializeArrayItem(item, lines, indent) {
483
+ const prefix = ' '.repeat(indent);
484
+
485
+ if (typeof item === 'object' && item !== null && !Array.isArray(item)) {
486
+ const entries = Object.entries(item);
487
+ if (entries.length === 0) {
488
+ lines.push(`${prefix}- {}`);
489
+ return;
490
+ }
491
+
492
+ // First key on same line as dash
493
+ const [firstKey, firstVal] = entries[0];
494
+ if (typeof firstVal === 'object' && firstVal !== null) {
495
+ lines.push(`${prefix}- ${firstKey}:`);
496
+ if (Array.isArray(firstVal)) {
497
+ for (const subItem of firstVal) {
498
+ serializeArrayItem(subItem, lines, indent + 2);
499
+ }
500
+ } else {
501
+ serializeObject(firstVal, lines, indent + 2);
502
+ }
503
+ } else {
504
+ lines.push(`${prefix}- ${firstKey}: ${serializeScalar(firstVal, indent)}`);
505
+ }
506
+
507
+ // Rest of keys indented
508
+ for (let i = 1; i < entries.length; i++) {
509
+ const [key, val] = entries[i];
510
+ if (typeof val === 'object' && val !== null) {
511
+ lines.push(`${prefix} ${key}:`);
512
+ if (Array.isArray(val)) {
513
+ for (const subItem of val) {
514
+ serializeArrayItem(subItem, lines, indent + 2);
515
+ }
516
+ } else {
517
+ serializeObject(val, lines, indent + 2);
518
+ }
519
+ } else {
520
+ lines.push(`${prefix} ${key}: ${serializeScalar(val, indent + 1)}`);
521
+ }
522
+ }
523
+ } else {
524
+ lines.push(`${prefix}- ${serializeScalar(item, indent)}`);
525
+ }
526
+ }
527
+
528
+ /**
529
+ * Check if array contains only simple values
530
+ */
531
+ function isSimpleArray(arr) {
532
+ return arr.length <= 5 && arr.every(v =>
533
+ (typeof v === 'string' && v.length < 50 && !v.includes('\n')) ||
534
+ typeof v === 'number' ||
535
+ typeof v === 'boolean'
536
+ );
537
+ }
538
+
539
+ /**
540
+ * Extract manual sections from existing YAML content
541
+ * @param {string} content - Existing YAML content
542
+ * @returns {Object} Extracted sections
543
+ */
544
+ export function extractManualSections(content) {
545
+ const result = {
546
+ hasManualSections: false,
547
+ manualContent: ''
548
+ };
549
+
550
+ const manualMatch = content.match(/# MANUAL ADDITIONS START\s*([\s\S]*?)# MANUAL ADDITIONS END/);
551
+ if (manualMatch) {
552
+ result.hasManualSections = true;
553
+ result.manualContent = manualMatch[1].trim();
554
+ }
555
+
556
+ return result;
557
+ }
558
+
559
+ /**
560
+ * Merge manual sections into generated YAML
561
+ */
562
+ export function mergeManualSections(generatedYaml, manualContent) {
563
+ if (!manualContent) {
564
+ return generatedYaml;
565
+ }
566
+
567
+ // Add markers and manual content at the end
568
+ return `${generatedYaml}
569
+
570
+ # MANUAL ADDITIONS START
571
+ # Add custom rules below this line
572
+ ${manualContent}
573
+ # MANUAL ADDITIONS END
574
+ `;
575
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "version": "3.5.0-beta.11",
4
- "lastUpdated": "2026-01-11",
3
+ "version": "3.5.0-beta.12",
4
+ "lastUpdated": "2026-01-12",
5
5
  "description": "Standards registry for universal-dev-standards with integrated skills and AI-optimized formats",
6
6
  "formats": {
7
7
  "ai": {
@@ -48,14 +48,14 @@
48
48
  "standards": {
49
49
  "name": "universal-dev-standards",
50
50
  "url": "https://github.com/AsiaOstrich/universal-dev-standards",
51
- "version": "3.5.0-beta.11"
51
+ "version": "3.5.0-beta.12"
52
52
  },
53
53
  "skills": {
54
54
  "name": "universal-dev-standards",
55
55
  "url": "https://github.com/AsiaOstrich/universal-dev-standards",
56
56
  "localPath": "skills/claude-code",
57
57
  "rawUrl": "https://raw.githubusercontent.com/AsiaOstrich/universal-dev-standards/main/skills/claude-code",
58
- "version": "3.5.0-beta.11",
58
+ "version": "3.5.0-beta.12",
59
59
  "note": "Skills are now included in the main repository under skills/"
60
60
  }
61
61
  },
@@ -588,6 +588,16 @@
588
588
  "level": 2,
589
589
  "description": "Red-Green-Refactor methodology for TDD workflow"
590
590
  },
591
+ {
592
+ "id": "refactoring-standards",
593
+ "name": "Refactoring Standards",
594
+ "nameZh": "重構標準",
595
+ "source": "core/refactoring-standards.md",
596
+ "category": "reference",
597
+ "skillName": null,
598
+ "level": 2,
599
+ "description": "Comprehensive refactoring guidelines covering legacy code strategies, large-scale patterns, database refactoring, metrics, and team collaboration"
600
+ },
591
601
  {
592
602
  "id": "csharp-style",
593
603
  "name": "C# Style Guide",