universal-dev-standards 5.7.0 → 5.7.2
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/bundled/locales/zh-CN/CHANGELOG.md +17 -3
- package/bundled/locales/zh-CN/README.md +1 -1
- package/bundled/locales/zh-TW/CHANGELOG.md +17 -3
- package/bundled/locales/zh-TW/README.md +1 -1
- package/package.json +1 -1
- package/src/commands/check.js +51 -9
- package/src/commands/config.js +2 -3
- package/src/commands/flow.js +3 -3
- package/src/commands/init.js +3 -3
- package/src/commands/update.js +69 -12
- package/src/core/manifest.js +93 -4
- package/src/reconciler/desired-state-calculator.js +19 -2
- package/src/utils/friction-detector.js +14 -2
- package/src/utils/health-checker.js +16 -1
- package/standards-registry.json +3 -3
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
source: ../../CHANGELOG.md
|
|
3
|
-
source_version: 5.7.
|
|
4
|
-
translation_version: 5.7.
|
|
3
|
+
source_version: 5.7.2
|
|
4
|
+
translation_version: 5.7.2
|
|
5
5
|
last_synced: 2026-05-08
|
|
6
6
|
status: current
|
|
7
7
|
---
|
|
@@ -17,6 +17,19 @@ status: current
|
|
|
17
17
|
|
|
18
18
|
## [Unreleased]
|
|
19
19
|
|
|
20
|
+
## [5.7.1] - 2026-05-08
|
|
21
|
+
|
|
22
|
+
### 修复
|
|
23
|
+
- **`cli/package-lock.json`**:同步 lock file,修正 GitHub Actions `npm ci` 失败(`@emnapi/core`、`@emnapi/runtime` 条目缺失)。
|
|
24
|
+
|
|
25
|
+
### 移除
|
|
26
|
+
- **`specs/`**:删除已迁移至 dev-platform 的 4 个 spec 文件(XSPEC-026/005/006 对应)。保留 `execution-history-spec.md`(Archived)、`schemas/`、`standards-effectiveness-schema.json`。
|
|
27
|
+
- **`docs/archive/`**:删除 7 个过时的迁移指南与工作流程分析文件。
|
|
28
|
+
- **`.project-context/`**:删除 gemini-cli 自动生成的架构文件(内容已由 CLAUDE.md 涵盖)。
|
|
29
|
+
|
|
30
|
+
### 新增
|
|
31
|
+
- **`.npmignore`**:排除 `tests/`、`scripts/`、`.github/` 等开发用目录,不再随 npm publish 发出(v5.7.0 前这些目录一直被误打包)。
|
|
32
|
+
|
|
20
33
|
## [5.7.0] - 2026-05-08
|
|
21
34
|
|
|
22
35
|
> **跨平台脚本迁移**(XSPEC-179 + XSPEC-180):bash 脚本逐步被单一来源的
|
|
@@ -911,7 +924,8 @@ status: current
|
|
|
911
924
|
- 范本:需求文档范本
|
|
912
925
|
- 集成:OpenSpec 框架
|
|
913
926
|
|
|
914
|
-
[Unreleased]: https://github.com/AsiaOstrich/universal-dev-standards/compare/v5.7.
|
|
927
|
+
[Unreleased]: https://github.com/AsiaOstrich/universal-dev-standards/compare/v5.7.1...HEAD
|
|
928
|
+
[5.7.1]: https://github.com/AsiaOstrich/universal-dev-standards/compare/v5.7.0...v5.7.1
|
|
915
929
|
[5.7.0]: https://github.com/AsiaOstrich/universal-dev-standards/compare/v5.6.0...v5.7.0
|
|
916
930
|
[3.0.0]: https://github.com/AsiaOstrich/universal-dev-standards/compare/v2.3.0...v3.0.0
|
|
917
931
|
[2.3.0]: https://github.com/AsiaOstrich/universal-dev-standards/compare/v2.2.0...v2.3.0
|
|
@@ -14,7 +14,7 @@ status: current
|
|
|
14
14
|
|
|
15
15
|
> **语言**: [English](../../README.md) | [繁體中文](../zh-TW/README.md) | 简体中文
|
|
16
16
|
|
|
17
|
-
**版本**: 5.7.
|
|
17
|
+
**版本**: 5.7.2 | **发布日期**: 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.
|
|
4
|
-
translation_version: 5.7.
|
|
3
|
+
source_version: 5.7.2
|
|
4
|
+
translation_version: 5.7.2
|
|
5
5
|
last_synced: 2026-05-08
|
|
6
6
|
status: current
|
|
7
7
|
---
|
|
@@ -17,6 +17,19 @@ status: current
|
|
|
17
17
|
|
|
18
18
|
## [Unreleased]
|
|
19
19
|
|
|
20
|
+
## [5.7.1] - 2026-05-08
|
|
21
|
+
|
|
22
|
+
### 修復
|
|
23
|
+
- **`cli/package-lock.json`**:同步 lock file,修正 GitHub Actions `npm ci` 失敗(`@emnapi/core`、`@emnapi/runtime` 條目缺失)。
|
|
24
|
+
|
|
25
|
+
### 移除
|
|
26
|
+
- **`specs/`**:刪除已遷移至 dev-platform 的 4 個 spec 檔案(XSPEC-026/005/006 對應)。保留 `execution-history-spec.md`(Archived)、`schemas/`、`standards-effectiveness-schema.json`。
|
|
27
|
+
- **`docs/archive/`**:刪除 7 個過時的遷移指南與工作流程分析文件。
|
|
28
|
+
- **`.project-context/`**:刪除 gemini-cli 自動生成的架構文件(內容已由 CLAUDE.md 涵蓋)。
|
|
29
|
+
|
|
30
|
+
### 新增
|
|
31
|
+
- **`.npmignore`**:排除 `tests/`、`scripts/`、`.github/` 等開發用目錄,不再隨 npm publish 發出(v5.7.0 前這些目錄一直被誤打包)。
|
|
32
|
+
|
|
20
33
|
## [5.7.0] - 2026-05-08
|
|
21
34
|
|
|
22
35
|
> **跨平台腳本遷移**(XSPEC-179 + XSPEC-180):bash 腳本逐步被單一來源的
|
|
@@ -911,7 +924,8 @@ status: current
|
|
|
911
924
|
- 範本:需求文件範本
|
|
912
925
|
- 整合:OpenSpec 框架
|
|
913
926
|
|
|
914
|
-
[Unreleased]: https://github.com/AsiaOstrich/universal-dev-standards/compare/v5.7.
|
|
927
|
+
[Unreleased]: https://github.com/AsiaOstrich/universal-dev-standards/compare/v5.7.1...HEAD
|
|
928
|
+
[5.7.1]: https://github.com/AsiaOstrich/universal-dev-standards/compare/v5.7.0...v5.7.1
|
|
915
929
|
[5.7.0]: https://github.com/AsiaOstrich/universal-dev-standards/compare/v5.6.0...v5.7.0
|
|
916
930
|
[3.4.0]: https://github.com/AsiaOstrich/universal-dev-standards/compare/v3.3.0...v3.4.0
|
|
917
931
|
[3.3.0]: https://github.com/AsiaOstrich/universal-dev-standards/compare/v3.0.0...v3.3.0
|
|
@@ -14,7 +14,7 @@ status: current
|
|
|
14
14
|
|
|
15
15
|
> **語言**: [English](../../README.md) | 繁體中文 | [简体中文](../zh-CN/README.md)
|
|
16
16
|
|
|
17
|
-
**版本**: 5.7.
|
|
17
|
+
**版本**: 5.7.2 | **發布日期**: 2026-04-13 | **授權**: [雙重授權](../../LICENSE) (CC BY 4.0 + MIT)
|
|
18
18
|
|
|
19
19
|
語言無關、框架無關的軟體專案文件標準。透過 AI 原生工作流,確保不同技術堆疊之間的一致性、品質和可維護性。
|
|
20
20
|
|
package/package.json
CHANGED
package/src/commands/check.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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/${
|
|
148
|
+
fileStatus.noHash.push(`.standards/${fileName}`);
|
|
120
149
|
} else {
|
|
121
|
-
fileStatus.missing.push(`.standards/${
|
|
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
|
-
|
|
745
|
-
|
|
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);
|
package/src/commands/config.js
CHANGED
|
@@ -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
|
|
643
|
+
} catch {
|
|
645
644
|
console.log(chalk.yellow(` ${msgObj.couldNotRemove}: ${getToolFilePath(tool)}`));
|
|
646
645
|
}
|
|
647
646
|
}
|
package/src/commands/flow.js
CHANGED
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
18
|
-
import { join
|
|
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
|
|
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(
|
|
108
|
+
export async function flowCreateCommand(_options) {
|
|
109
109
|
const inquirer = await import('inquirer');
|
|
110
110
|
const projectPath = process.cwd();
|
|
111
111
|
|
package/src/commands/init.js
CHANGED
|
@@ -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
|
|
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
|
|
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'));
|
package/src/commands/update.js
CHANGED
|
@@ -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
|
-
|
|
209
|
-
|
|
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,20 @@ 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
|
-
|
|
399
|
-
|
|
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) {
|
|
418
|
+
const src = getStandardSource(entry, displayFormat);
|
|
419
|
+
if (src) resolvedStd = src;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
const fileName = resolvedStd.split('/').pop();
|
|
423
|
+
const displayPath = getStandardTargetDir(resolvedStd) + '/' + fileName;
|
|
400
424
|
console.log(chalk.gray(` ${displayPath}`));
|
|
401
425
|
}
|
|
402
426
|
for (const ext of manifest.extensions) {
|
|
@@ -445,8 +469,19 @@ export async function updateCommand(options) {
|
|
|
445
469
|
};
|
|
446
470
|
|
|
447
471
|
// Update standards
|
|
472
|
+
const updateFormat = manifest.format || 'ai';
|
|
473
|
+
const allStdsUpdate = getAllStandards();
|
|
448
474
|
for (const std of manifest.standards) {
|
|
449
|
-
|
|
475
|
+
// ID format: resolve to source path for copying
|
|
476
|
+
let sourcePath = std;
|
|
477
|
+
if (!std.includes('/') && !std.includes('.')) {
|
|
478
|
+
const entry = allStdsUpdate.find(r => r.id === std);
|
|
479
|
+
if (entry) {
|
|
480
|
+
const src = getStandardSource(entry, updateFormat);
|
|
481
|
+
if (src) sourcePath = src;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
const result = await copyStandard(sourcePath, getStandardTargetDir(sourcePath), projectPath);
|
|
450
485
|
if (result.success) {
|
|
451
486
|
results.updated.push(std);
|
|
452
487
|
} else {
|
|
@@ -602,10 +637,21 @@ export async function updateCommand(options) {
|
|
|
602
637
|
manifest.fileHashes = {};
|
|
603
638
|
}
|
|
604
639
|
|
|
605
|
-
// Update hashes for standards
|
|
640
|
+
// Update hashes for standards (handles both ID format and legacy path format)
|
|
641
|
+
const hashFormat = manifest.format || 'ai';
|
|
642
|
+
const allStdsHash = getAllStandards();
|
|
606
643
|
for (const std of manifest.standards) {
|
|
607
|
-
|
|
608
|
-
|
|
644
|
+
// Resolve ID-format to source path
|
|
645
|
+
let resolvedPath = std;
|
|
646
|
+
if (!std.includes('/') && !std.includes('.')) {
|
|
647
|
+
const entry = allStdsHash.find(r => r.id === std);
|
|
648
|
+
if (entry) {
|
|
649
|
+
const src = getStandardSource(entry, hashFormat);
|
|
650
|
+
if (src) resolvedPath = src;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
const fileName = basename(resolvedPath);
|
|
654
|
+
const relativePath = (resolvedPath.includes('options/')
|
|
609
655
|
? join('.standards', 'options', fileName)
|
|
610
656
|
: join('.standards', fileName)).replace(/\\/g, '/');
|
|
611
657
|
const fullPath = join(projectPath, relativePath);
|
|
@@ -705,9 +751,20 @@ export async function updateCommand(options) {
|
|
|
705
751
|
|
|
706
752
|
// Post-update integrity check: detect and restore missing files
|
|
707
753
|
const allTrackedFiles = [];
|
|
754
|
+
const trackFormat = manifest.format || 'ai';
|
|
755
|
+
const allStdsTrack = getAllStandards();
|
|
708
756
|
for (const std of manifest.standards) {
|
|
709
|
-
|
|
710
|
-
|
|
757
|
+
// Resolve ID-format to source path for computing relativePath
|
|
758
|
+
let resolvedStd = std;
|
|
759
|
+
if (!std.includes('/') && !std.includes('.')) {
|
|
760
|
+
const entry = allStdsTrack.find(r => r.id === std);
|
|
761
|
+
if (entry) {
|
|
762
|
+
const src = getStandardSource(entry, trackFormat);
|
|
763
|
+
if (src) resolvedStd = src;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
const fileName = basename(resolvedStd);
|
|
767
|
+
const relativePath = resolvedStd.includes('options/')
|
|
711
768
|
? join('.standards', 'options', fileName)
|
|
712
769
|
: join('.standards', fileName);
|
|
713
770
|
allTrackedFiles.push(relativePath);
|
package/src/core/manifest.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
-
//
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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({
|
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.7.
|
|
3
|
+
"version": "5.7.2",
|
|
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.
|
|
61
|
+
"version": "5.7.2"
|
|
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.
|
|
68
|
+
"version": "5.7.2",
|
|
69
69
|
"note": "Skills are now included in the main repository under skills/"
|
|
70
70
|
}
|
|
71
71
|
},
|