universal-dev-standards 5.14.0 → 5.16.0

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.
Files changed (134) hide show
  1. package/bin/uds.js +2 -0
  2. package/bundled/ai/standards/acceptance-criteria-traceability.ai.yaml +31 -0
  3. package/bundled/ai/standards/ai-instruction-standards.ai.yaml +190 -3
  4. package/bundled/ai/standards/forward-derivation-standards.ai.yaml +23 -0
  5. package/bundled/ai/standards/knowledge-graph-memory.ai.yaml +83 -0
  6. package/bundled/core/acceptance-criteria-traceability.md +46 -0
  7. package/bundled/core/ai-instruction-standards.md +136 -11
  8. package/bundled/core/forward-derivation-standards.md +19 -0
  9. package/bundled/core/knowledge-graph-memory.md +119 -0
  10. package/bundled/locales/COVERAGE.md +226 -0
  11. package/bundled/locales/zh-CN/CHANGELOG.md +42 -3
  12. package/bundled/locales/zh-CN/README.md +1 -1
  13. package/bundled/locales/zh-CN/SECURITY.md +1 -1
  14. package/bundled/locales/zh-CN/core/acceptance-criteria-traceability.md +46 -0
  15. package/bundled/locales/zh-CN/core/ai-instruction-standards.md +111 -5
  16. package/bundled/locales/zh-CN/core/forward-derivation-standards.md +19 -0
  17. package/bundled/locales/zh-CN/skills/ac-coverage/SKILL.md +194 -0
  18. package/bundled/locales/zh-CN/skills/adr-assistant/SKILL.md +135 -40
  19. package/bundled/locales/zh-CN/skills/brainstorm-assistant/SKILL.md +217 -63
  20. package/bundled/locales/zh-CN/skills/brainstorm-assistant/guide.md +599 -0
  21. package/bundled/locales/zh-CN/skills/commands/brainstorm.md +92 -25
  22. package/bundled/locales/zh-CN/skills/commit-standards/SKILL.md +78 -16
  23. package/bundled/locales/zh-CN/skills/contract-test-assistant/SKILL.md +85 -26
  24. package/bundled/locales/zh-CN/skills/deploy-assistant/SKILL.md +189 -0
  25. package/bundled/locales/zh-CN/skills/dev-methodology/SKILL.md +110 -0
  26. package/bundled/locales/zh-CN/skills/dev-methodology/guide.md +255 -0
  27. package/bundled/locales/zh-CN/skills/dev-workflow-guide/SKILL.md +70 -11
  28. package/bundled/locales/zh-CN/skills/journey-test-assistant/SKILL.md +209 -0
  29. package/bundled/locales/zh-CN/skills/knowledge-graph/SKILL.md +58 -0
  30. package/bundled/locales/zh-CN/skills/knowledge-graph/guide.md +74 -0
  31. package/bundled/locales/zh-CN/skills/migration-assistant/SKILL.md +125 -8
  32. package/bundled/locales/zh-CN/skills/observability-assistant/guide.md +188 -0
  33. package/bundled/locales/zh-CN/skills/orchestrate/SKILL.md +173 -0
  34. package/bundled/locales/zh-CN/skills/plan/SKILL.md +240 -0
  35. package/bundled/locales/zh-CN/skills/push/SKILL.md +242 -0
  36. package/bundled/locales/zh-CN/skills/retrospective-assistant/SKILL.md +104 -36
  37. package/bundled/locales/zh-CN/skills/reverse-engineer/SKILL.md +88 -32
  38. package/bundled/locales/zh-CN/skills/runbook-assistant/guide.md +216 -0
  39. package/bundled/locales/zh-CN/skills/skill-builder/SKILL.md +149 -0
  40. package/bundled/locales/zh-CN/skills/slo-assistant/guide.md +188 -0
  41. package/bundled/locales/zh-CN/skills/spec-derivation/SKILL.md +86 -0
  42. package/bundled/locales/zh-CN/skills/spec-derivation/guide.md +476 -0
  43. package/bundled/locales/zh-CN/skills/spec-driven-dev/SKILL.md +155 -81
  44. package/bundled/locales/zh-CN/skills/sweep/SKILL.md +151 -0
  45. package/bundled/locales/zh-CN/skills/testing-guide/SKILL.md +207 -110
  46. package/bundled/locales/zh-TW/CHANGELOG.md +46 -3
  47. package/bundled/locales/zh-TW/README.md +1 -1
  48. package/bundled/locales/zh-TW/SECURITY.md +1 -1
  49. package/bundled/locales/zh-TW/core/acceptance-criteria-traceability.md +46 -0
  50. package/bundled/locales/zh-TW/core/ai-instruction-standards.md +130 -5
  51. package/bundled/locales/zh-TW/core/browser-compatibility-standards.md +222 -5
  52. package/bundled/locales/zh-TW/core/contract-testing-standards.md +184 -5
  53. package/bundled/locales/zh-TW/core/cross-flow-regression.md +192 -5
  54. package/bundled/locales/zh-TW/core/forward-derivation-standards.md +19 -0
  55. package/bundled/locales/zh-TW/core/knowledge-graph-memory.md +127 -0
  56. package/bundled/locales/zh-TW/core/release-readiness-gate.md +186 -5
  57. package/bundled/locales/zh-TW/core/self-review-protocol.md +9 -1
  58. package/bundled/locales/zh-TW/skills/ac-coverage/SKILL.md +192 -0
  59. package/bundled/locales/zh-TW/skills/adr-assistant/SKILL.md +21 -42
  60. package/bundled/locales/zh-TW/skills/ai-collaboration-standards/SKILL.md +5 -1
  61. package/bundled/locales/zh-TW/skills/brainstorm-assistant/SKILL.md +212 -59
  62. package/bundled/locales/zh-TW/skills/brainstorm-assistant/guide.md +266 -579
  63. package/bundled/locales/zh-TW/skills/commands/brainstorm.md +91 -26
  64. package/bundled/locales/zh-TW/skills/commit-standards/SKILL.md +77 -15
  65. package/bundled/locales/zh-TW/skills/contract-test-assistant/SKILL.md +75 -16
  66. package/bundled/locales/zh-TW/skills/deploy-assistant/SKILL.md +187 -0
  67. package/bundled/locales/zh-TW/skills/dev-methodology/SKILL.md +108 -0
  68. package/bundled/locales/zh-TW/skills/dev-methodology/guide.md +255 -0
  69. package/bundled/locales/zh-TW/skills/dev-workflow-guide/SKILL.md +125 -64
  70. package/bundled/locales/zh-TW/skills/journey-test-assistant/SKILL.md +222 -0
  71. package/bundled/locales/zh-TW/skills/knowledge-graph/SKILL.md +56 -0
  72. package/bundled/locales/zh-TW/skills/knowledge-graph/guide.md +74 -0
  73. package/bundled/locales/zh-TW/skills/migration-assistant/SKILL.md +128 -11
  74. package/bundled/locales/zh-TW/skills/observability-assistant/guide.md +188 -0
  75. package/bundled/locales/zh-TW/skills/orchestrate/SKILL.md +173 -0
  76. package/bundled/locales/zh-TW/skills/plan/SKILL.md +240 -0
  77. package/bundled/locales/zh-TW/skills/project-structure-guide/SKILL.md +5 -1
  78. package/bundled/locales/zh-TW/skills/push/SKILL.md +242 -0
  79. package/bundled/locales/zh-TW/skills/retrospective-assistant/SKILL.md +94 -28
  80. package/bundled/locales/zh-TW/skills/reverse-engineer/SKILL.md +84 -28
  81. package/bundled/locales/zh-TW/skills/runbook-assistant/guide.md +216 -0
  82. package/bundled/locales/zh-TW/skills/skill-builder/SKILL.md +165 -0
  83. package/bundled/locales/zh-TW/skills/slo-assistant/guide.md +188 -0
  84. package/bundled/locales/zh-TW/skills/spec-derivation/SKILL.md +83 -0
  85. package/bundled/locales/zh-TW/skills/spec-derivation/guide.md +476 -0
  86. package/bundled/locales/zh-TW/skills/spec-driven-dev/SKILL.md +148 -77
  87. package/bundled/locales/zh-TW/skills/sweep/SKILL.md +149 -0
  88. package/bundled/locales/zh-TW/skills/testing-guide/SKILL.md +141 -44
  89. package/bundled/skills/adr-assistant/SKILL.md +1 -1
  90. package/bundled/skills/ai-collaboration-standards/SKILL.md +1 -1
  91. package/bundled/skills/ai-friendly-architecture/SKILL.md +1 -1
  92. package/bundled/skills/ai-instruction-standards/SKILL.md +1 -1
  93. package/bundled/skills/api-design-assistant/SKILL.md +1 -1
  94. package/bundled/skills/audit-assistant/SKILL.md +1 -1
  95. package/bundled/skills/brainstorm-assistant/SKILL.md +142 -106
  96. package/bundled/skills/brainstorm-assistant/guide.md +256 -661
  97. package/bundled/skills/ci-cd-assistant/SKILL.md +1 -1
  98. package/bundled/skills/commands/brainstorm.md +51 -30
  99. package/bundled/skills/contract-test-assistant/SKILL.md +1 -1
  100. package/bundled/skills/database-assistant/SKILL.md +1 -1
  101. package/bundled/skills/deploy-assistant/SKILL.md +1 -1
  102. package/bundled/skills/documentation-guide/SKILL.md +1 -1
  103. package/bundled/skills/error-code-guide/SKILL.md +1 -1
  104. package/bundled/skills/git-workflow-guide/SKILL.md +1 -1
  105. package/bundled/skills/incident-response-assistant/SKILL.md +1 -1
  106. package/bundled/skills/journey-test-assistant/SKILL.md +1 -1
  107. package/bundled/skills/knowledge-graph/SKILL.md +58 -0
  108. package/bundled/skills/knowledge-graph/guide.md +69 -0
  109. package/bundled/skills/logging-guide/SKILL.md +1 -1
  110. package/bundled/skills/observability-assistant/SKILL.md +1 -1
  111. package/bundled/skills/orchestrate/SKILL.md +1 -1
  112. package/bundled/skills/plan/SKILL.md +1 -1
  113. package/bundled/skills/pr-automation-assistant/SKILL.md +1 -1
  114. package/bundled/skills/project-structure-guide/SKILL.md +1 -1
  115. package/bundled/skills/push/SKILL.md +1 -1
  116. package/bundled/skills/retrospective-assistant/SKILL.md +1 -1
  117. package/bundled/skills/reverse-engineer/SKILL.md +1 -1
  118. package/bundled/skills/runbook-assistant/SKILL.md +1 -1
  119. package/bundled/skills/security-assistant/SKILL.md +1 -1
  120. package/bundled/skills/security-scan-assistant/SKILL.md +1 -1
  121. package/bundled/skills/slo-assistant/SKILL.md +1 -1
  122. package/bundled/skills/sweep/SKILL.md +1 -1
  123. package/bundled/skills/testing-guide/SKILL.md +1 -1
  124. package/package.json +2 -2
  125. package/src/commands/check.js +80 -0
  126. package/src/commands/init.js +8 -1
  127. package/src/commands/update.js +49 -14
  128. package/src/i18n/messages.js +32 -5
  129. package/src/installers/skills-installer.js +49 -0
  130. package/src/lint/i18n.js +424 -0
  131. package/src/utils/config-manager.js +39 -0
  132. package/src/utils/skills-installer.js +39 -7
  133. package/standards-registry.json +16 -4
  134. package/bundled/locales/zh-TW/docs/SKILL-FALLBACK-GUIDE.md +0 -407
