universal-dev-standards 6.14.0-beta.3 → 6.14.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/bundled/ai/standards/ai-response-navigation.ai.yaml +43 -3
  2. package/bundled/ai/standards/checkin-standards.ai.yaml +25 -6
  3. package/bundled/ai/standards/pipeline-security-gates.ai.yaml +5 -1
  4. package/bundled/core/ai-response-navigation.md +128 -12
  5. package/bundled/extensions/frameworks/fat-free-patterns.md +937 -0
  6. package/bundled/extensions/languages/csharp-style.md +464 -0
  7. package/bundled/extensions/languages/php/fat-free-patterns.md +915 -0
  8. package/bundled/extensions/languages/php/php-style.md +693 -0
  9. package/bundled/extensions/languages/php-style.md +700 -0
  10. package/bundled/extensions/locales/zh-cn.md +717 -0
  11. package/bundled/extensions/locales/zh-tw.md +717 -0
  12. package/bundled/locales/COVERAGE.md +5 -4
  13. package/bundled/locales/zh-CN/CHANGELOG.md +27 -3
  14. package/bundled/locales/zh-CN/README.md +2 -2
  15. package/bundled/locales/zh-CN/SECURITY.md +1 -1
  16. package/bundled/locales/zh-CN/core/ai-response-navigation.md +110 -12
  17. package/bundled/locales/zh-CN/skills/README.md +1 -0
  18. package/bundled/locales/zh-CN/skills/comprehension-ladder/SKILL.md +289 -0
  19. package/bundled/locales/zh-CN/skills/comprehension-ladder/eval-cases.md +261 -0
  20. package/bundled/locales/zh-TW/CHANGELOG.md +27 -3
  21. package/bundled/locales/zh-TW/README.md +2 -2
  22. package/bundled/locales/zh-TW/SECURITY.md +1 -1
  23. package/bundled/locales/zh-TW/core/ai-response-navigation.md +110 -12
  24. package/bundled/locales/zh-TW/skills/README.md +1 -0
  25. package/bundled/locales/zh-TW/skills/comprehension-ladder/SKILL.md +289 -0
  26. package/bundled/locales/zh-TW/skills/comprehension-ladder/eval-cases.md +261 -0
  27. package/bundled/skills/README.md +1 -0
  28. package/bundled/skills/comprehension-ladder/SKILL.md +283 -0
  29. package/bundled/skills/comprehension-ladder/eval-cases.md +255 -0
  30. package/package.json +2 -2
  31. package/src/commands/init.js +29 -9
  32. package/src/commands/update.js +3 -2
  33. package/src/installers/standards-installer.js +16 -23
  34. package/src/reconciler/plan-executor.js +10 -11
  35. package/src/uninstallers/hook-uninstaller.js +5 -3
  36. package/src/utils/copier.js +57 -0
  37. package/src/utils/git-hooks.js +8 -4
  38. package/src/utils/locale.js +19 -0
  39. package/standards-registry.json +21 -7
@@ -7,7 +7,7 @@ import {
7
7
  getOptionSource,
8
8
  findOption
9
9
  } from '../utils/registry.js';
10
- import { copyStandard } from '../utils/copier.js';
10
+ import { copyStandard, copyExtension } from '../utils/copier.js';
11
11
  import { t } from '../i18n/messages.js';
12
12
  import { computeFileHash } from '../utils/hasher.js';
13
13
  import { MANIFEST_OPTION_BINDINGS } from '../core/constants.js';
