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 +1 -1
- package/src/utils/conversion-rules.js +232 -0
- package/src/utils/detector.js +6 -1
- package/src/utils/md-parser.js +429 -0
- package/src/utils/yaml-generator.js +575 -0
- package/standards-registry.json +14 -4
package/package.json
CHANGED
|
@@ -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
|
+
}
|
package/src/utils/detector.js
CHANGED
|
@@ -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
|
+
}
|
package/standards-registry.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
-
"version": "3.5.0-beta.
|
|
4
|
-
"lastUpdated": "2026-01-
|
|
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.
|
|
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.
|
|
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",
|