universal-dev-standards 5.7.1 → 5.7.3

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.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  source: ../../CHANGELOG.md
3
- source_version: 5.7.1
4
- translation_version: 5.7.1
3
+ source_version: 5.7.3
4
+ translation_version: 5.7.3
5
5
  last_synced: 2026-05-08
6
6
  status: current
7
7
  ---
@@ -17,6 +17,11 @@ status: current
17
17
 
18
18
  ## [Unreleased]
19
19
 
20
+ ## [5.7.3] - 2026-05-08
21
+
22
+ ### 修复
23
+ - **`uds update` 跳过无效 ID**(`cli/src/commands/update.js`):display、copy、hash 重算、post-update integrity check 四个循环,现在会跳过 `manifest.standards` 中无法解析的 short ID(没有 `/` 或 `.` 且 registry 无对应 entry,例如残留的 AI 工具名称 `claude-code`、`opencode`)。修复前,这类条目会在 `uds update` 中触发无意义的"缺失文件"警告与失败的还原尝试。
24
+
20
25
  ## [5.7.1] - 2026-05-08
21
26
 
22
27
  ### 修复
@@ -14,7 +14,7 @@ status: current
14
14
 
15
15
  > **语言**: [English](../../README.md) | [繁體中文](../zh-TW/README.md) | 简体中文
16
16
 
17
- **版本**: 5.7.1 | **发布日期**: 2026-04-13 | **授权**: [双重授权](../../LICENSE) (CC BY 4.0 + MIT)
17
+ **版本**: 5.7.3 | **发布日期**: 2026-04-13 | **授权**: [双重授权](../../LICENSE) (CC BY 4.0 + MIT)
18
18
 
19
19
  语言无关、框架无关的软件项目文档标准。通过 AI 原生工作流,确保不同技术栈之间的一致性、质量和可维护性。
20
20
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  source: ../../CHANGELOG.md
3
- source_version: 5.7.1
4
- translation_version: 5.7.1
3
+ source_version: 5.7.3
4
+ translation_version: 5.7.3
5
5
  last_synced: 2026-05-08
6
6
  status: current
7
7
  ---
@@ -17,6 +17,11 @@ status: current
17
17
 
18
18
  ## [Unreleased]
19
19
 
20
+ ## [5.7.3] - 2026-05-08
21
+
22
+ ### 修復
23
+ - **`uds update` 跳過無效 ID**(`cli/src/commands/update.js`):display、copy、hash 重算、post-update integrity check 四個迴圈,現在會跳過 `manifest.standards` 中無法解析的 short ID(沒有 `/` 或 `.` 且 registry 無對應 entry,例如殘留的 AI 工具名稱 `claude-code`、`opencode`)。修正前,這類條目會在 `uds update` 中觸發無意義的「缺失檔案」警告與失敗的還原嘗試。
24
+
20
25
  ## [5.7.1] - 2026-05-08
21
26
 
22
27
  ### 修復
@@ -14,7 +14,7 @@ status: current
14
14
 
15
15
  > **語言**: [English](../../README.md) | 繁體中文 | [简体中文](../zh-CN/README.md)
16
16
 
17
- **版本**: 5.7.1 | **發布日期**: 2026-04-13 | **授權**: [雙重授權](../../LICENSE) (CC BY 4.0 + MIT)
17
+ **版本**: 5.7.3 | **發布日期**: 2026-04-13 | **授權**: [雙重授權](../../LICENSE) (CC BY 4.0 + MIT)
18
18
 
19
19
  語言無關、框架無關的軟體專案文件標準。透過 AI 原生工作流,確保不同技術堆疊之間的一致性、品質和可維護性。
20
20
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "universal-dev-standards",
3
- "version": "5.7.1",
3
+ "version": "5.7.3",
4
4
  "description": "CLI tool for adopting Universal Development Standards",
