universal-dev-standards 5.14.0 → 5.15.1
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/bin/uds.js +2 -0
- package/bundled/ai/standards/ai-instruction-standards.ai.yaml +190 -3
- package/bundled/ai/standards/knowledge-graph-memory.ai.yaml +83 -0
- package/bundled/core/ai-instruction-standards.md +136 -11
- package/bundled/core/knowledge-graph-memory.md +119 -0
- package/bundled/locales/COVERAGE.md +226 -0
- package/bundled/locales/zh-CN/CHANGELOG.md +32 -3
- package/bundled/locales/zh-CN/README.md +1 -1
- package/bundled/locales/zh-CN/SECURITY.md +1 -1
- package/bundled/locales/zh-CN/core/ai-instruction-standards.md +111 -5
- package/bundled/locales/zh-TW/CHANGELOG.md +36 -3
- package/bundled/locales/zh-TW/README.md +1 -1
- package/bundled/locales/zh-TW/SECURITY.md +1 -1
- package/bundled/locales/zh-TW/core/ai-instruction-standards.md +130 -5
- package/bundled/locales/zh-TW/core/knowledge-graph-memory.md +127 -0
- package/bundled/locales/zh-TW/core/self-review-protocol.md +9 -1
- package/bundled/locales/zh-TW/skills/ac-coverage/SKILL.md +192 -0
- package/bundled/locales/zh-TW/skills/ai-collaboration-standards/SKILL.md +5 -1
- package/bundled/locales/zh-TW/skills/deploy-assistant/SKILL.md +187 -0
- package/bundled/locales/zh-TW/skills/dev-methodology/SKILL.md +108 -0
- package/bundled/locales/zh-TW/skills/journey-test-assistant/SKILL.md +222 -0
- package/bundled/locales/zh-TW/skills/knowledge-graph/SKILL.md +56 -0
- package/bundled/locales/zh-TW/skills/orchestrate/SKILL.md +172 -0
- package/bundled/locales/zh-TW/skills/plan/SKILL.md +239 -0
- package/bundled/locales/zh-TW/skills/project-structure-guide/SKILL.md +5 -1
- package/bundled/locales/zh-TW/skills/push/SKILL.md +241 -0
- package/bundled/locales/zh-TW/skills/skill-builder/SKILL.md +165 -0
- package/bundled/locales/zh-TW/skills/spec-derivation/SKILL.md +83 -0
- package/bundled/locales/zh-TW/skills/sweep/SKILL.md +149 -0
- package/bundled/skills/adr-assistant/SKILL.md +1 -1
- package/bundled/skills/ai-collaboration-standards/SKILL.md +1 -1
- package/bundled/skills/ai-friendly-architecture/SKILL.md +1 -1
- package/bundled/skills/ai-instruction-standards/SKILL.md +1 -1
- package/bundled/skills/api-design-assistant/SKILL.md +1 -1
- package/bundled/skills/audit-assistant/SKILL.md +1 -1
- package/bundled/skills/ci-cd-assistant/SKILL.md +1 -1
- package/bundled/skills/contract-test-assistant/SKILL.md +1 -1
- package/bundled/skills/database-assistant/SKILL.md +1 -1
- package/bundled/skills/deploy-assistant/SKILL.md +1 -1
- package/bundled/skills/documentation-guide/SKILL.md +1 -1
- package/bundled/skills/error-code-guide/SKILL.md +1 -1
- package/bundled/skills/git-workflow-guide/SKILL.md +1 -1
- package/bundled/skills/incident-response-assistant/SKILL.md +1 -1
- package/bundled/skills/journey-test-assistant/SKILL.md +1 -1
- package/bundled/skills/knowledge-graph/SKILL.md +58 -0
- package/bundled/skills/knowledge-graph/guide.md +69 -0
- package/bundled/skills/logging-guide/SKILL.md +1 -1
- package/bundled/skills/observability-assistant/SKILL.md +1 -1
- package/bundled/skills/orchestrate/SKILL.md +1 -1
- package/bundled/skills/plan/SKILL.md +1 -1
- package/bundled/skills/pr-automation-assistant/SKILL.md +1 -1
- package/bundled/skills/project-structure-guide/SKILL.md +1 -1
- package/bundled/skills/push/SKILL.md +1 -1
- package/bundled/skills/retrospective-assistant/SKILL.md +1 -1
- package/bundled/skills/reverse-engineer/SKILL.md +1 -1
- package/bundled/skills/runbook-assistant/SKILL.md +1 -1
- package/bundled/skills/security-assistant/SKILL.md +1 -1
- package/bundled/skills/security-scan-assistant/SKILL.md +1 -1
- package/bundled/skills/slo-assistant/SKILL.md +1 -1
- package/bundled/skills/sweep/SKILL.md +1 -1
- package/bundled/skills/testing-guide/SKILL.md +1 -1
- package/package.json +1 -1
- package/src/commands/check.js +71 -0
- package/src/commands/init.js +8 -1
- package/src/commands/update.js +49 -14
- package/src/i18n/messages.js +32 -5
- package/src/installers/skills-installer.js +49 -0
- package/src/lint/i18n.js +338 -0
- package/src/utils/config-manager.js +39 -0
- package/src/utils/skills-installer.js +39 -7
- package/standards-registry.json +16 -4
package/src/lint/i18n.js
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* i18n Lint Rules (XSPEC-239 §Req-5 + Chimera Prevention)
|
|
3
|
+
*
|
|
4
|
+
* Detects layered-language-strategy violations in canonical and locale files.
|
|
5
|
+
*
|
|
6
|
+
* Rules:
|
|
7
|
+
* canonical:description-must-be-ascii (error)
|
|
8
|
+
* locale:description-must-match-language (error)
|
|
9
|
+
* locale:must-have-source-frontmatter (error)
|
|
10
|
+
* canonical:l3-language-consistency (warn)
|
|
11
|
+
* translation-drift-warn (warn)
|
|
12
|
+
*
|
|
13
|
+
* `adopter-must-match-installed-locale` is deferred (needs project context,
|
|
14
|
+
* not source-tree level).
|
|
15
|
+
*
|
|
16
|
+
* @module lint/i18n
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
20
|
+
import { join, dirname, basename, resolve } from 'path';
|
|
21
|
+
|
|
22
|
+
// CJK Unified Ideographs ranges (excluding fullwidth punctuation, but
|
|
23
|
+
// including ranges Ext-A/B used by zh-TW/zh-CN). Hangul, Hiragana,
|
|
24
|
+
// Katakana included to also flag accidentally-mixed CJK content.
|
|
25
|
+
const CJK_REGEX = /[㐀-䶿一-鿿豈--ゟ゠-ヿ가-]/;
|
|
26
|
+
|
|
27
|
+
// ASCII-only regex (allow printable ASCII + space + common punctuation).
|
|
28
|
+
// Anything outside extended-ASCII counts as non-ASCII for description purposes.
|
|
29
|
+
// Control chars \x09 (tab), \x0A (LF), \x0D (CR) are intentionally allowed.
|
|
30
|
+
// eslint-disable-next-line no-control-regex
|
|
31
|
+
const NON_ASCII_REGEX = /[^\x09\x0A\x0D\x20-\x7E]/;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Parse a minimal YAML front matter from markdown content.
|
|
35
|
+
* Returns { fm: {key→value}, fmEndLine: number, body: string }
|
|
36
|
+
* If there is no front matter, returns fm=null, fmEndLine=0, body=full content.
|
|
37
|
+
*/
|
|
38
|
+
function parseFrontmatter(content) {
|
|
39
|
+
const lines = content.split('\n');
|
|
40
|
+
if (lines[0] !== '---') {
|
|
41
|
+
return { fm: null, fmEndLine: 0, body: content };
|
|
42
|
+
}
|
|
43
|
+
const fm = {};
|
|
44
|
+
let fmEndLine = -1;
|
|
45
|
+
for (let i = 1; i < lines.length; i++) {
|
|
46
|
+
if (lines[i] === '---') {
|
|
47
|
+
fmEndLine = i;
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
const m = lines[i].match(/^([a-zA-Z_][\w-]*):\s*(.*)$/);
|
|
51
|
+
if (m) {
|
|
52
|
+
let value = m[2].trim();
|
|
53
|
+
// Multi-line block scalar (| or >) — collect lines until indentation breaks
|
|
54
|
+
if (value === '|' || value === '>') {
|
|
55
|
+
const blockLines = [];
|
|
56
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
57
|
+
if (lines[j] === '---') break;
|
|
58
|
+
if (lines[j].startsWith(' ') || lines[j] === '') {
|
|
59
|
+
blockLines.push(lines[j].replace(/^ {0,2}/, ''));
|
|
60
|
+
} else {
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
value = blockLines.join('\n').trim();
|
|
65
|
+
} else if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
66
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
67
|
+
value = value.slice(1, -1);
|
|
68
|
+
}
|
|
69
|
+
fm[m[1]] = { value, line: i + 1 };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (fmEndLine === -1) {
|
|
73
|
+
return { fm: null, fmEndLine: 0, body: content };
|
|
74
|
+
}
|
|
75
|
+
const body = lines.slice(fmEndLine + 1).join('\n');
|
|
76
|
+
return { fm, fmEndLine, body };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function semverParts(v) {
|
|
80
|
+
if (!v) return null;
|
|
81
|
+
const stripped = String(v).split('-')[0];
|
|
82
|
+
const parts = stripped.split('.').map(n => parseInt(n, 10));
|
|
83
|
+
if (parts.some(Number.isNaN)) return null;
|
|
84
|
+
return parts;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isMajorOrMinorGap(translation, source, maxMinorGap = 2) {
|
|
88
|
+
const t = semverParts(translation);
|
|
89
|
+
const s = semverParts(source);
|
|
90
|
+
if (!t || !s) return null;
|
|
91
|
+
if ((t[0] || 0) !== (s[0] || 0)) return { kind: 'major' };
|
|
92
|
+
const gap = (s[1] || 0) - (t[1] || 0);
|
|
93
|
+
if (gap > maxMinorGap) return { kind: 'minor', gap };
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Look up the source markdown's version from its first 30 lines.
|
|
99
|
+
* Supports YAML frontmatter `version:` plus `**Version**: x.y.z` patterns.
|
|
100
|
+
*/
|
|
101
|
+
function readSourceVersion(filePath) {
|
|
102
|
+
if (!existsSync(filePath)) return null;
|
|
103
|
+
const head = readFileSync(filePath, 'utf-8').split('\n').slice(0, 30);
|
|
104
|
+
for (const line of head) {
|
|
105
|
+
let m = line.match(/^\s*version:\s*(\S+)/i);
|
|
106
|
+
if (m) return m[1].trim();
|
|
107
|
+
m = line.match(/^\s*\*\*Version\*\*:\s*([\d][^|\s]*)/i);
|
|
108
|
+
if (m) return m[1].trim();
|
|
109
|
+
m = line.match(/^\s*>\s*Version:\s*([\d][^|\s]*)/i);
|
|
110
|
+
if (m) return m[1].trim();
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Lint a canonical SKILL.md or core/*.md file for English-only frontmatter
|
|
117
|
+
* and L3 language consistency.
|
|
118
|
+
*
|
|
119
|
+
* @param {string} skillMdPath - Absolute path to canonical file
|
|
120
|
+
* @returns {Array<{rule, severity, line, message, file}>}
|
|
121
|
+
*/
|
|
122
|
+
export function lintCanonical(skillMdPath) {
|
|
123
|
+
const findings = [];
|
|
124
|
+
if (!existsSync(skillMdPath)) return findings;
|
|
125
|
+
const content = readFileSync(skillMdPath, 'utf-8');
|
|
126
|
+
const { fm, body } = parseFrontmatter(content);
|
|
127
|
+
|
|
128
|
+
// Rule: canonical:description-must-be-ascii
|
|
129
|
+
if (fm && fm.description) {
|
|
130
|
+
if (NON_ASCII_REGEX.test(fm.description.value)) {
|
|
131
|
+
findings.push({
|
|
132
|
+
rule: 'canonical:description-must-be-ascii',
|
|
133
|
+
severity: 'error',
|
|
134
|
+
line: fm.description.line,
|
|
135
|
+
file: skillMdPath,
|
|
136
|
+
message: 'Canonical `description` must be ASCII-only (English). Found non-ASCII characters; move translation to locale variant.'
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Rule: canonical:l3-language-consistency
|
|
142
|
+
// Heuristic: scan code-block-style output templates (```...```) for
|
|
143
|
+
// non-English example response text. Only fire when description is ASCII
|
|
144
|
+
// (otherwise the description-must-be-ascii error already covers it).
|
|
145
|
+
if (fm && fm.description && !NON_ASCII_REGEX.test(fm.description.value)) {
|
|
146
|
+
const fenceRegex = /```([a-zA-Z0-9_-]*)\n([\s\S]*?)```/g;
|
|
147
|
+
let match;
|
|
148
|
+
while ((match = fenceRegex.exec(body)) !== null) {
|
|
149
|
+
const lang = match[1];
|
|
150
|
+
const block = match[2];
|
|
151
|
+
// Skip code fences for known programming languages — those legitimately
|
|
152
|
+
// contain non-English string literals.
|
|
153
|
+
const skipLangs = new Set(['', 'json', 'yaml', 'yml', 'toml', 'ini',
|
|
154
|
+
'sh', 'bash', 'zsh', 'powershell',
|
|
155
|
+
'js', 'ts', 'tsx', 'jsx', 'javascript', 'typescript',
|
|
156
|
+
'py', 'python', 'rb', 'ruby', 'go', 'rust', 'java', 'c', 'cpp',
|
|
157
|
+
'html', 'css', 'sql', 'diff', 'patch']);
|
|
158
|
+
if (skipLangs.has(lang.toLowerCase())) continue;
|
|
159
|
+
|
|
160
|
+
// Likely an output template / example response (e.g., ```markdown,
|
|
161
|
+
// ```text, ```output, ```response). Flag if it contains CJK.
|
|
162
|
+
if (CJK_REGEX.test(block)) {
|
|
163
|
+
// Compute approx. line number of the fence start
|
|
164
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
165
|
+
findings.push({
|
|
166
|
+
rule: 'canonical:l3-language-consistency',
|
|
167
|
+
severity: 'warn',
|
|
168
|
+
line: lineNum,
|
|
169
|
+
file: skillMdPath,
|
|
170
|
+
message: `Canonical body contains a non-English example response inside a non-code fenced block (lang=\`${lang || 'plain'}\`). Move translated examples to the locale variant.`
|
|
171
|
+
});
|
|
172
|
+
// Only report first occurrence per file to keep output readable
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return findings;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Lint a locale variant (e.g. locales/zh-TW/skills/foo/SKILL.md or
|
|
183
|
+
* locales/zh-CN/core/some-standard.md).
|
|
184
|
+
*
|
|
185
|
+
* @param {string} skillMdPath - Absolute path to locale variant file
|
|
186
|
+
* @param {string} locale - Locale code (e.g. 'zh-TW', 'zh-CN')
|
|
187
|
+
* @returns {Array<{rule, severity, line, message, file}>}
|
|
188
|
+
*/
|
|
189
|
+
export function lintLocale(skillMdPath, locale) {
|
|
190
|
+
const findings = [];
|
|
191
|
+
if (!existsSync(skillMdPath)) return findings;
|
|
192
|
+
const content = readFileSync(skillMdPath, 'utf-8');
|
|
193
|
+
const { fm } = parseFrontmatter(content);
|
|
194
|
+
|
|
195
|
+
// Rule: locale:must-have-source-frontmatter
|
|
196
|
+
if (!fm) {
|
|
197
|
+
findings.push({
|
|
198
|
+
rule: 'locale:must-have-source-frontmatter',
|
|
199
|
+
severity: 'error',
|
|
200
|
+
line: 1,
|
|
201
|
+
file: skillMdPath,
|
|
202
|
+
message: 'Locale variant is missing YAML front matter (must include `source:`, `source_version:`, `translation_version:`).'
|
|
203
|
+
});
|
|
204
|
+
return findings;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const required = ['source', 'source_version', 'translation_version'];
|
|
208
|
+
const missing = required.filter(k => !fm[k]);
|
|
209
|
+
if (missing.length > 0) {
|
|
210
|
+
findings.push({
|
|
211
|
+
rule: 'locale:must-have-source-frontmatter',
|
|
212
|
+
severity: 'error',
|
|
213
|
+
line: 1,
|
|
214
|
+
file: skillMdPath,
|
|
215
|
+
message: `Locale variant is missing required frontmatter field(s): ${missing.join(', ')}.`
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Rule: locale:description-must-match-language
|
|
220
|
+
// For zh-TW / zh-CN, the description (if present) must contain CJK.
|
|
221
|
+
if (fm.description) {
|
|
222
|
+
const expectsCjk = locale === 'zh-TW' || locale === 'zh-CN' ||
|
|
223
|
+
locale.startsWith('zh-') || locale === 'ja-JP' ||
|
|
224
|
+
locale === 'ko-KR';
|
|
225
|
+
if (expectsCjk && !CJK_REGEX.test(fm.description.value)) {
|
|
226
|
+
findings.push({
|
|
227
|
+
rule: 'locale:description-must-match-language',
|
|
228
|
+
severity: 'error',
|
|
229
|
+
line: fm.description.line,
|
|
230
|
+
file: skillMdPath,
|
|
231
|
+
message: `Locale \`${locale}\` variant has ASCII-only \`description\`; locale variants must contain ${locale}-script characters (likely chimera).`
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Rule: translation-drift-warn
|
|
237
|
+
if (fm.source && fm.translation_version) {
|
|
238
|
+
// Resolve source path relative to this file's directory
|
|
239
|
+
const sourceRel = fm.source.value;
|
|
240
|
+
const sourcePath = resolve(dirname(skillMdPath), sourceRel);
|
|
241
|
+
const actualSourceVersion = readSourceVersion(sourcePath);
|
|
242
|
+
if (actualSourceVersion) {
|
|
243
|
+
const gap = isMajorOrMinorGap(fm.translation_version.value, actualSourceVersion);
|
|
244
|
+
if (gap) {
|
|
245
|
+
const detail = gap.kind === 'major'
|
|
246
|
+
? 'major-version gap'
|
|
247
|
+
: `${gap.gap} minor versions behind`;
|
|
248
|
+
findings.push({
|
|
249
|
+
rule: 'translation-drift-warn',
|
|
250
|
+
severity: 'warn',
|
|
251
|
+
line: fm.translation_version.line,
|
|
252
|
+
file: skillMdPath,
|
|
253
|
+
message: `translation_version \`${fm.translation_version.value}\` lags source_version \`${actualSourceVersion}\` (${detail}). Re-sync recommended.`
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return findings;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Batch-lint a UDS project tree. Scans canonical `skills/` and `core/`,
|
|
264
|
+
* plus `locales/{locale}/skills/` and `locales/{locale}/core/`.
|
|
265
|
+
*
|
|
266
|
+
* @param {object} opts
|
|
267
|
+
* @param {string} opts.projectPath - Project (or UDS) root path
|
|
268
|
+
* @param {string[]} [opts.locales] - Locales to lint (defaults to discovering
|
|
269
|
+
* every subdirectory of `locales/`)
|
|
270
|
+
* @returns {Array<{rule, severity, line, message, file}>}
|
|
271
|
+
*/
|
|
272
|
+
export function lintAll({ projectPath, locales }) {
|
|
273
|
+
const findings = [];
|
|
274
|
+
|
|
275
|
+
const skillsDir = join(projectPath, 'skills');
|
|
276
|
+
const coreDir = join(projectPath, 'core');
|
|
277
|
+
const localesDir = join(projectPath, 'locales');
|
|
278
|
+
|
|
279
|
+
// --- Canonical skills ---
|
|
280
|
+
if (existsSync(skillsDir)) {
|
|
281
|
+
for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
|
|
282
|
+
if (!entry.isDirectory()) continue;
|
|
283
|
+
const skillFile = join(skillsDir, entry.name, 'SKILL.md');
|
|
284
|
+
if (existsSync(skillFile)) {
|
|
285
|
+
findings.push(...lintCanonical(skillFile));
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// --- Canonical core/*.md ---
|
|
291
|
+
if (existsSync(coreDir)) {
|
|
292
|
+
for (const entry of readdirSync(coreDir, { withFileTypes: true })) {
|
|
293
|
+
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
|
|
294
|
+
findings.push(...lintCanonical(join(coreDir, entry.name)));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// --- Locale variants ---
|
|
299
|
+
if (existsSync(localesDir)) {
|
|
300
|
+
const candidateLocales = locales || readdirSync(localesDir, { withFileTypes: true })
|
|
301
|
+
.filter(d => d.isDirectory())
|
|
302
|
+
.map(d => d.name)
|
|
303
|
+
.filter(name => /^[a-z]{2}(-[A-Z]{2})?$/.test(name));
|
|
304
|
+
|
|
305
|
+
for (const locale of candidateLocales) {
|
|
306
|
+
const localeSkillsDir = join(localesDir, locale, 'skills');
|
|
307
|
+
const localeCoreDir = join(localesDir, locale, 'core');
|
|
308
|
+
|
|
309
|
+
if (existsSync(localeSkillsDir)) {
|
|
310
|
+
for (const entry of readdirSync(localeSkillsDir, { withFileTypes: true })) {
|
|
311
|
+
if (!entry.isDirectory()) continue;
|
|
312
|
+
const skillFile = join(localeSkillsDir, entry.name, 'SKILL.md');
|
|
313
|
+
if (existsSync(skillFile)) {
|
|
314
|
+
findings.push(...lintLocale(skillFile, locale));
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (existsSync(localeCoreDir)) {
|
|
319
|
+
for (const entry of readdirSync(localeCoreDir, { withFileTypes: true })) {
|
|
320
|
+
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
|
|
321
|
+
findings.push(...lintLocale(join(localeCoreDir, entry.name), locale));
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return findings;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Convenience helper: split findings into errors and warnings.
|
|
332
|
+
*/
|
|
333
|
+
export function partitionFindings(findings) {
|
|
334
|
+
return {
|
|
335
|
+
errors: findings.filter(f => f.severity === 'error'),
|
|
336
|
+
warnings: findings.filter(f => f.severity === 'warn'),
|
|
337
|
+
};
|
|
338
|
+
}
|
|
@@ -110,3 +110,42 @@ export class ConfigManager {
|
|
|
110
110
|
|
|
111
111
|
// Singleton instance for the CLI
|
|
112
112
|
export const config = new ConfigManager();
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Read the adopter-declared install settings from `.uds/install.yaml`.
|
|
116
|
+
*
|
|
117
|
+
* This file is a *declarative* install descriptor (distinct from
|
|
118
|
+
* `.uds/config.yaml` which holds runtime preferences). Adopters use it to
|
|
119
|
+
* pin install-time choices — most importantly `locale:` — so they do not
|
|
120
|
+
* need to pass `--locale` every time they run `uds init`/`uds update`.
|
|
121
|
+
*
|
|
122
|
+
* Locale resolution order (XSPEC-239 §Req-3 / P1-CLI-2 + P1-CLI-3):
|
|
123
|
+
* CLI `--locale` > install.yaml `locale:` > `UDS_LOCALE` env > LANG > 'en'
|
|
124
|
+
*
|
|
125
|
+
* @param {string} projectPath - Project root path
|
|
126
|
+
* @returns {{locale: string|null, [key: string]: any}} Parsed YAML object, or
|
|
127
|
+
* an empty object when the file is missing/unreadable. `locale` is always
|
|
128
|
+
* present (null when not declared) so callers can safely do `.locale`.
|
|
129
|
+
*/
|
|
130
|
+
export function readInstallYaml(projectPath) {
|
|
131
|
+
if (!projectPath) {
|
|
132
|
+
return { locale: null };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const filePath = path.join(projectPath, '.uds', 'install.yaml');
|
|
136
|
+
if (!fs.existsSync(filePath)) {
|
|
137
|
+
return { locale: null };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
142
|
+
const parsed = yaml.load(content);
|
|
143
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
144
|
+
return { locale: null };
|
|
145
|
+
}
|
|
146
|
+
return { locale: null, ...parsed };
|
|
147
|
+
} catch (error) {
|
|
148
|
+
console.warn(`Warning: Failed to load .uds/install.yaml: ${error.message}`);
|
|
149
|
+
return { locale: null };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -197,13 +197,20 @@ export async function installSkillsForAgent(agent, level, skillNames = null, pro
|
|
|
197
197
|
targetDir,
|
|
198
198
|
installed: [],
|
|
199
199
|
errors: [],
|
|
200
|
-
fileHashes: {} // New: file hashes for installed skills
|
|
200
|
+
fileHashes: {}, // New: file hashes for installed skills
|
|
201
|
+
// List of skill names that requested a localized variant but fell back to
|
|
202
|
+
// the English source because no locale variant exists for them.
|
|
203
|
+
// Populated only when `locale` is a localized locale (not 'en').
|
|
204
|
+
localeFallbacks: []
|
|
201
205
|
};
|
|
202
206
|
|
|
203
207
|
for (const skillName of toInstall) {
|
|
204
208
|
const result = installSingleSkill(skillName, targetDir, locale);
|
|
205
209
|
if (result.success) {
|
|
206
210
|
results.installed.push(skillName);
|
|
211
|
+
if (result.fallbackToEn) {
|
|
212
|
+
results.localeFallbacks.push(skillName);
|
|
213
|
+
}
|
|
207
214
|
} else {
|
|
208
215
|
results.errors.push({ skill: skillName, error: result.error });
|
|
209
216
|
results.success = false;
|
|
@@ -356,11 +363,17 @@ function mergeSkillFrontmatter(enSourceDir, targetDir) {
|
|
|
356
363
|
}
|
|
357
364
|
|
|
358
365
|
/**
|
|
359
|
-
* Install a single skill to a target directory
|
|
366
|
+
* Install a single skill to a target directory.
|
|
367
|
+
*
|
|
368
|
+
* When `locale` requests a localized variant but no localized directory exists
|
|
369
|
+
* for the requested skill, the function silently copies the English source and
|
|
370
|
+
* sets `fallbackToEn: true` on the result so callers can surface a WARN at the
|
|
371
|
+
* end of the install run (XSPEC-239 §Req-3 / P1-CLI-1).
|
|
372
|
+
*
|
|
360
373
|
* @param {string} skillName - Skill name
|
|
361
374
|
* @param {string} targetBaseDir - Target base directory
|
|
362
375
|
* @param {string} locale - Locale for skill content (default: 'en')
|
|
363
|
-
* @returns {
|
|
376
|
+
* @returns {{success: boolean, skillName: string, path?: string, error?: string, fallbackToEn?: boolean}} Result
|
|
364
377
|
*/
|
|
365
378
|
function installSingleSkill(skillName, targetBaseDir, locale = 'en') {
|
|
366
379
|
const enSourceDir = join(SKILLS_LOCAL_DIR, skillName);
|
|
@@ -369,6 +382,7 @@ function installSingleSkill(skillName, targetBaseDir, locale = 'en') {
|
|
|
369
382
|
// Determine the actual source directory based on locale
|
|
370
383
|
let sourceDir = enSourceDir;
|
|
371
384
|
let needsFrontmatterMerge = false;
|
|
385
|
+
let fallbackToEn = false;
|
|
372
386
|
|
|
373
387
|
if (isLocalizedLocale(locale)) {
|
|
374
388
|
const localizedDir = getLocalizedSkillsSourceDir(locale);
|
|
@@ -376,8 +390,11 @@ function installSingleSkill(skillName, targetBaseDir, locale = 'en') {
|
|
|
376
390
|
if (existsSync(localizedSkillDir)) {
|
|
377
391
|
sourceDir = localizedSkillDir;
|
|
378
392
|
needsFrontmatterMerge = true;
|
|
393
|
+
} else {
|
|
394
|
+
// Locale requested but missing for this skill — fall back to English source.
|
|
395
|
+
// Flag the result so the caller can aggregate a WARN.
|
|
396
|
+
fallbackToEn = true;
|
|
379
397
|
}
|
|
380
|
-
// else: fall back to English source
|
|
381
398
|
}
|
|
382
399
|
|
|
383
400
|
if (!existsSync(sourceDir)) {
|
|
@@ -410,12 +427,13 @@ function installSingleSkill(skillName, targetBaseDir, locale = 'en') {
|
|
|
410
427
|
mergeSkillFrontmatter(enSourceDir, targetDir);
|
|
411
428
|
}
|
|
412
429
|
|
|
413
|
-
return { success: true, skillName, path: targetDir };
|
|
430
|
+
return { success: true, skillName, path: targetDir, fallbackToEn };
|
|
414
431
|
} catch (error) {
|
|
415
432
|
return {
|
|
416
433
|
success: false,
|
|
417
434
|
skillName,
|
|
418
|
-
error: error.message
|
|
435
|
+
error: error.message,
|
|
436
|
+
fallbackToEn
|
|
419
437
|
};
|
|
420
438
|
}
|
|
421
439
|
}
|
|
@@ -909,9 +927,14 @@ export async function installSkillsToMultipleAgents(installations, skillNames =
|
|
|
909
927
|
installations: [],
|
|
910
928
|
totalInstalled: 0,
|
|
911
929
|
totalErrors: 0,
|
|
912
|
-
allFileHashes: {} // New: combined file hashes from all installations
|
|
930
|
+
allFileHashes: {}, // New: combined file hashes from all installations
|
|
931
|
+
// Aggregated set of skill names that fell back to English across all
|
|
932
|
+
// agents/levels. Used by the high-level installer to print a single WARN.
|
|
933
|
+
localeFallbacks: []
|
|
913
934
|
};
|
|
914
935
|
|
|
936
|
+
const fallbackSet = new Set();
|
|
937
|
+
|
|
915
938
|
for (const { agent, level } of uniqueInstallations) {
|
|
916
939
|
const result = await installSkillsForAgent(agent, level, skillNames, projectPath, locale);
|
|
917
940
|
results.installations.push(result);
|
|
@@ -926,8 +949,17 @@ export async function installSkillsToMultipleAgents(installations, skillNames =
|
|
|
926
949
|
if (result.fileHashes) {
|
|
927
950
|
Object.assign(results.allFileHashes, result.fileHashes);
|
|
928
951
|
}
|
|
952
|
+
|
|
953
|
+
// Merge locale fallbacks (dedupe across agents)
|
|
954
|
+
if (Array.isArray(result.localeFallbacks)) {
|
|
955
|
+
for (const name of result.localeFallbacks) {
|
|
956
|
+
fallbackSet.add(name);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
929
959
|
}
|
|
930
960
|
|
|
961
|
+
results.localeFallbacks = Array.from(fallbackSet).sort();
|
|
962
|
+
|
|
931
963
|
return results;
|
|
932
964
|
}
|
|
933
965
|
|
package/standards-registry.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.15.1",
|
|
4
4
|
"lastUpdated": "2026-05-13",
|
|
5
5
|
"description": "Standards registry for universal-dev-standards with integrated skills and AI-optimized formats",
|
|
6
6
|
"formats": {
|
|
@@ -58,14 +58,14 @@
|
|
|
58
58
|
"standards": {
|
|
59
59
|
"name": "universal-dev-standards",
|
|
60
60
|
"url": "https://github.com/AsiaOstrich/universal-dev-standards",
|
|
61
|
-
"version": "5.
|
|
61
|
+
"version": "5.15.1"
|
|
62
62
|
},
|
|
63
63
|
"skills": {
|
|
64
64
|
"name": "universal-dev-standards",
|
|
65
65
|
"url": "https://github.com/AsiaOstrich/universal-dev-standards",
|
|
66
66
|
"localPath": "skills",
|
|
67
67
|
"rawUrl": "https://raw.githubusercontent.com/AsiaOstrich/universal-dev-standards/main/skills",
|
|
68
|
-
"version": "5.
|
|
68
|
+
"version": "5.15.1",
|
|
69
69
|
"note": "Skills are now included in the main repository under skills/"
|
|
70
70
|
}
|
|
71
71
|
},
|
|
@@ -1490,6 +1490,18 @@
|
|
|
1490
1490
|
"skillName": null,
|
|
1491
1491
|
"description": "System for capturing, retrieving, and enforcing project-specific context, architectural decisions, and domain knowledge"
|
|
1492
1492
|
},
|
|
1493
|
+
{
|
|
1494
|
+
"id": "knowledge-graph-memory",
|
|
1495
|
+
"name": "Knowledge Graph Memory Standards",
|
|
1496
|
+
"nameZh": "知識圖記憶標準",
|
|
1497
|
+
"source": {
|
|
1498
|
+
"human": "core/knowledge-graph-memory.md",
|
|
1499
|
+
"ai": "ai/standards/knowledge-graph-memory.ai.yaml"
|
|
1500
|
+
},
|
|
1501
|
+
"category": "reference",
|
|
1502
|
+
"skillName": "knowledge-graph",
|
|
1503
|
+
"description": "Relationship schema for traversing specs, decisions, and code as a graph (related/impacts/supersedes front-matter -> IMPACTS/SUPERSEDES edges); complements vector memory with structural multi-hop queries; engine-optional with Markdown degradation; basis for self-evolving (SAGE) confidence"
|
|
1504
|
+
},
|
|
1493
1505
|
{
|
|
1494
1506
|
"id": "context-aware-loading",
|
|
1495
1507
|
"name": "Context-Aware Standard Loading",
|
|
@@ -2338,7 +2350,7 @@
|
|
|
2338
2350
|
"id": "license-compliance",
|
|
2339
2351
|
"name": "License Compliance Standards",
|
|
2340
2352
|
"nameZh": "授權合規標準",
|
|
2341
|
-
"version": "5.
|
|
2353
|
+
"version": "5.15.1",
|
|
2342
2354
|
"source": {
|
|
2343
2355
|
"human": "core/license-compliance.md",
|
|
2344
2356
|
"ai": "ai/standards/license-compliance.ai.yaml"
|