@@ -115,36 +115,29 @@ export async function installStandards(config, projectPath) {
115
115
  if (config.languages.length > 0 || config.frameworks.length > 0 || localeExtension) {
116
116
  const extSpinner = createSpinner(msg.copyingExtensions).start();
117
117
 
118
- for (const lang of config.languages) {
119
- if (EXTENSION_MAPPINGS[lang]) {
120
- const result = await copyStandard(EXTENSION_MAPPINGS[lang], '.standards', projectPath);
121
- if (result.success) {
122
- results.extensions.push(EXTENSION_MAPPINGS[lang]);
123
- } else {
124
- results.errors.push(`${EXTENSION_MAPPINGS[lang]}: ${result.error}`);
125
- }
118
+ // One helper, one copy call. Extension files come from the installed package only:
119
+ // a declared extension that is missing from it is an error naming that file, never a
120
+ // download and never a silent skip (XSPEC-452 R2).
121
+ const installExtension = async (sourcePath) => {
122
+ const result = await copyExtension(sourcePath, '.standards', projectPath);
123
+ if (result.success) {
124
+ results.extensions.push(sourcePath);
125
+ } else {
126
+ results.errors.push(`${sourcePath}: ${result.error}`);
126
127
  }
128
+ };
129
+
130
+ for (const lang of config.languages) {
131
+ if (EXTENSION_MAPPINGS[lang]) await installExtension(EXTENSION_MAPPINGS[lang]);
127
132
  }
128
133
 
129
134
  for (const fw of config.frameworks) {
130
- if (EXTENSION_MAPPINGS[fw]) {
131
- const result = await copyStandard(EXTENSION_MAPPINGS[fw], '.standards', projectPath);
132
- if (result.success) {
133
- results.extensions.push(EXTENSION_MAPPINGS[fw]);
134
- } else {
135
- results.errors.push(`${EXTENSION_MAPPINGS[fw]}: ${result.error}`);
136
- }
137
- }
135
+ if (EXTENSION_MAPPINGS[fw]) await installExtension(EXTENSION_MAPPINGS[fw]);
138
136
  }
139
137
 
140
138
  // Auto-install locale extension based on display language
141
139
  if (localeExtension && EXTENSION_MAPPINGS[localeExtension]) {
142
- const result = await copyStandard(EXTENSION_MAPPINGS[localeExtension], '.standards', projectPath);
143
- if (result.success) {
144
- results.extensions.push(EXTENSION_MAPPINGS[localeExtension]);
145
- } else {
146
- results.errors.push(`${EXTENSION_MAPPINGS[localeExtension]}: ${result.error}`);
147
- }
140
+ await installExtension(EXTENSION_MAPPINGS[localeExtension]);
148
141
  }
149
142
 
150
143
  extSpinner.succeed(msg.copiedExtensions.replace('{count}', results.extensions.length));
@@ -249,18 +249,17 @@ async function executeCreateOrUpdate(projectPath, action, manifest) {
249
249
  let targetDir = '.standards';
250
250
 
251
251
  if (metadata?.extensionSource) {
252
- // An entry from `manifest.extensions`. PathResolver cannot resolve these
253
- // from an npm install: the published package's `files` list is bin, src,
254
- // bundled, standards-registry.json and README.md — no `extensions/`. So
255
- // `sourcePath` is null for every adopter who did not install from a
256
- // source checkout, and this function answered "No source path available"
257
- // for a file it was perfectly able to fetch.
252
+ // An entry from `manifest.extensions`. It is resolved from the installed
253
+ // package (`bundled/extensions/`, shipped since XSPEC-452) — never from GitHub.
254
+ // `copyStandard` hands any `extensions/` path to `copyExtension`, which fails
255
+ // by name when the package lacks the file, instead of downloading it.
258
256
  //
259
- // The legacy update path never had the bug because it calls copyStandard
260
- // directly (`update.js`, "Update extensions"). The same upgrade therefore
261
- // refreshed `.standards/zh-tw.md` under `uds update` and failed under the
262
- // reconciler — the two paths disagreed about whether the file was
263
- // reachable, and only one of them was right. (XSPEC-343)
257
+ // History: before XSPEC-452 the published package had no `extensions/`, so
258
+ // `sourcePath` was null for every adopter who did not install from a source
259
+ // checkout, and this function answered "No source path available" for a file it
260
+ // could fetch from GitHub. The legacy update path (`update.js`, "Update
261
+ // extensions") was the one that worked — only because it fell back to a
262
+ // download. (XSPEC-343)
264
263
  sourceStr = metadata.extensionSource;
265
264
  } else if (metadata?.registryEntry) {
266
265
  const source = metadata.registryEntry.source;
@@ -426,11 +426,13 @@ export function uninstallHook(projectPath, options = {}) {
426
426
  if (!dryRun) unlinkSync(nativeHookPath);
427
427
  result.removed.push(`.git/hooks/pre-commit (UDS native hook, file removed — ${proof.why})`);
428
428
  result.deletedPaths.push('.git/hooks/pre-commit');
429
- } else if (!NATIVE_UDS_LINE.test(content)) {
429
+ } else if (!NATIVE_UDS_LINE.test(content) && !stripUdsHookBlock(content).removed) {
430
430
  result.skipped.push('.git/hooks/pre-commit (no UDS lines found)');
431
431
  } else {
432
- const lines = content.split('\n');
433
- const filtered = lines.filter(line => !NATIVE_UDS_LINE.test(line));
432
+ // The block the native hook carries is removed whole, by its markers — its
433
+ // inner lines are not individually matchable (same as the husky path above).
434
+ const filtered = stripUdsHookBlock(content).content.split('\n')
435
+ .filter(line => !NATIVE_UDS_LINE.test(line));
434
436
  if (nothingButShebang(filtered)) {
435
437
  if (!dryRun) unlinkSync(nativeHookPath);
436
438
  result.removed.push('.git/hooks/pre-commit (UDS native hook, file removed)');
@@ -26,15 +26,72 @@ function getSourcePath(sourcePath) {
26
26
  return PathResolver.getStandardSource(sourcePath);
27
27
  }
28
28
 
29
+ /**
30
+ * Is this source path one of the add-on files under `extensions/`?
31
+ * @param {string} sourcePath - Relative path from repo root
32
+ * @returns {boolean}
33
+ */
34
+ export function isExtensionPath(sourcePath) {
35
+ return typeof sourcePath === 'string' && sourcePath.replace(/\\/g, '/').startsWith('extensions/');
36
+ }
37
+
38
+ /**
39
+ * Copy an extension file (language style guide, framework pattern, locale pack)
40
+ * into the target project — from the installed package ONLY.
41
+ *
42
+ * 🔴 There is deliberately no download fallback here (XSPEC-452 R2). `extensions/`
43
+ * used to be missing from the npm package, so the copy below fell through to
44
+ * `raw.githubusercontent.com/.../main`: offline installs failed, an install pulled
45
+ * whatever `main` held that day instead of the version the adopter had installed,
46
+ * and a file that did not exist anywhere (`extensions/locales/zh-cn.md`, XSPEC-451)
47
+ * hid behind that fallback until someone ran the locale. A declared extension that
48
+ * is not in the package is a packaging defect, so it fails — by name.
49
+ *
50
+ * @param {string} sourcePath - Relative path from repo root (e.g., 'extensions/locales/zh-tw.md')
51
+ * @param {string} targetDir - Target directory (usually '.standards')
52
+ * @param {string} projectPath - Project root path
53
+ * @returns {Promise<Object>} Result with success status and copied path
54
+ */
55
+ export async function copyExtension(sourcePath, targetDir, projectPath) {
56
+ try {
57
+ const source = getSourcePath(sourcePath);
58
+ if (!source) {
59
+ return failure(
60
+ `Extension file not found in the installed package: ${sourcePath} (extension files are never downloaded; reinstall or upgrade universal-dev-standards)`,
61
+ ERROR_CODES.FILE_NOT_FOUND,
62
+ { sourcePath, targetDir }
63
+ );
64
+ }
65
+ const targetFolder = join(projectPath, targetDir);
66
+ const targetFile = join(targetFolder, basename(sourcePath));
67
+ if (!existsSync(targetFolder)) {
68
+ mkdirSync(targetFolder, { recursive: true });
69
+ }
70
+ copyFileSync(source, targetFile);
71
+ return success(targetFile, { source: 'local', sourcePath, targetFile });
72
+ } catch (error) {
73
+ return failure(
74
+ error.message,
75
+ ERROR_CODES.FILE_COPY_FAILED,
76
+ { sourcePath, targetDir, projectPath }
77
+ );
78
+ }
79
+ }
80
+
29
81
  /**
30
82
  * Copy a standard file to target project
31
83
  * Falls back to downloading from GitHub if local file not found
84
+ * (except for `extensions/` files, which are package-only — see copyExtension)
32
85
  * @param {string} sourcePath - Relative path from repo root (e.g., 'core/anti-hallucination.md')
33
86
  * @param {string} targetDir - Target directory (usually '.standards')
34
87
  * @param {string} projectPath - Project root path
35
88
  * @returns {Promise<Object>} Result with success status and copied path
36
89
  */
37
90
  export async function copyStandard(sourcePath, targetDir, projectPath) {
91
+ // Extensions never take the GitHub fallback below, whichever caller got here (XSPEC-452 R2).
92
+ if (isExtensionPath(sourcePath)) {
93
+ return copyExtension(sourcePath, targetDir, projectPath);
94
+ }
38
95
  try {
39
96
  const targetFolder = join(projectPath, targetDir);
40
97
  const targetFile = join(targetFolder, basename(sourcePath));
@@ -189,11 +189,15 @@ export function hookRunsUdsCheck(content) {
189
189
  * - Not installed: say what is missing and how to fix it, exit non-zero. It
190
190
  * neither skips the check nor downloads anything.
191
191
  *
192
- * @param {{args?: string}} [opts] extra arguments to keep on the check command
193
- * (e.g. `--standard checkin-standards`, which older UDS versions wrote).
192
+ * @param {{args?: string, hookFile?: string}} [opts]
193
+ * args — extra arguments to keep on the check command
194
+ * (e.g. `--standard checkin-standards`, which older UDS versions wrote).
195
+ * hookFile — the file the block is written into, named in the "blocked" message so
196
+ * it points at the file the adopter actually has (default `.husky/pre-commit`;
197
+ * the native hook passes `.git/hooks/pre-commit`).
194
198
  * @returns {string} the block, LF line endings, ending with a newline
195
199
  */
196
- export function buildPreCommitBlock({ args = '' } = {}) {
200
+ export function buildPreCommitBlock({ args = '', hookFile = '.husky/pre-commit' } = {}) {
197
201
  const cmd = args ? `${UDS_BIN_NAME} check ${args}` : `${UDS_BIN_NAME} check`;
198
202
  return [
199
203
  UDS_HOOK_MARKER,
@@ -208,7 +212,7 @@ export function buildPreCommitBlock({ args = '' } = {}) {
208
212
  ' fi',
209
213
  ` echo "[UDS] Pre-commit check cannot run: the UDS CLI (${UDS_BIN_NAME}) is not installed." >&2`,
210
214
  ` echo "[UDS] Install it: npm install --save-dev ${UDS_BIN_NAME} (or: npm install -g ${UDS_BIN_NAME})" >&2`,
211
- ' echo "[UDS] This commit is blocked until it is installed, or until you remove this block from .husky/pre-commit." >&2',
215
+ ` echo "[UDS] This commit is blocked until it is installed, or until you remove this block from ${hookFile}." >&2`,
212
216
  ' exit 1',
213
217
  ') || exit $?',
214
218
  UDS_HOOK_END_MARKER,
@@ -32,6 +32,24 @@ export function displayLanguageToLocale(displayLanguage) {
32
32
  return LOCALE_MAP[displayLanguage.toLowerCase()] || 'en';
33
33
  }
34
34
 
35
+ /**
36
+ * Normalise a requested display language and say whether it is supported.
37
+ *
38
+ * `uds init --locale zh-CN` used to install English and exit 0: the value was compared case-sensitively
39
+ * further down, so the upper-case spelling silently missed every Chinese branch. An unsupported value such
40
+ * as `fr` did the same — English, exit 0, nothing said. The caller still falls back to English for an
41
+ * unsupported value, but now it can say so.
42
+ * @param {string|undefined|null} requested - what the user, install.yaml or the environment asked for
43
+ * @returns {{ displayLanguage: string|undefined|null, supported: boolean }}
44
+ */
45
+ export function normalizeRequestedLocale(requested) {
46
+ if (typeof requested !== 'string' || requested.trim() === '') {
47
+ return { displayLanguage: requested, supported: true };
48
+ }
49
+ const displayLanguage = requested.trim().toLowerCase();
50
+ return { displayLanguage, supported: Object.prototype.hasOwnProperty.call(LOCALE_MAP, displayLanguage) };
51
+ }
52
+
35
53
  /**
36
54
  * Check if a locale requires localized skill installation.
37
55
  * English locale uses the default source, no localization needed.
@@ -67,6 +85,7 @@ export function detectLocaleFromStandards(projectPath) {
67
85
 
68
86
  export default {
69
87
  displayLanguageToLocale,
88
+ normalizeRequestedLocale,
70
89
  isLocalizedLocale,
71
90
  detectLocaleFromStandards,
72
91
  LOCALE_MAP
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "version": "6.14.0-beta.3",
3
+ "version": "6.14.0-beta.4",
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": "6.14.0-beta.3"
61
+ "version": "6.14.0-beta.4"
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": "6.14.0-beta.3",
68
+ "version": "6.14.0-beta.4",
69
69
  "note": "Skills are now included in the main repository under skills/"
70
70
  }
71
71
  },
@@ -382,6 +382,10 @@
382
382
  "skills/brainstorm-assistant/SKILL.md",
383
383
  "skills/brainstorm-assistant/guide.md"
384
384
  ],
385
+ "comprehension-ladder": [
386
+ "skills/comprehension-ladder/SKILL.md",
387
+ "skills/comprehension-ladder/eval-cases.md"
388
+ ],
385
389
  "bdd-assistant": [
386
390
  "skills/bdd-assistant/SKILL.md",
387
391
  "skills/bdd-assistant/guide.md",
@@ -1536,6 +1540,16 @@
1536
1540
  "description": "Traditional Chinese (Taiwan) localization for all standards",
1537
1541
  "applicability": "Traditional Chinese speaking teams"
1538
1542
  },
1543
+ {
1544
+ "id": "zh-cn-locale",
1545
+ "name": "Simplified Chinese Locale",
1546
+ "nameZh": "简体中文本地化",
1547
+ "source": "extensions/locales/zh-cn.md",
1548
+ "category": "extension",
1549
+ "skillName": null,
1550
+ "description": "Simplified Chinese (Mainland China) localization for all standards",
1551
+ "applicability": "Simplified Chinese speaking teams"
1552
+ },
1539
1553
  {
1540
1554
  "id": "copilot-integration",
1541
1555
  "name": "GitHub Copilot Instructions",
@@ -2295,7 +2309,7 @@
2295
2309
  "id": "license-compliance",
2296
2310
  "name": "License Compliance Standards",
2297
2311
  "nameZh": "授權合規標準",
2298
- "version": "6.14.0-beta.3",
2312
+ "version": "6.14.0-beta.4",
2299
2313
  "source": {
2300
2314
  "human": "core/license-compliance.md",
2301
2315
  "ai": "ai/standards/license-compliance.ai.yaml"
@@ -2307,7 +2321,7 @@
2307
2321
  "id": "verification-oracle",
2308
2322
  "name": "Verification Oracle Standards",
2309
2323
  "nameZh": "驗證 Oracle 標準",
2310
- "version": "6.14.0-beta.3",
2324
+ "version": "6.14.0-beta.4",
2311
2325
  "source": {
2312
2326
  "human": "core/verification-oracle.md",
2313
2327
  "ai": "ai/standards/verification-oracle.ai.yaml"
@@ -2319,7 +2333,7 @@
2319
2333
  "id": "model-provenance",
2320
2334
  "name": "Model Provenance Policy Standards",
2321
2335
  "nameZh": "模型來源政策標準",
2322
- "version": "6.14.0-beta.3",
2336
+ "version": "6.14.0-beta.4",
2323
2337
  "source": {
2324
2338
  "human": "core/model-provenance.md",
2325
2339
  "ai": "ai/standards/model-provenance.ai.yaml"
@@ -2331,7 +2345,7 @@
2331
2345
  "id": "resource-cost-boundary",
2332
2346
  "name": "Resource / Cost Boundary Declaration Standards",
2333
2347
  "nameZh": "資源/成本邊界宣告標準",
2334
- "version": "6.14.0-beta.3",
2348
+ "version": "6.14.0-beta.4",
2335
2349
  "source": {
2336
2350
  "human": "core/resource-cost-boundary.md",
2337
2351
  "ai": "ai/standards/resource-cost-boundary.ai.yaml"