5
5
  "keywords": [
6
6
  "documentation",
@@ -36,7 +36,7 @@ import { checkForUpdates } from '../utils/npm-registry.js';
36
36
  import { writeUpdateCache } from '../utils/update-checker.js';
37
37
  import { StandardValidator } from '../utils/standard-validator.js';
38
38
  import { WorkflowGate } from '../utils/workflow-gate.js';
39
- import { t, getLanguage, setLanguage, isLanguageExplicitlySet } from '../i18n/messages.js';
39
+ import { t, setLanguage, isLanguageExplicitlySet } from '../i18n/messages.js';
40
40
  import { guardAgainstSelfAdoption } from '../utils/detect-self-adoption.js';
41
41
 
42
42
  /**
@@ -112,13 +112,42 @@ function performFileIntegrityCheck(projectPath, manifest, msg) {
112
112
  console.log(chalk.gray(` ${msg.checkingExistence}`));
113
113
  console.log();
114
114
 
115
- // Check standards
115
+ // Check standards — support both legacy path format and current ID format
116
+ const allStdsLegacy = getAllStandards();
117
+ const legacyFormat = manifest.format || 'ai';
116
118
  for (const std of manifest.standards) {
117
- const filePath = join(projectPath, '.standards', std.split('/').pop());
119
+ // Option file paths (ai/options/...) use a subdirectory; handle separately
120
+ if (std.includes('/options/') || std.startsWith('options/')) {
121
+ const fileName = std.split('/').pop();
122
+ const filePath = join(projectPath, '.standards', 'options', fileName);
123
+ if (existsSync(filePath)) {
124
+ fileStatus.noHash.push(`.standards/options/${fileName}`);
125
+ } else {
126
+ fileStatus.missing.push(`.standards/options/${fileName}`);
127
+ }
128
+ continue;
129
+ }
130
+ // ID format (no '/' or '.'): resolve via registry to get actual filename
131
+ let fileName;
132
+ if (!std.includes('/') && !std.includes('.')) {
133
+ const entry = allStdsLegacy.find(s => s.id === std);
134
+ if (entry) {
135
+ const src = entry.source;
136
+ const sourcePath = typeof src === 'string'
137
+ ? src
138
+ : (src?.[legacyFormat] || src?.ai || src?.human || std);
139
+ fileName = basename(sourcePath);
140
+ } else {
141
+ fileName = std;
142
+ }
143
+ } else {
144
+ fileName = std.split('/').pop();
145
+ }
146
+ const filePath = join(projectPath, '.standards', fileName);
118
147
  if (existsSync(filePath)) {
119
- fileStatus.noHash.push(`.standards/${std.split('/').pop()}`);
148
+ fileStatus.noHash.push(`.standards/${fileName}`);
120
149
  } else {
121
- fileStatus.missing.push(`.standards/${std.split('/').pop()}`);
150
+ fileStatus.missing.push(`.standards/${fileName}`);
122
151
  }
123
152
  }
124
153
 
@@ -496,7 +525,7 @@ async function showSingleFileDiff(projectPath, manifest, relativePath, msg) {
496
525
  const fullPath = join(projectPath, relativePath);
497
526
 
498
527
  // Get current content
499
- let currentContent = '';
528
+ let currentContent;
500
529
  try {
501
530
  currentContent = readFileSync(fullPath, 'utf-8');
502
531
  } catch {
@@ -739,10 +768,23 @@ async function migrateToHashBasedTracking(projectPath, manifest) {
739
768
  const now = new Date().toISOString();
740
769
  let count = 0;
741
770
 
742
- // Process standards
771
+ // Process standards — support both legacy path format and current ID format
772
+ const format = manifest.format || 'ai';
773
+ const allStds = getAllStandards();
743
774
  for (const std of manifest.standards) {
744
- const fileName = basename(std);
745
- const relativePath = (std.includes('options/')
775
+ // Resolve path: ID format → look up source path from registry
776
+ let resolvedPath = std;
777
+ if (!std.includes('/') && !std.includes('.')) {
778
+ const entry = allStds.find(s => s.id === std);
779
+ if (entry) {
780
+ const src = entry.source;
781
+ resolvedPath = typeof src === 'string'
782
+ ? src
783
+ : (src?.[format] || src?.ai || src?.human || std);
784
+ }
785
+ }
786
+ const fileName = basename(resolvedPath);
787
+ const relativePath = (resolvedPath.includes('options/')
746
788
  ? join('.standards', 'options', fileName)
747
789
  : join('.standards', fileName)).replace(/\\/g, '/');
748
790
  const fullPath = join(projectPath, relativePath);
@@ -8,8 +8,7 @@ import { msg, t as getMessages, setLanguage, isLanguageExplicitlySet } from '../
8
8
  import {
9
9
  getOptionSource,
10
10
  findOption,
11
- getAllStandards,
12
- getStandardSource
11
+ getAllStandards
13
12
  } from '../utils/registry.js';
14
13
  import {
15
14
  copyStandard,
@@ -641,7 +640,7 @@ export async function runProjectConfiguration(options) {
641
640
  try {
642
641
  unlinkSync(filePath);
643
642
  console.log(chalk.gray(` ${msgObj.removed}: ${getToolFilePath(tool)}`));
644
- } catch (err) {
643
+ } catch {
645
644
  console.log(chalk.yellow(` ${msgObj.couldNotRemove}: ${getToolFilePath(tool)}`));
646
645
  }
647
646
  }
@@ -15,11 +15,11 @@
15
15
  */
16
16
 
17
17
  import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
18
- import { join, basename } from 'path';
18
+ import { join } from 'path';
19
19
  import yaml from 'js-yaml';
20
20
  import { parseFlow } from '../flow/flow-parser.js';
21
21
  import { listFlows, validateFlowById, diffFlows } from '../flow/flow-commands.js';
22
- import { exportBundle, importBundle, validateBundle } from '../flow/flow-bundler.js';
22
+ import { exportBundle, importBundle } from '../flow/flow-bundler.js';
23
23
 
24
24
  /**
25
25
  * 從互動式回答建立 Flow 物件。
@@ -105,7 +105,7 @@ export function loadAllFlows(projectPath) {
105
105
  /**
106
106
  * CLI action: uds flow create
107
107
  */
108
- export async function flowCreateCommand(options) {
108
+ export async function flowCreateCommand(_options) {
109
109
  const inquirer = await import('inquirer');
110
110
  const projectPath = process.cwd();
111
111
 
@@ -65,7 +65,7 @@ export async function initCommand(options) {
65
65
  console.log();
66
66
 
67
67
  // Configuration object
68
- let config = {};
68
+ let config;
69
69
 
70
70
  if (!options.yes) {
71
71
  // Interactive Mode
@@ -218,7 +218,7 @@ async function setupHuskyHook(projectPath) {
218
218
  console.log(chalk.gray(' Initializing husky...'));
219
219
  execSync('npx husky init', { stdio: 'ignore', cwd: projectPath });
220
220
  }
221
- } catch (e) {
221
+ } catch {
222
222
  // Ignore, might already be init
223
223
  }
224
224
 
@@ -249,7 +249,7 @@ async function setupHuskyHook(projectPath) {
249
249
  writeFileSync(preCommitPath, content + `\n# UDS Standard Check\n${udsCmd}\n`, 'utf-8');
250
250
  try {
251
251
  execSync(`chmod +x ${preCommitPath}`);
252
- } catch (e) {
252
+ } catch {
253
253
  // Ignore chmod failures on systems that don't support it
254
254
  }
255
255
  console.log(chalk.green(' ✓ Adding uds check to pre-commit hook'));
@@ -204,10 +204,23 @@ function checkNewStandards(manifest) {
204
204
  // Include all reference and skill standards
205
205
  const eligibleStandards = registryStandards.filter(s => s.category === 'reference' || s.category === 'skill');
206
206
 
207
- // Get installed standard basenames for comparison
208
- const installedBasenames = new Set(
209
- (manifest.standards || []).map(s => basename(s))
210
- );
207
+ // Get installed standard basenames for comparison.
208
+ // Handles both legacy path format ("ai/standards/foo.ai.yaml" → basename "foo.ai.yaml")
209
+ // and v3.4.0 ID format ("foo" resolve to source path, then get basename).
210
+ const installedBasenames = new Set();
211
+ for (const s of (manifest.standards || [])) {
212
+ if (s.includes('/options/') || s.startsWith('options/')) continue; // Skip option paths
213
+ if (!s.includes('/') && !s.includes('.')) {
214
+ // ID format: resolve to actual source filename via registry
215
+ const entry = registryStandards.find(r => r.id === s);
216
+ if (entry) {
217
+ const sourcePath = getStandardSource(entry, format);
218
+ if (sourcePath) installedBasenames.add(basename(sourcePath));
219
+ }
220
+ } else {
221
+ installedBasenames.add(basename(s));
222
+ }
223
+ }
211
224
 
212
225
  // Find standards in registry that are not yet installed
213
226
  const newStandards = [];
@@ -394,9 +407,19 @@ export async function updateCommand(options) {
394
407
 
395
408
  // List files to update
396
409
  console.log(chalk.gray(msg.filesToUpdate));
410
+ const displayFormat = manifest.format || 'ai';
411
+ const allStdsDisplay = getAllStandards();
397
412
  for (const std of manifest.standards) {
398
- const fileName = std.split('/').pop();
399
- const displayPath = getStandardTargetDir(std) + '/' + fileName;
413
+ // ID format: resolve to source path for display
414
+ let resolvedStd = std;
415
+ if (!std.includes('/') && !std.includes('.')) {
416
+ const entry = allStdsDisplay.find(r => r.id === std);
417
+ if (!entry) continue; // Skip unrecognized IDs (e.g. stale AI tool names)
418
+ const src = getStandardSource(entry, displayFormat);
419
+ if (src) resolvedStd = src;
420
+ }
421
+ const fileName = resolvedStd.split('/').pop();
422
+ const displayPath = getStandardTargetDir(resolvedStd) + '/' + fileName;
400
423
  console.log(chalk.gray(` ${displayPath}`));
401
424
  }
402
425
  for (const ext of manifest.extensions) {
@@ -445,8 +468,18 @@ export async function updateCommand(options) {
445
468
  };
446
469
 
447
470
  // Update standards
471
+ const updateFormat = manifest.format || 'ai';
472
+ const allStdsUpdate = getAllStandards();
448
473
  for (const std of manifest.standards) {
449
- const result = await copyStandard(std, getStandardTargetDir(std), projectPath);
474
+ // ID format: resolve to source path for copying
475
+ let sourcePath = std;
476
+ if (!std.includes('/') && !std.includes('.')) {
477
+ const entry = allStdsUpdate.find(r => r.id === std);
478
+ if (!entry) continue; // Skip unrecognized IDs (e.g. stale AI tool names)
479
+ const src = getStandardSource(entry, updateFormat);
480
+ if (src) sourcePath = src;
481
+ }
482
+ const result = await copyStandard(sourcePath, getStandardTargetDir(sourcePath), projectPath);
450
483
  if (result.success) {
451
484
  results.updated.push(std);
452
485
  } else {
@@ -602,10 +635,20 @@ export async function updateCommand(options) {
602
635
  manifest.fileHashes = {};
603
636
  }
604
637
 
605
- // Update hashes for standards
638
+ // Update hashes for standards (handles both ID format and legacy path format)
639
+ const hashFormat = manifest.format || 'ai';
640
+ const allStdsHash = getAllStandards();
606
641
  for (const std of manifest.standards) {
607
- const fileName = basename(std);
608
- const relativePath = (std.includes('options/')
642
+ // Resolve ID-format to source path
643
+ let resolvedPath = std;
644
+ if (!std.includes('/') && !std.includes('.')) {
645
+ const entry = allStdsHash.find(r => r.id === std);
646
+ if (!entry) continue; // Skip unrecognized IDs (e.g. stale AI tool names)
647
+ const src = getStandardSource(entry, hashFormat);
648
+ if (src) resolvedPath = src;
649
+ }
650
+ const fileName = basename(resolvedPath);
651
+ const relativePath = (resolvedPath.includes('options/')
609
652
  ? join('.standards', 'options', fileName)
610
653
  : join('.standards', fileName)).replace(/\\/g, '/');
611
654
  const fullPath = join(projectPath, relativePath);
@@ -705,9 +748,19 @@ export async function updateCommand(options) {
705
748
 
706
749
  // Post-update integrity check: detect and restore missing files
707
750
  const allTrackedFiles = [];
751
+ const trackFormat = manifest.format || 'ai';
752
+ const allStdsTrack = getAllStandards();
708
753
  for (const std of manifest.standards) {
709
- const fileName = basename(std);
710
- const relativePath = std.includes('options/')
754
+ // Resolve ID-format to source path for computing relativePath
755
+ let resolvedStd = std;
756
+ if (!std.includes('/') && !std.includes('.')) {
757
+ const entry = allStdsTrack.find(r => r.id === std);
758
+ if (!entry) continue; // Skip unrecognized IDs (e.g. stale AI tool names)
759
+ const src = getStandardSource(entry, trackFormat);
760
+ if (src) resolvedStd = src;
761
+ }
762
+ const fileName = basename(resolvedStd);
763
+ const relativePath = resolvedStd.includes('options/')
711
764
  ? join('.standards', 'options', fileName)
712
765
  : join('.standards', fileName);
713
766
  allTrackedFiles.push(relativePath);
@@ -1,20 +1,21 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
2
- import { join, dirname } from 'path';
2
+ import { join, dirname, basename } from 'path';
3
+ import { getAllStandards } from '../utils/registry.js';
3
4
 
4
5
  /**
5
- * UDS Manifest Schema v3.3.0
6
+ * UDS Manifest Schema v3.4.0
6
7
  * Central configuration and state tracking for UDS installations
7
8
  */
8
9
 
9
10
  /**
10
11
  * Supported manifest schema versions
11
12
  */
12
- export const SUPPORTED_SCHEMA_VERSIONS = ['3.0.0', '3.1.0', '3.2.0', '3.3.0'];
13
+ export const SUPPORTED_SCHEMA_VERSIONS = ['3.0.0', '3.1.0', '3.2.0', '3.3.0', '3.4.0'];
13
14
 
14
15
  /**
15
16
  * Current manifest schema version
16
17
  */
17
- export const CURRENT_SCHEMA_VERSION = '3.3.0';
18
+ export const CURRENT_SCHEMA_VERSION = '3.4.0';
18
19
 
19
20
  /**
20
21
  * Default manifest values
@@ -294,6 +295,11 @@ export function migrateManifest(manifest) {
294
295
  migrated = migrateToV330(migrated);
295
296
  }
296
297
 
298
+ if (currentVersion < '3.4.0') {
299
+ // Migrate to 3.4.0 - convert standards array from path format to ID format
300
+ migrated = migrateToV340(migrated);
301
+ }
302
+
297
303
  // Update schema version
298
304
  migrated.version = CURRENT_SCHEMA_VERSION;
299
305
 
@@ -367,6 +373,83 @@ function migrateToV330(manifest) {
367
373
  };
368
374
  }
369
375
 
376
+ /**
377
+ * Migration to version 3.4.0
378
+ * Converts standards array from legacy path format ("ai/standards/foo.ai.yaml",
379
+ * "core/foo.md") to registry ID format ("foo"). Deduplicates entries that
380
+ * refer to the same standard via different format paths (ai vs human).
381
+ * @param {Object} manifest - V3.3.x manifest
382
+ * @returns {Object} V3.4.0 compatible manifest
383
+ */
384
+ function migrateToV340(manifest) {
385
+ return {
386
+ ...manifest,
387
+ version: '3.4.0',
388
+ standards: migrateStandardsPathsToIds(manifest.standards || [])
389
+ };
390
+ }
391
+
392
+ /**
393
+ * Convert a standards array from legacy path format to registry ID format.
394
+ * Entries that already look like IDs (no "/" or ".") are kept as-is.
395
+ * Path entries are matched against the registry's source.ai / source.human
396
+ * fields (exact path match or basename match), then replaced with the
397
+ * registry ID. Entries that match no registry standard are dropped.
398
+ *
399
+ * @param {string[]} standards - Standards array (may be IDs or legacy paths)
400
+ * @returns {string[]} Sorted, deduplicated array of registry IDs
401
+ */
402
+ export function migrateStandardsPathsToIds(standards) {
403
+ if (!standards || standards.length === 0) return [];
404
+
405
+ const hasPathFormat = standards.some(
406
+ s => typeof s === 'string' && (s.includes('/') || s.includes('.'))
407
+ );
408
+ if (!hasPathFormat) return standards;
409
+
410
+ const allStandards = getAllStandards();
411
+ const ids = new Set();
412
+ // Option file paths (ai/options/...) have no short-ID equivalent; collect separately
413
+ const optionPaths = [];
414
+
415
+ for (const entry of standards) {
416
+ if (typeof entry !== 'string') continue;
417
+
418
+ // Already an ID (no path separators or extensions)
419
+ if (!entry.includes('/') && !entry.includes('.')) {
420
+ ids.add(entry);
421
+ continue;
422
+ }
423
+
424
+ // Option file paths (e.g. ai/options/commit-message/english.ai.yaml):
425
+ // no registry ID equivalent — preserve as-is so update/check can manage them
426
+ if (entry.includes('/options/') || entry.includes('options/')) {
427
+ optionPaths.push(entry);
428
+ continue;
429
+ }
430
+
431
+ // Base-standard path format — find matching registry entry
432
+ let matched = false;
433
+ for (const s of allStandards) {
434
+ const src = s.source;
435
+ const paths = typeof src === 'string'
436
+ ? [src]
437
+ : Object.values(src || {}).filter(p => typeof p === 'string');
438
+
439
+ if (paths.some(p => p === entry || basename(p) === basename(entry))) {
440
+ ids.add(s.id);
441
+ matched = true;
442
+ break;
443
+ }
444
+ }
445
+ // Unmatched non-option paths are silently dropped (standard removed from registry)
446
+ void matched;
447
+ }
448
+
449
+ // Return: sorted IDs first, then option paths (preserving order)
450
+ return [...ids].sort().concat(optionPaths);
451
+ }
452
+
370
453
  /**
371
454
  * Ensure all required fields exist on a manifest that already has the current
372
455
  * schema version. Older CLIs may have stamped the version number without
@@ -398,9 +481,15 @@ function ensureRequiredFields(manifest) {
398
481
  }
399
482
  }
400
483
 
484
+ // Safety net: convert any remaining path-format standards to IDs.
485
+ // Runs even at current schema version in case a manifest was written with
486
+ // the wrong format by an older CLI or a partial migration.
487
+ const standards = migrateStandardsPathsToIds(manifest.standards || []);
488
+
401
489
  return {
402
490
  ...manifest,
403
491
  options,
492
+ standards,
404
493
  contentMode: manifest.contentMode || 'index',
405
494
  fileHashes: normalizedHashes,
406
495
  skillHashes: manifest.skillHashes || {},
@@ -82,8 +82,25 @@ function calculateStandards(state, manifest) {
82
82
  const allStandards = getAllStandards();
83
83
 
84
84
  for (const standardId of (manifest.standards || [])) {
85
- // Find registry entry for this standard
86
- const registryEntry = allStandards.find(s => s.id === standardId);
85
+ // Skip option file paths — they are handled by calculateOptions, not this function.
86
+ // Option paths look like "ai/options/commit-message/english.ai.yaml".
87
+ if (standardId.includes('/options/') || standardId.startsWith('options/')) continue;
88
+
89
+ // Primary lookup: match by registry ID
90
+ let registryEntry = allStandards.find(s => s.id === standardId);
91
+
92
+ // Fallback: legacy path-format manifest entry (e.g. "ai/standards/foo.ai.yaml").
93
+ // Handles manifests that have not yet been migrated to v3.4.0 ID format.
94
+ if (!registryEntry && (standardId.includes('/') || standardId.includes('.'))) {
95
+ registryEntry = allStandards.find(s => {
96
+ const src = s.source;
97
+ const paths = typeof src === 'string'
98
+ ? [src]
99
+ : Object.values(src || {}).filter(p => typeof p === 'string');
100
+ return paths.some(p => p === standardId || basename(p) === basename(standardId));
101
+ });
102
+ }
103
+
87
104
  if (!registryEntry) continue;
88
105
 
89
106
  const source = getStandardSource(registryEntry, format);
@@ -14,6 +14,7 @@ import { existsSync, readFileSync, readdirSync } from 'fs';
14
14
  import { join } from 'path';
15
15
  import { compareFileHash, hasFileHashes, computeFileHash } from './hasher.js';
16
16
  import { SUPPORTED_AI_TOOLS } from '../core/constants.js';
17
+ import { getAllStandards, getStandardSource } from './registry.js';
17
18
 
18
19
  /**
19
20
  * Run friction detection on a UDS installation
@@ -113,9 +114,20 @@ function detectUnusedStandards(projectPath, manifest) {
113
114
 
114
115
  const allConfigText = configContents.join('\n');
115
116
 
116
- // Check each standard file
117
+ // Check each standard file (handles both ID format and legacy path format)
118
+ const allStdsFriction = getAllStandards();
119
+ const frictionFormat = manifest.format || 'ai';
117
120
  const standardFiles = (manifest.standards || []).map(s => {
118
- return s.includes('/') ? s.split('/').pop() : s;
121
+ if (!s.includes('/') && !s.includes('.')) {
122
+ // ID format: resolve to actual filename
123
+ const entry = allStdsFriction.find(r => r.id === s);
124
+ if (entry) {
125
+ const src = getStandardSource(entry, frictionFormat);
126
+ if (src) return src.split('/').pop();
127
+ }
128
+ return s;
129
+ }
130
+ return s.split('/').pop();
119
131
  });
120
132
 
121
133
  for (const fileName of standardFiles) {
@@ -16,6 +16,7 @@ import { join } from 'path';
16
16
  import { readManifest } from './copier.js';
17
17
  import { compareFileHash, hasFileHashes } from './hasher.js';
18
18
  import { SUPPORTED_AI_TOOLS } from '../core/constants.js';
19
+ import { getAllStandards, getStandardSource } from './registry.js';
19
20
 
20
21
  /**
21
22
  * Run Layer 1 health check on a UDS installation
@@ -64,8 +65,22 @@ export function runHealthCheck(projectPath) {
64
65
 
65
66
  // Check 4: Each standard file in manifest exists
66
67
  if (manifest.standards && Array.isArray(manifest.standards)) {
68
+ const allStds = getAllStandards();
69
+ const stdFormat = manifest.format || 'ai';
67
70
  for (const standardId of manifest.standards) {
68
- const fileName = standardId.includes('/') ? standardId.split('/').pop() : standardId;
71
+ // Resolve ID-format to actual filename via registry
72
+ let fileName;
73
+ if (!standardId.includes('/') && !standardId.includes('.')) {
74
+ const entry = allStds.find(s => s.id === standardId);
75
+ if (entry) {
76
+ const src = getStandardSource(entry, stdFormat);
77
+ fileName = src ? src.split('/').pop() : standardId;
78
+ } else {
79
+ fileName = standardId;
80
+ }
81
+ } else {
82
+ fileName = standardId.split('/').pop();
83
+ }
69
84
  const filePath = join(standardsDir, fileName);
70
85
  if (!existsSync(filePath)) {
71
86
  issues.push({
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "version": "5.7.1",
3
+ "version": "5.7.3",
4
4
  "lastUpdated": "2026-04-16",
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.7.1"
61
+ "version": "5.7.3"
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.7.1",
68
+ "version": "5.7.3",
69
69
  "note": "Skills are now included in the main repository under skills/"
70
70
  }
71
71
  },