@@ -0,0 +1,424 @@
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) version-gap (needs a version on the canonical)
12
+ * translation-content-drift-warn (warn) source_hash mismatch — catches SILENT drift, version-independent (XSPEC-248)
13
+ * translation-hash-missing (info) blind-spot marker: no canonical version AND no source_hash → freshness unverifiable (XSPEC-248)
14
+ *
15
+ * `adopter-must-match-installed-locale` is deferred (needs project context,
16
+ * not source-tree level).
17
+ *
18
+ * @module lint/i18n
19
+ */
20
+
21
+ import { existsSync, readFileSync, readdirSync } from 'fs';
22
+ import { join, dirname, basename, resolve } from 'path';
23
+ import { createHash } from 'crypto';
24
+
25
+ // CJK Unified Ideographs ranges (excluding fullwidth punctuation, but
26
+ // including ranges Ext-A/B used by zh-TW/zh-CN). Hangul, Hiragana,
27
+ // Katakana included to also flag accidentally-mixed CJK content.
28
+ const CJK_REGEX = /[㐀-䶿一-鿿豈-﫿぀-ゟ゠-ヿ가-힯]/;
29
+
30
+ // ASCII-only regex (allow printable ASCII + space + common punctuation).
31
+ // Anything outside extended-ASCII counts as non-ASCII for description purposes.
32
+ // Control chars \x09 (tab), \x0A (LF), \x0D (CR) are intentionally allowed.
33
+ // eslint-disable-next-line no-control-regex
34
+ const NON_ASCII_REGEX = /[^\x09\x0A\x0D\x20-\x7E]/;
35
+
36
+ // Global variant of CJK_REGEX (same character ranges, derived to avoid
37
+ // re-typing the ranges) — used to *count* CJK characters for the
38
+ // l3-language-consistency ratio check (bilingual templates vs. real drift).
39
+ const CJK_GLOBAL_REGEX = new RegExp(CJK_REGEX.source, 'g');
40
+
41
+ /**
42
+ * Parse a minimal YAML front matter from markdown content.
43
+ * Returns { fm: {key→value}, fmEndLine: number, body: string }
44
+ * If there is no front matter, returns fm=null, fmEndLine=0, body=full content.
45
+ */
46
+ function parseFrontmatter(content) {
47
+ const lines = content.split('\n');
48
+ if (lines[0] !== '---') {
49
+ return { fm: null, fmEndLine: 0, body: content };
50
+ }
51
+ const fm = {};
52
+ let fmEndLine = -1;
53
+ for (let i = 1; i < lines.length; i++) {
54
+ if (lines[i] === '---') {
55
+ fmEndLine = i;
56
+ break;
57
+ }
58
+ const m = lines[i].match(/^([a-zA-Z_][\w-]*):\s*(.*)$/);
59
+ if (m) {
60
+ let value = m[2].trim();
61
+ // Multi-line block scalar (| or >) — collect lines until indentation breaks
62
+ if (value === '|' || value === '>') {
63
+ const blockLines = [];
64
+ for (let j = i + 1; j < lines.length; j++) {
65
+ if (lines[j] === '---') break;
66
+ if (lines[j].startsWith(' ') || lines[j] === '') {
67
+ blockLines.push(lines[j].replace(/^ {0,2}/, ''));
68
+ } else {
69
+ break;
70
+ }
71
+ }
72
+ value = blockLines.join('\n').trim();
73
+ } else if ((value.startsWith('"') && value.endsWith('"')) ||
74
+ (value.startsWith("'") && value.endsWith("'"))) {
75
+ value = value.slice(1, -1);
76
+ }
77
+ fm[m[1]] = { value, line: i + 1 };
78
+ }
79
+ }
80
+ if (fmEndLine === -1) {
81
+ return { fm: null, fmEndLine: 0, body: content };
82
+ }
83
+ const body = lines.slice(fmEndLine + 1).join('\n');
84
+ return { fm, fmEndLine, body };
85
+ }
86
+
87
+ function semverParts(v) {
88
+ if (!v) return null;
89
+ const stripped = String(v).split('-')[0];
90
+ const parts = stripped.split('.').map(n => parseInt(n, 10));
91
+ if (parts.some(Number.isNaN)) return null;
92
+ return parts;
93
+ }
94
+
95
+ function isMajorOrMinorGap(translation, source, maxMinorGap = 2) {
96
+ const t = semverParts(translation);
97
+ const s = semverParts(source);
98
+ if (!t || !s) return null;
99
+ if ((t[0] || 0) !== (s[0] || 0)) return { kind: 'major' };
100
+ const gap = (s[1] || 0) - (t[1] || 0);
101
+ if (gap > maxMinorGap) return { kind: 'minor', gap };
102
+ return null;
103
+ }
104
+
105
+ /**
106
+ * Look up the source markdown's version from its first 30 lines.
107
+ * Supports YAML frontmatter `version:` plus `**Version**: x.y.z` patterns.
108
+ */
109
+ function readSourceVersion(filePath) {
110
+ if (!existsSync(filePath)) return null;
111
+ const head = readFileSync(filePath, 'utf-8').split('\n').slice(0, 30);
112
+ for (const line of head) {
113
+ let m = line.match(/^\s*version:\s*(\S+)/i);
114
+ if (m) return m[1].trim();
115
+ m = line.match(/^\s*\*\*Version\*\*:\s*([\d][^|\s]*)/i);
116
+ if (m) return m[1].trim();
117
+ m = line.match(/^\s*>\s*Version:\s*([\d][^|\s]*)/i);
118
+ if (m) return m[1].trim();
119
+ }
120
+ return null;
121
+ }
122
+
123
+ /**
124
+ * Compute a stable content hash of a canonical file: sha256 of its content
125
+ * with CRLF normalized and trailing whitespace stripped, truncated to 12 hex.
126
+ *
127
+ * Used for `source_hash` drift detection (XSPEC-248), which — unlike the
128
+ * version-gap check — does NOT depend on the canonical carrying a `version:`
129
+ * field. This is what catches "silent drift": canonical content changed but
130
+ * nobody bumped the locale's version metadata.
131
+ *
132
+ * @param {string} filePath - Absolute path to canonical file
133
+ * @returns {string|null} 12-hex digest, or null if file is missing
134
+ */
135
+ export function computeSourceHash(filePath) {
136
+ if (!existsSync(filePath)) return null;
137
+ const normalized = readFileSync(filePath, 'utf-8')
138
+ .replace(/\r\n/g, '\n')
139
+ .replace(/\s+$/, '');
140
+ return createHash('sha256').update(normalized).digest('hex').slice(0, 12);
141
+ }
142
+
143
+ /**
144
+ * Lint a canonical SKILL.md or core/*.md file for English-only frontmatter
145
+ * and L3 language consistency.
146
+ *
147
+ * @param {string} skillMdPath - Absolute path to canonical file
148
+ * @returns {Array<{rule, severity, line, message, file}>}
149
+ */
150
+ export function lintCanonical(skillMdPath, opts = {}) {
151
+ // opts.isGuide — canonical guide.md (XSPEC-248): guide descriptions
152
+ // legitimately carry CJK discoverability keywords (e.g. "可觀測性"), so the
153
+ // ASCII-description rule does NOT apply. The l3 body check still does (and
154
+ // runs regardless of description language, since there is no ASCII rule to
155
+ // double-report against).
156
+ const { isGuide = false } = opts;
157
+ const findings = [];
158
+ if (!existsSync(skillMdPath)) return findings;
159
+ const content = readFileSync(skillMdPath, 'utf-8');
160
+ const { fm, body } = parseFrontmatter(content);
161
+
162
+ // Rule: canonical:description-must-be-ascii (SKILL.md / core only; not guides)
163
+ if (!isGuide && fm && fm.description) {
164
+ if (NON_ASCII_REGEX.test(fm.description.value)) {
165
+ findings.push({
166
+ rule: 'canonical:description-must-be-ascii',
167
+ severity: 'error',
168
+ line: fm.description.line,
169
+ file: skillMdPath,
170
+ message: 'Canonical `description` must be ASCII-only (English). Found non-ASCII characters; move translation to locale variant.'
171
+ });
172
+ }
173
+ }
174
+
175
+ // Rule: canonical:l3-language-consistency
176
+ // Heuristic: scan code-block-style output templates (```...```) for
177
+ // non-English example response text. For SKILL.md/core only run when the
178
+ // description is ASCII (otherwise the description-must-be-ascii error already
179
+ // covers it); for guides (no ASCII rule) always scan the body.
180
+ const runL3 = isGuide
181
+ ? true
182
+ : Boolean(fm && fm.description && !NON_ASCII_REGEX.test(fm.description.value));
183
+ if (runL3) {
184
+ const fenceRegex = /```([a-zA-Z0-9_-]*)\n([\s\S]*?)```/g;
185
+ let match;
186
+ while ((match = fenceRegex.exec(body)) !== null) {
187
+ const lang = match[1];
188
+ const block = match[2];
189
+ // Skip code fences for known programming languages — those legitimately
190
+ // contain non-English string literals.
191
+ const skipLangs = new Set(['', 'json', 'yaml', 'yml', 'toml', 'ini',
192
+ 'sh', 'bash', 'zsh', 'powershell',
193
+ 'js', 'ts', 'tsx', 'jsx', 'javascript', 'typescript',
194
+ 'py', 'python', 'rb', 'ruby', 'go', 'rust', 'java', 'c', 'cpp',
195
+ 'html', 'css', 'sql', 'diff', 'patch']);
196
+ if (skipLangs.has(lang.toLowerCase())) continue;
197
+
198
+ // Likely an output template / example response (e.g., ```markdown,
199
+ // ```text, ```output, ```response). Only flag when the block is
200
+ // *predominantly* CJK — i.e. an untranslated example dumped into
201
+ // canonical. Deliberate bilingual `English | 中文` templates (house
202
+ // style) stay English-dominant, so they are allowed; a Chinese-only
203
+ // example response is CJK-dominant and still caught. (XSPEC-248 l3
204
+ // refinement — rule vs. bilingual-convention tension.)
205
+ if (CJK_REGEX.test(block)) {
206
+ const cjkCount = (block.match(CJK_GLOBAL_REGEX) || []).length;
207
+ const asciiLetterCount = (block.match(/[A-Za-z]/g) || []).length;
208
+ if (cjkCount > asciiLetterCount) {
209
+ // Compute approx. line number of the fence start
210
+ const lineNum = content.substring(0, match.index).split('\n').length;
211
+ findings.push({
212
+ rule: 'canonical:l3-language-consistency',
213
+ severity: 'warn',
214
+ line: lineNum,
215
+ file: skillMdPath,
216
+ message: `Canonical body contains a predominantly-CJK example response inside a non-code fenced block (lang=\`${lang || 'plain'}\`). Move the translated example to the locale variant (bilingual \`EN | 中文\` templates are allowed).`
217
+ });
218
+ // Only report first occurrence per file to keep output readable
219
+ break;
220
+ }
221
+ }
222
+ }
223
+ }
224
+
225
+ return findings;
226
+ }
227
+
228
+ /**
229
+ * Lint a locale variant (e.g. locales/zh-TW/skills/foo/SKILL.md or
230
+ * locales/zh-CN/core/some-standard.md).
231
+ *
232
+ * @param {string} skillMdPath - Absolute path to locale variant file
233
+ * @param {string} locale - Locale code (e.g. 'zh-TW', 'zh-CN')
234
+ * @returns {Array<{rule, severity, line, message, file}>}
235
+ */
236
+ export function lintLocale(skillMdPath, locale) {
237
+ const findings = [];
238
+ if (!existsSync(skillMdPath)) return findings;
239
+ const content = readFileSync(skillMdPath, 'utf-8');
240
+ const { fm } = parseFrontmatter(content);
241
+
242
+ // Rule: locale:must-have-source-frontmatter
243
+ if (!fm) {
244
+ findings.push({
245
+ rule: 'locale:must-have-source-frontmatter',
246
+ severity: 'error',
247
+ line: 1,
248
+ file: skillMdPath,
249
+ message: 'Locale variant is missing YAML front matter (must include `source:`, `source_version:`, `translation_version:`).'
250
+ });
251
+ return findings;
252
+ }
253
+
254
+ const required = ['source', 'source_version', 'translation_version'];
255
+ const missing = required.filter(k => !fm[k]);
256
+ if (missing.length > 0) {
257
+ findings.push({
258
+ rule: 'locale:must-have-source-frontmatter',
259
+ severity: 'error',
260
+ line: 1,
261
+ file: skillMdPath,
262
+ message: `Locale variant is missing required frontmatter field(s): ${missing.join(', ')}.`
263
+ });
264
+ }
265
+
266
+ // Rule: locale:description-must-match-language
267
+ // For zh-TW / zh-CN, the description (if present) must contain CJK.
268
+ if (fm.description) {
269
+ const expectsCjk = locale === 'zh-TW' || locale === 'zh-CN' ||
270
+ locale.startsWith('zh-') || locale === 'ja-JP' ||
271
+ locale === 'ko-KR';
272
+ if (expectsCjk && !CJK_REGEX.test(fm.description.value)) {
273
+ findings.push({
274
+ rule: 'locale:description-must-match-language',
275
+ severity: 'error',
276
+ line: fm.description.line,
277
+ file: skillMdPath,
278
+ message: `Locale \`${locale}\` variant has ASCII-only \`description\`; locale variants must contain ${locale}-script characters (likely chimera).`
279
+ });
280
+ }
281
+ }
282
+
283
+ // Rules: translation-drift-warn (version) + translation-content-drift-warn (hash) + translation-hash-missing
284
+ if (fm.source && fm.translation_version) {
285
+ // Resolve source path relative to this file's directory
286
+ const sourceRel = fm.source.value;
287
+ const sourcePath = resolve(dirname(skillMdPath), sourceRel);
288
+ const actualSourceVersion = readSourceVersion(sourcePath);
289
+
290
+ // (a) version-gap drift — only works when the canonical carries a version
291
+ if (actualSourceVersion) {
292
+ const gap = isMajorOrMinorGap(fm.translation_version.value, actualSourceVersion);
293
+ if (gap) {
294
+ const detail = gap.kind === 'major'
295
+ ? 'major-version gap'
296
+ : `${gap.gap} minor versions behind`;
297
+ findings.push({
298
+ rule: 'translation-drift-warn',
299
+ severity: 'warn',
300
+ line: fm.translation_version.line,
301
+ file: skillMdPath,
302
+ message: `translation_version \`${fm.translation_version.value}\` lags source_version \`${actualSourceVersion}\` (${detail}). Re-sync recommended.`
303
+ });
304
+ }
305
+ }
306
+
307
+ // (b) content-hash drift — version-independent; catches SILENT drift (XSPEC-248)
308
+ if (fm.source_hash) {
309
+ const currentHash = computeSourceHash(sourcePath);
310
+ if (currentHash && currentHash !== fm.source_hash.value) {
311
+ findings.push({
312
+ rule: 'translation-content-drift-warn',
313
+ severity: 'warn',
314
+ line: fm.source_hash.line,
315
+ file: skillMdPath,
316
+ message: `Canonical content changed since last sync (source_hash \`${fm.source_hash.value}\` != current \`${currentHash}\`). Re-translate and update source_hash.`
317
+ });
318
+ }
319
+ } else if (!actualSourceVersion) {
320
+ // (c) blind-spot marker: neither a canonical version NOR a source_hash exists,
321
+ // so content freshness cannot be verified at all (how brainstorm rotted to v1.0).
322
+ findings.push({
323
+ rule: 'translation-hash-missing',
324
+ severity: 'info',
325
+ line: fm.source.line,
326
+ file: skillMdPath,
327
+ message: 'Cannot verify content freshness: canonical has no version field and locale has no source_hash. Stamp source_hash (sha256[:12] of canonical) to enable silent-drift detection.'
328
+ });
329
+ }
330
+ }
331
+
332
+ return findings;
333
+ }
334
+
335
+ /**
336
+ * Batch-lint a UDS project tree. Scans canonical `skills/` (SKILL.md + guide.md)
337
+ * and `core/`, plus `locales/{locale}/skills/` (SKILL.md + guide.md) and
338
+ * `locales/{locale}/core/`. Canonical guide.md uses the guide ruleset
339
+ * (CJK discoverability keywords allowed in description; XSPEC-248).
340
+ *
341
+ * @param {object} opts
342
+ * @param {string} opts.projectPath - Project (or UDS) root path
343
+ * @param {string[]} [opts.locales] - Locales to lint (defaults to discovering
344
+ * every subdirectory of `locales/`)
345
+ * @returns {Array<{rule, severity, line, message, file}>}
346
+ */
347
+ export function lintAll({ projectPath, locales }) {
348
+ const findings = [];
349
+
350
+ const skillsDir = join(projectPath, 'skills');
351
+ const coreDir = join(projectPath, 'core');
352
+ const localesDir = join(projectPath, 'locales');
353
+
354
+ // --- Canonical skills (SKILL.md + guide.md) ---
355
+ if (existsSync(skillsDir)) {
356
+ for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
357
+ if (!entry.isDirectory()) continue;
358
+ const skillFile = join(skillsDir, entry.name, 'SKILL.md');
359
+ if (existsSync(skillFile)) {
360
+ findings.push(...lintCanonical(skillFile));
361
+ }
362
+ // XSPEC-248: guide.md uses the guide ruleset (CJK description allowed).
363
+ const guideFile = join(skillsDir, entry.name, 'guide.md');
364
+ if (existsSync(guideFile)) {
365
+ findings.push(...lintCanonical(guideFile, { isGuide: true }));
366
+ }
367
+ }
368
+ }
369
+
370
+ // --- Canonical core/*.md ---
371
+ if (existsSync(coreDir)) {
372
+ for (const entry of readdirSync(coreDir, { withFileTypes: true })) {
373
+ if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
374
+ findings.push(...lintCanonical(join(coreDir, entry.name)));
375
+ }
376
+ }
377
+
378
+ // --- Locale variants ---
379
+ if (existsSync(localesDir)) {
380
+ const candidateLocales = locales || readdirSync(localesDir, { withFileTypes: true })
381
+ .filter(d => d.isDirectory())
382
+ .map(d => d.name)
383
+ .filter(name => /^[a-z]{2}(-[A-Z]{2})?$/.test(name));
384
+
385
+ for (const locale of candidateLocales) {
386
+ const localeSkillsDir = join(localesDir, locale, 'skills');
387
+ const localeCoreDir = join(localesDir, locale, 'core');
388
+
389
+ if (existsSync(localeSkillsDir)) {
390
+ for (const entry of readdirSync(localeSkillsDir, { withFileTypes: true })) {
391
+ if (!entry.isDirectory()) continue;
392
+ const skillFile = join(localeSkillsDir, entry.name, 'SKILL.md');
393
+ if (existsSync(skillFile)) {
394
+ findings.push(...lintLocale(skillFile, locale));
395
+ }
396
+ // XSPEC-248: locale guide.md reuses the locale ruleset (freshness checks).
397
+ const guideFile = join(localeSkillsDir, entry.name, 'guide.md');
398
+ if (existsSync(guideFile)) {
399
+ findings.push(...lintLocale(guideFile, locale));
400
+ }
401
+ }
402
+ }
403
+ if (existsSync(localeCoreDir)) {
404
+ for (const entry of readdirSync(localeCoreDir, { withFileTypes: true })) {
405
+ if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
406
+ findings.push(...lintLocale(join(localeCoreDir, entry.name), locale));
407
+ }
408
+ }
409
+ }
410
+ }
411
+
412
+ return findings;
413
+ }
414
+
415
+ /**
416
+ * Convenience helper: split findings into errors and warnings.
417
+ */
418
+ export function partitionFindings(findings) {
419
+ return {
420
+ errors: findings.filter(f => f.severity === 'error'),
421
+ warnings: findings.filter(f => f.severity === 'warn'),
422
+ infos: findings.filter(f => f.severity === 'info'),
423
+ };
424
+ }
@@ -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 {Object} Result
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
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "version": "5.14.0",
3
+ "version": "5.16.0",
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.14.0"
61
+ "version": "5.16.0"
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.14.0",
68
+ "version": "5.16.0",
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.14.0",
2353
+ "version": "5.16.0",
2342
2354
  "source": {
2343
2355
  "human": "core/license-compliance.md",
2344
2356
  "ai": "ai/standards/license-compliance.ai.yaml"