universal-dev-standards 6.1.1 → 6.2.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.
- package/bundled/locales/zh-CN/CHANGELOG.md +33 -3
- package/bundled/locales/zh-CN/README.md +75 -33
- package/bundled/locales/zh-CN/SECURITY.md +1 -1
- package/bundled/locales/zh-CN/core/behavior-snapshot.md +2 -2
- package/bundled/locales/zh-CN/core/data-migration-testing.md +2 -2
- package/bundled/locales/zh-CN/docs/CHEATSHEET.md +4 -4
- package/bundled/locales/zh-CN/docs/CLI-INIT-OPTIONS.md +9 -3
- package/bundled/locales/zh-CN/docs/FEATURE-REFERENCE.md +10 -11
- package/bundled/locales/zh-CN/docs/USER-MANUAL.md +40 -21
- package/bundled/locales/zh-CN/integrations/gemini-cli/README.md +12 -0
- package/bundled/locales/zh-CN/skills/ac-coverage/SKILL.md +11 -5
- package/bundled/locales/zh-CN/skills/adr-assistant/SKILL.md +2 -2
- package/bundled/locales/zh-CN/skills/commands/brainstorm.md +2 -2
- package/bundled/locales/zh-CN/skills/commit-standards/SKILL.md +2 -2
- package/bundled/locales/zh-CN/skills/contract-test-assistant/SKILL.md +2 -2
- package/bundled/locales/zh-CN/skills/deploy-assistant/SKILL.md +2 -2
- package/bundled/locales/zh-CN/skills/dev-methodology/SKILL.md +2 -2
- package/bundled/locales/zh-CN/skills/dev-workflow-guide/SKILL.md +2 -2
- package/bundled/locales/zh-CN/skills/journey-test-assistant/SKILL.md +2 -2
- package/bundled/locales/zh-CN/skills/knowledge-graph/SKILL.md +2 -2
- package/bundled/locales/zh-CN/skills/knowledge-graph/guide.md +2 -2
- package/bundled/locales/zh-CN/skills/migration-assistant/SKILL.md +188 -2
- package/bundled/locales/zh-CN/skills/observability-assistant/guide.md +2 -2
- package/bundled/locales/zh-CN/skills/orchestrate/SKILL.md +2 -2
- package/bundled/locales/zh-CN/skills/plan/SKILL.md +2 -2
- package/bundled/locales/zh-CN/skills/push/SKILL.md +2 -2
- package/bundled/locales/zh-CN/skills/retrospective-assistant/SKILL.md +2 -2
- package/bundled/locales/zh-CN/skills/reverse-engineer/SKILL.md +2 -2
- package/bundled/locales/zh-CN/skills/runbook-assistant/guide.md +2 -2
- package/bundled/locales/zh-CN/skills/skill-builder/SKILL.md +2 -2
- package/bundled/locales/zh-CN/skills/slo-assistant/guide.md +2 -2
- package/bundled/locales/zh-CN/skills/spec-derivation/SKILL.md +2 -2
- package/bundled/locales/zh-CN/skills/spec-driven-dev/SKILL.md +40 -2
- package/bundled/locales/zh-CN/skills/sweep/SKILL.md +2 -2
- package/bundled/locales/zh-CN/skills/testing-guide/SKILL.md +2 -2
- package/bundled/locales/zh-TW/CHANGELOG.md +34 -3
- package/bundled/locales/zh-TW/README.md +75 -33
- package/bundled/locales/zh-TW/SECURITY.md +1 -1
- package/bundled/locales/zh-TW/core/audit-trail.md +2 -2
- package/bundled/locales/zh-TW/core/behavior-snapshot.md +2 -2
- package/bundled/locales/zh-TW/core/browser-compatibility-standards.md +18 -7
- package/bundled/locales/zh-TW/docs/CHEATSHEET.md +4 -4
- package/bundled/locales/zh-TW/docs/CLI-INIT-OPTIONS.md +9 -3
- package/bundled/locales/zh-TW/docs/FEATURE-REFERENCE.md +10 -11
- package/bundled/locales/zh-TW/docs/USER-MANUAL.md +40 -21
- package/bundled/locales/zh-TW/integrations/gemini-cli/README.md +12 -0
- package/bundled/skills/commands/journey-test.md +80 -6
- package/bundled/skills/commands/skill-builder.md +75 -6
- package/package.json +3 -3
- package/src/commands/check.js +54 -7
- package/src/commands/config.js +9 -0
- package/src/commands/init.js +15 -1
- package/src/commands/release.js +1 -1
- package/src/commands/update.js +82 -31
- package/src/config/ai-agent-paths.js +47 -7
- package/src/core/constants.js +61 -1
- package/src/core/manifest.js +56 -0
- package/src/flow/flow-parser.js +1 -1
- package/src/flow/gate-loader.js +1 -1
- package/src/i18n/messages.js +6 -0
- package/src/installers/standards-installer.js +10 -20
- package/src/reconciler/actual-state-scanner.js +118 -43
- package/src/reconciler/desired-state-calculator.js +157 -43
- package/src/reconciler/diff-engine.js +7 -2
- package/src/reconciler/manifest-migrator.js +5 -2
- package/src/reconciler/plan-executor.js +53 -14
- package/src/uninstallers/integration-uninstaller.js +7 -1
- package/src/utils/config-loader.js +1 -1
- package/src/utils/config-manager.js +1 -1
- package/src/utils/github.js +5 -1
- package/src/utils/hasher.js +7 -4
- package/src/utils/integration-generator.js +121 -78
- package/src/utils/registry.js +39 -0
- package/src/utils/skills-installer.js +49 -34
- package/src/utils/skills-source.js +51 -0
- package/src/utils/standard-fixer.js +1 -1
- package/src/utils/standard-validator.js +1 -1
- package/standards-registry.json +21 -11
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
} from '../core/manifest.js';
|
|
18
18
|
import { computeFileHash, computeIntegrationBlockHash } from '../utils/hasher.js';
|
|
19
19
|
import { SUPPORTED_AI_TOOLS } from '../core/constants.js';
|
|
20
|
+
import { resolveToolKey } from '../core/constants.js';
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* Migrate manifest to latest schema and backfill missing hashes.
|
|
@@ -144,8 +145,10 @@ export function backfillFileHashes(projectPath, manifest) {
|
|
|
144
145
|
}
|
|
145
146
|
|
|
146
147
|
// Backfill integration block hashes
|
|
147
|
-
for (const
|
|
148
|
-
|
|
148
|
+
for (const entry of (updated.integrations || [])) {
|
|
149
|
+
// 兩種形狀都要能解出工具(XSPEC-343 R1)
|
|
150
|
+
const toolName = resolveToolKey(entry);
|
|
151
|
+
const toolConfig = toolName ? SUPPORTED_AI_TOOLS[toolName] : null;
|
|
149
152
|
if (!toolConfig) continue;
|
|
150
153
|
const filePath = join(projectPath, toolConfig.file);
|
|
151
154
|
if (!updated.integrationBlockHashes[toolConfig.file] && existsSync(filePath)) {
|
|
@@ -14,12 +14,13 @@
|
|
|
14
14
|
import { existsSync, unlinkSync, mkdirSync, rmSync, copyFileSync, statSync } from 'fs';
|
|
15
15
|
import { join, dirname } from 'path';
|
|
16
16
|
import { copyStandard } from '../utils/copier.js';
|
|
17
|
-
import { writeIntegrationFile } from '../utils/integration-generator.js';
|
|
17
|
+
import { writeIntegrationFile, buildToolIntegrationConfig } from '../utils/integration-generator.js';
|
|
18
18
|
import {
|
|
19
19
|
installSkillsToMultipleAgents,
|
|
20
20
|
installCommandsToMultipleAgents
|
|
21
21
|
} from '../utils/skills-installer.js';
|
|
22
22
|
import { writeManifest } from '../core/manifest.js';
|
|
23
|
+
import { getRepositoryInfo } from '../utils/registry.js';
|
|
23
24
|
import { displayLanguageToLocale } from '../utils/locale.js';
|
|
24
25
|
import { computeFileHash } from '../utils/hasher.js';
|
|
25
26
|
import { createBackup, cleanupBackups } from './backup-manager.js';
|
|
@@ -146,6 +147,24 @@ export async function executePlan(projectPath, plan, manifest, options = {}) {
|
|
|
146
147
|
}
|
|
147
148
|
}
|
|
148
149
|
|
|
150
|
+
// Record the version we just reconciled to — but only on a clean run, mirroring
|
|
151
|
+
// `uds update`'s rule (a partial failure must stay retryable).
|
|
152
|
+
//
|
|
153
|
+
// Without this the reconciler applied everything and advanced nothing: the repo
|
|
154
|
+
// still recorded 6.1.0, `uds check` still said "behind the latest release", and
|
|
155
|
+
// the weekly staleness scout — which reads exactly `upstream.version` — kept
|
|
156
|
+
// reporting the repo as stale after a fully successful reconcile. (XSPEC-343 R2)
|
|
157
|
+
if (!dryRun && results.every(r => r.success)) {
|
|
158
|
+
const version = getRepositoryInfo()?.standards?.version;
|
|
159
|
+
if (version) {
|
|
160
|
+
updatedManifest.upstream = {
|
|
161
|
+
...(updatedManifest.upstream || {}),
|
|
162
|
+
version,
|
|
163
|
+
installed: new Date().toISOString().split('T')[0]
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
149
168
|
// Write updated manifest
|
|
150
169
|
if (!dryRun) {
|
|
151
170
|
try {
|
|
@@ -304,6 +323,21 @@ function executeMigrateBlock(projectPath, action, manifest) {
|
|
|
304
323
|
if (result.blockHashInfo) {
|
|
305
324
|
manifest.integrationBlockHashes[result.path] = result.blockHashInfo;
|
|
306
325
|
}
|
|
326
|
+
// `init` also records integration files in the whole-file `fileHashes`, which
|
|
327
|
+
// is what `uds check`'s File Integrity compares. Rewriting the block without
|
|
328
|
+
// refreshing that entry left the file permanently reported as "modified" —
|
|
329
|
+
// a successful reconcile that reads, afterwards, as a damaged install.
|
|
330
|
+
// (XSPEC-343 R2)
|
|
331
|
+
const tracked = manifest.fileHashes?.[result.path];
|
|
332
|
+
if (tracked) {
|
|
333
|
+
const info = computeFileHash(join(projectPath, result.path));
|
|
334
|
+
if (info) {
|
|
335
|
+
manifest.fileHashes[result.path] = {
|
|
336
|
+
...info,
|
|
337
|
+
installedAt: tracked.installedAt || new Date().toISOString()
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
}
|
|
307
341
|
return { action, success: true };
|
|
308
342
|
}
|
|
309
343
|
|
|
@@ -341,10 +375,19 @@ async function executeSkillBatch(projectPath, skillActions, manifest) {
|
|
|
341
375
|
|
|
342
376
|
if (skillNames.length > 0) {
|
|
343
377
|
try {
|
|
378
|
+
// The locale argument was omitted here while the command path below
|
|
379
|
+
// passes it, so a reconcile silently reinstalled every skill in English.
|
|
380
|
+
// telemetry-server lost 59 zh-TW skill files to this before it was caught,
|
|
381
|
+
// and its manifest went on recording `skills.locale: zh-TW` throughout.
|
|
382
|
+
// Prefer the recorded skills locale; fall back to the display language,
|
|
383
|
+
// which is what `uds update` uses. (XSPEC-343 R2)
|
|
384
|
+
const skillLocale = manifest.skills?.locale
|
|
385
|
+
|| displayLanguageToLocale(manifest?.options?.display_language);
|
|
344
386
|
const installResult = await installSkillsToMultipleAgents(
|
|
345
387
|
manifest.skills.installations,
|
|
346
388
|
skillNames,
|
|
347
|
-
projectPath
|
|
389
|
+
projectPath,
|
|
390
|
+
skillLocale
|
|
348
391
|
);
|
|
349
392
|
|
|
350
393
|
if (installResult.allFileHashes) {
|
|
@@ -434,20 +477,16 @@ async function executeCommandBatch(projectPath, commandActions, manifest) {
|
|
|
434
477
|
|
|
435
478
|
/**
|
|
436
479
|
* Build integration generation config from manifest.
|
|
480
|
+
*
|
|
481
|
+
* Delegates to the shared builder so the reconciler and `uds update` generate the
|
|
482
|
+
* same block. The private copy that used to live here omitted `categories`,
|
|
483
|
+
* defaulted the output language to English, and read `integrationConfigs` by tool
|
|
484
|
+
* key when the manifest stores it by file name — so reconciling a repo silently
|
|
485
|
+
* dropped sections the normal update path always wrote. (XSPEC-343 R2)
|
|
437
486
|
*/
|
|
438
487
|
function buildIntegrationConfig(manifest, toolName) {
|
|
439
488
|
return {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
s.startsWith('.standards/') ? s : `.standards/${s}`
|
|
443
|
-
),
|
|
444
|
-
format: manifest.format || 'ai',
|
|
445
|
-
contentMode: manifest.contentMode || 'index',
|
|
446
|
-
language: manifest.integrationConfigs?.[toolName]?.language || 'en',
|
|
447
|
-
outputLanguage: manifest.integrationConfigs?.[toolName]?.outputLanguage || manifest.integrationConfigs?.[toolName]?.commitLanguage || 'english',
|
|
448
|
-
installedSkills: manifest.skills?.names || [],
|
|
449
|
-
installedCommands: manifest.commands?.names || [],
|
|
450
|
-
methodology: manifest.methodology,
|
|
451
|
-
...(manifest.integrationConfigs?.[toolName] || {})
|
|
489
|
+
...buildToolIntegrationConfig(manifest, toolName),
|
|
490
|
+
format: manifest.format || 'ai'
|
|
452
491
|
};
|
|
453
492
|
}
|
|
@@ -2,6 +2,7 @@ import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
|
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { extractMarkedContent } from '../utils/integration-generator.js';
|
|
4
4
|
import { SUPPORTED_AI_TOOLS } from '../core/constants.js';
|
|
5
|
+
import { resolveIntegrationFile } from '../core/constants.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Get the format for a given integration file name
|
|
@@ -36,7 +37,12 @@ export async function uninstallIntegrations(projectPath, manifest, options = {})
|
|
|
36
37
|
return result;
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
for (const
|
|
40
|
+
for (const entry of integrations) {
|
|
41
|
+
// Accept both shapes: this uninstaller was written against file names,
|
|
42
|
+
// while the reconciler reads the same field as tool keys. Normalising the
|
|
43
|
+
// manifest to one shape fixes one consumer and breaks the other, so both
|
|
44
|
+
// resolve through the shared helper instead. (XSPEC-343 R1)
|
|
45
|
+
const fileName = resolveIntegrationFile(entry) || entry;
|
|
40
46
|
const filePath = join(projectPath, fileName);
|
|
41
47
|
|
|
42
48
|
if (!existsSync(filePath)) {
|
|
@@ -2,7 +2,7 @@ import { ConfigLoader } from './config-loader.js';
|
|
|
2
2
|
import { ConfigMerger } from './config-merger.js';
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import path from 'path';
|
|
5
|
-
import yaml from 'js-yaml';
|
|
5
|
+
import * as yaml from 'js-yaml';
|
|
6
6
|
|
|
7
7
|
export class ConfigManager {
|
|
8
8
|
constructor(cwd = process.cwd()) {
|
package/src/utils/github.js
CHANGED
|
@@ -4,6 +4,7 @@ import { homedir } from 'os';
|
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import https from 'https';
|
|
6
6
|
import { setTimeout as delay } from 'timers/promises';
|
|
7
|
+
import { getSkillsSourceDir } from './skills-source.js';
|
|
7
8
|
|
|
8
9
|
// Re-export agent-specific functions from ai-agent-paths for unified API
|
|
9
10
|
export {
|
|
@@ -26,7 +27,10 @@ const SKILLS_RAW_BASE = 'https://raw.githubusercontent.com/AsiaOstrich/universal
|
|
|
26
27
|
const __filename = fileURLToPath(import.meta.url);
|
|
27
28
|
const __dirname = dirname(__filename);
|
|
28
29
|
const CLI_ROOT = join(__dirname, '..', '..');
|
|
29
|
-
|
|
30
|
+
|
|
31
|
+
// Resolved centrally — this file used to hardcode `<CLI_ROOT>/../skills/claude-code`, a
|
|
32
|
+
// layout that no longer exists in either a checkout or an npm install. See skills-source.js.
|
|
33
|
+
const SKILLS_LOCAL_DIR = getSkillsSourceDir();
|
|
30
34
|
|
|
31
35
|
/**
|
|
32
36
|
* Status codes that are safe to retry (transient errors)
|
package/src/utils/hasher.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createHash } from 'crypto';
|
|
|
2
2
|
import { readFileSync, statSync, existsSync, readdirSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import { UDS_MARKERS } from '../core/constants.js';
|
|
5
|
+
import { resolveIntegrationFile } from '../core/constants.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Compute SHA-256 hash for a file
|
|
@@ -109,10 +110,12 @@ export function getFileStatusSummary(projectPath, manifest) {
|
|
|
109
110
|
source: e,
|
|
110
111
|
target: join('.standards', e.split('/').pop())
|
|
111
112
|
})),
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
// `integrations` holds file names in most repos and tool keys in some
|
|
114
|
+
// (XSPEC-343 R1) — resolve so both shapes yield a real path.
|
|
115
|
+
...manifest.integrations.map(i => {
|
|
116
|
+
const f = resolveIntegrationFile(i) || i;
|
|
117
|
+
return { source: f, target: f };
|
|
118
|
+
})
|
|
116
119
|
];
|
|
117
120
|
|
|
118
121
|
for (const file of allFiles) {
|
|
@@ -3,6 +3,7 @@ import { dirname, join, basename } from 'path';
|
|
|
3
3
|
import { getLanguageRules } from '../prompts/integrations.js';
|
|
4
4
|
import { computeIntegrationBlockHash } from './hasher.js';
|
|
5
5
|
import { UDS_MARKERS, SUPPORTED_AI_TOOLS, LEGACY_TOOL_MAPPINGS } from '../core/constants.js';
|
|
6
|
+
import { resolveSelectedOptionSources } from './registry.js';
|
|
6
7
|
import { getAgentConfig, getAgentTier } from '../config/ai-agent-paths.js';
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -107,24 +108,13 @@ const STANDARD_TASK_MAPPING = {
|
|
|
107
108
|
/**
|
|
108
109
|
* Standard descriptions for index generation
|
|
109
110
|
*/
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
'versioning.md': '語意化版本',
|
|
118
|
-
'changelog-standards.md': '變更日誌標準',
|
|
119
|
-
'code-review-checklist.md': '程式碼審查清單',
|
|
120
|
-
'spec-driven-development.md': '規格驅動開發',
|
|
121
|
-
'test-completeness-dimensions.md': '測試完整性維度',
|
|
122
|
-
'git-workflow.ai.yaml': 'Git 工作流程',
|
|
123
|
-
'developer-memory.ai.yaml': '開發者持久記憶',
|
|
124
|
-
'project-context-memory.ai.yaml': '專案情境記憶',
|
|
125
|
-
'zh-tw.md': '繁體中文本地化',
|
|
126
|
-
'workflow-enforcement.ai.yaml': '工作流程強制執行'
|
|
127
|
-
};
|
|
111
|
+
// STANDARD_DESCRIPTIONS 已移除(XSPEC-358 R1 裁決為方案 A:索引區塊不再列名稱清單)。
|
|
112
|
+
// 移除前的實況值得記錄,因為它是一個**綠燈測試釘住生產環境不成立假設**的實例:
|
|
113
|
+
// 該表有 16 筆真實中文描述,鍵**帶副檔名**('anti-hallucination.md')。
|
|
114
|
+
// init 路徑傳入 copyStandard 的 sourcePath → basename **含副檔名** → 查表命中。
|
|
115
|
+
// update 路徑傳入 manifest.standards → **不含副檔名**(71/78 筆)→ 查表全 miss。
|
|
116
|
+
// 於是同一個區塊在 `uds init` 後有描述、在 `uds update` 後全是 `名稱 - 名稱`。
|
|
117
|
+
// 單元測試餵的是完整檔名,所以它一直是綠的;dev-platform 實測 78/78 皆 fallback。
|
|
128
118
|
|
|
129
119
|
/**
|
|
130
120
|
* Commit type templates for different output_language options
|
|
@@ -2908,6 +2898,11 @@ export function generateComplianceInstructions(installedStandards, mode, format,
|
|
|
2908
2898
|
const sections = [];
|
|
2909
2899
|
|
|
2910
2900
|
if (format === 'markdown') {
|
|
2901
|
+
// 三個清單全空時不得留下一個空標題(XSPEC-358 §1)——
|
|
2902
|
+
// 一個沒有內容的章節,讀起來像「這裡本來該有東西而它不見了」。
|
|
2903
|
+
if (mustFollow.length === 0 && shouldFollow.length === 0) {
|
|
2904
|
+
return '';
|
|
2905
|
+
}
|
|
2911
2906
|
sections.push('## Standards Compliance Instructions');
|
|
2912
2907
|
sections.push('');
|
|
2913
2908
|
|
|
@@ -2936,6 +2931,9 @@ export function generateComplianceInstructions(installedStandards, mode, format,
|
|
|
2936
2931
|
}
|
|
2937
2932
|
} else {
|
|
2938
2933
|
// Plaintext format
|
|
2934
|
+
if (mustFollow.length === 0 && shouldFollow.length === 0) {
|
|
2935
|
+
return '';
|
|
2936
|
+
}
|
|
2939
2937
|
sections.push('## Standards Compliance Instructions');
|
|
2940
2938
|
sections.push('');
|
|
2941
2939
|
|
|
@@ -2975,71 +2973,108 @@ export function generateStandardsIndex(installedStandards, format, language = 'z
|
|
|
2975
2973
|
return '';
|
|
2976
2974
|
}
|
|
2977
2975
|
|
|
2978
|
-
const
|
|
2979
|
-
const
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2976
|
+
const total = installedStandards.length;
|
|
2977
|
+
const optionCount = installedStandards.filter(
|
|
2978
|
+
(p) => p.includes('/options/') || p.includes('\\options\\')
|
|
2979
|
+
).length;
|
|
2980
|
+
const coreCount = total - optionCount;
|
|
2981
|
+
|
|
2982
|
+
const heading = '## Installed Standards Index';
|
|
2983
|
+
const md = format === 'markdown';
|
|
2984
|
+
const body = language === 'en'
|
|
2985
|
+
? [
|
|
2986
|
+
`This project has adopted **${total}** UDS standards (${coreCount} core, ${optionCount} options), installed in \`.standards/\`.`,
|
|
2987
|
+
'',
|
|
2988
|
+
'The authoritative list is the `standards` field of `.standards/manifest.json`. Read a standard\'s own file to see what it requires — and check its first line, which is where a deprecated standard says so.'
|
|
2989
|
+
]
|
|
2990
|
+
: [
|
|
2991
|
+
`本專案採用 UDS 標準共 **${total}** 條(core ${coreCount}、options ${optionCount}),安裝於 \`.standards/\`。`,
|
|
2992
|
+
'',
|
|
2993
|
+
'權威清單為 `.standards/manifest.json` 的 `standards` 欄位。要知道某標準要求什麼,讀它自己的檔案——並看第一行,已棄用的標準會在那裡寫明。'
|
|
2994
|
+
];
|
|
2995
2995
|
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
sections.push(language === 'en'
|
|
3000
|
-
? 'This project has adopted UDS standards. All standards are in `.standards/`:'
|
|
3001
|
-
: '本專案採用 UDS 標準。所有規範位於 `.standards/`:');
|
|
3002
|
-
sections.push('');
|
|
2996
|
+
const plainBody = body.map((l) => l.replace(/`/g, ''));
|
|
2997
|
+
return [heading, '', ...(md ? body : plainBody), ''].join('\n');
|
|
2998
|
+
}
|
|
3003
2999
|
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3000
|
+
/**
|
|
3001
|
+
* Build the generation config for one AI tool from the manifest.
|
|
3002
|
+
*
|
|
3003
|
+
* Both the normal `uds update` path and the reconciler regenerate integration
|
|
3004
|
+
* blocks, and they used to derive this config independently. The reconciler's
|
|
3005
|
+
* copy omitted `categories` entirely, defaulted `outputLanguage` to English, and
|
|
3006
|
+
* looked up `manifest.integrationConfigs` by **tool key** while the manifest
|
|
3007
|
+
* stores it by **file name** — so the lookup always missed. The result was a
|
|
3008
|
+
* strictly poorer block: reconciling machine-setup silently dropped its
|
|
3009
|
+
* "提交訊息語言" and "Standards Compliance Instructions" sections, which the
|
|
3010
|
+
* normal update path had always written. Two generators, one contract, one of
|
|
3011
|
+
* them quietly lossy. (XSPEC-343 R2)
|
|
3012
|
+
*
|
|
3013
|
+
* `integrationConfigs` is deliberately NOT merged in: its `installedStandards`
|
|
3014
|
+
* is a snapshot frozen at install time, and honouring it would reintroduce
|
|
3015
|
+
* exactly the staleness this whole XSPEC is about.
|
|
3016
|
+
*
|
|
3017
|
+
* @param {Object} manifest - Project manifest
|
|
3018
|
+
* @param {string} tool - Tool key (e.g. 'claude-code')
|
|
3019
|
+
* @returns {Object} Config for generateIntegrationContent / writeIntegrationFile
|
|
3020
|
+
*/
|
|
3021
|
+
function withSelectedOptions(manifest) {
|
|
3022
|
+
const standards = manifest.standards || [];
|
|
3023
|
+
const seen = new Set(standards.map((p) => basename(p)));
|
|
3024
|
+
const extra = resolveSelectedOptionSources(manifest.options, manifest.format || 'ai')
|
|
3025
|
+
.filter((p) => !seen.has(basename(p)));
|
|
3026
|
+
return [...standards, ...extra];
|
|
3027
|
+
}
|
|
3011
3028
|
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
}
|
|
3019
|
-
} else {
|
|
3020
|
-
// Plaintext format
|
|
3021
|
-
sections.push('## Installed Standards Index');
|
|
3022
|
-
sections.push('');
|
|
3023
|
-
sections.push(language === 'en'
|
|
3024
|
-
? 'UDS standards installed in .standards/:'
|
|
3025
|
-
: 'UDS 標準已安裝於 .standards/:');
|
|
3026
|
-
sections.push('');
|
|
3029
|
+
export function buildToolIntegrationConfig(manifest, tool) {
|
|
3030
|
+
const selected = manifest.options?.output_language || manifest.options?.commit_language || 'english';
|
|
3031
|
+
const language = selected === 'bilingual'
|
|
3032
|
+
? 'bilingual'
|
|
3033
|
+
: selected === 'traditional-chinese' ? 'zh-tw' : 'en';
|
|
3034
|
+
const resolved = resolveContentModeForTool(tool, manifest.contentMode || 'auto');
|
|
3027
3035
|
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3036
|
+
return {
|
|
3037
|
+
tool,
|
|
3038
|
+
categories: ['anti-hallucination', 'commit-standards', 'code-review'],
|
|
3039
|
+
language,
|
|
3040
|
+
// Pass manifest entries UNCHANGED. Every consumer here documents this
|
|
3041
|
+
// parameter as "standard file paths" and basenames it for display, while
|
|
3042
|
+
// classifying options by `path.includes('/options/')`. Reducing to basenames
|
|
3043
|
+
// first destroys that signal: options silently count as core, so the index
|
|
3044
|
+
// block reported "(70 core, 0 options)" and the minimal block listed option
|
|
3045
|
+
// files under the wrong heading with a path that does not exist on disk.
|
|
3046
|
+
installedStandards: withSelectedOptions(manifest),
|
|
3047
|
+
contentMode: resolved.contentMode,
|
|
3048
|
+
level: resolved.level,
|
|
3049
|
+
outputLanguage: selected,
|
|
3050
|
+
methodology: manifest.methodology
|
|
3051
|
+
};
|
|
3052
|
+
}
|
|
3031
3053
|
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
}
|
|
3038
|
-
}
|
|
3039
|
-
sections.push('');
|
|
3040
|
-
}
|
|
3054
|
+
/**
|
|
3055
|
+
* Heading of the index-mode standards block. Not localised — both the English and
|
|
3056
|
+
* the Chinese body sit under this exact string, so it is a stable marker.
|
|
3057
|
+
*/
|
|
3058
|
+
export const STANDARDS_INDEX_HEADING = '## Installed Standards Index';
|
|
3041
3059
|
|
|
3042
|
-
|
|
3060
|
+
/**
|
|
3061
|
+
* Read the standards count declared by an index-mode block.
|
|
3062
|
+
*
|
|
3063
|
+
* XSPEC-358 R1 replaced the enumerated list with a count plus a pointer to the
|
|
3064
|
+
* manifest. Two checks in `uds check` still asserted the old contract by grepping
|
|
3065
|
+
* the integration file for every standard name, so after the change they reported
|
|
3066
|
+
* `5/70` and `0/7` and told the user to run `uds update` — which regenerates the
|
|
3067
|
+
* same non-enumerating block. An unsatisfiable loop. Checks must assert what the
|
|
3068
|
+
* block claims now, not what it used to list. (XSPEC-343 R2 / XSPEC-358 R1)
|
|
3069
|
+
*
|
|
3070
|
+
* @param {string} content - Full integration file content
|
|
3071
|
+
* @returns {number|null} Declared count, or null if this is not an index block
|
|
3072
|
+
*/
|
|
3073
|
+
export function parseStandardsIndexCount(content) {
|
|
3074
|
+
const at = content.indexOf(STANDARDS_INDEX_HEADING);
|
|
3075
|
+
if (at === -1) return null;
|
|
3076
|
+
const match = content.slice(at).match(/\*\*(\d+)\*\*/);
|
|
3077
|
+
return match ? Number(match[1]) : null;
|
|
3043
3078
|
}
|
|
3044
3079
|
|
|
3045
3080
|
/**
|
|
@@ -3053,7 +3088,15 @@ export function wrapWithMarkers(content, format) {
|
|
|
3053
3088
|
const warning = format === 'plaintext'
|
|
3054
3089
|
? '# WARNING: This block is managed by UDS (universal-dev-standards). DO NOT manually edit. Use \'npx uds install\' or \'npx uds update\' to modify.'
|
|
3055
3090
|
: '<!-- WARNING: This block is managed by UDS (universal-dev-standards). DO NOT manually edit. Use \'npx uds install\' or \'npx uds update\' to modify. -->';
|
|
3056
|
-
|
|
3091
|
+
// 冪等:warning 位於 markers **內部**,而 extractMarkedContent 取出的內容也含它,
|
|
3092
|
+
// 於是重新包裝會疊出第二份(dev-platform CLAUDE.md 實測 178/179 兩行完全相同)。
|
|
3093
|
+
// 這裡先剝掉內容開頭既有的 warning,不論上游哪條路徑造成都能修掉。
|
|
3094
|
+
// ⚠️ 未隔離出上游那條路徑——這是防禦性修法,如實記錄其範圍。
|
|
3095
|
+
const stripped = content.replace(
|
|
3096
|
+
/^(?:<!--\s*WARNING: This block is managed by UDS[\s\S]*?-->|#\s*WARNING: This block is managed by UDS[^\n]*)\n?/,
|
|
3097
|
+
''
|
|
3098
|
+
);
|
|
3099
|
+
return `${markers.start}\n${warning}\n${stripped}\n${markers.end}`;
|
|
3057
3100
|
}
|
|
3058
3101
|
|
|
3059
3102
|
/**
|
package/src/utils/registry.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from 'fs';
|
|
2
2
|
import { fileURLToPath } from 'url';
|
|
3
3
|
import { dirname, join } from 'path';
|
|
4
|
+
import { MANIFEST_OPTION_BINDINGS } from '../core/constants.js';
|
|
4
5
|
|
|
5
6
|
const __filename = fileURLToPath(import.meta.url);
|
|
6
7
|
const __dirname = dirname(__filename);
|
|
@@ -205,3 +206,41 @@ export function isMultiSelectOption(standard, categoryKey) {
|
|
|
205
206
|
}
|
|
206
207
|
return standard.options[categoryKey].multiSelect === true;
|
|
207
208
|
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Resolve the option source paths a manifest's `options` selection installs.
|
|
212
|
+
*
|
|
213
|
+
* `manifest.options` is the authoritative record of what the project chose;
|
|
214
|
+
* `manifest.standards` records option files inconsistently — most repos list
|
|
215
|
+
* them, machine-setup lists none — so anything counting options from
|
|
216
|
+
* `manifest.standards` alone reported "0 options" for a project that has seven
|
|
217
|
+
* of them on disk. (XSPEC-343 R2)
|
|
218
|
+
*
|
|
219
|
+
* @param {Object} manifestOptions - `manifest.options`
|
|
220
|
+
* @param {string} [format='ai'] - Source format
|
|
221
|
+
* @returns {string[]} Registry-relative source paths, e.g. ai/options/testing/unit-testing.ai.yaml
|
|
222
|
+
*/
|
|
223
|
+
export function resolveSelectedOptionSources(manifestOptions, format = 'ai') {
|
|
224
|
+
if (!manifestOptions) return [];
|
|
225
|
+
const all = getAllStandards();
|
|
226
|
+
const paths = [];
|
|
227
|
+
|
|
228
|
+
for (const binding of MANIFEST_OPTION_BINDINGS) {
|
|
229
|
+
const key = binding.manifestKeys.find((k) => manifestOptions[k] != null);
|
|
230
|
+
if (!key) continue;
|
|
231
|
+
|
|
232
|
+
const standard = all.find((s) => s.id === binding.standardId);
|
|
233
|
+
if (!standard) continue;
|
|
234
|
+
|
|
235
|
+
const selection = manifestOptions[key];
|
|
236
|
+
for (const optionId of Array.isArray(selection) ? selection : [selection]) {
|
|
237
|
+
if (typeof optionId !== 'string') continue;
|
|
238
|
+
const option = findOption(standard, binding.categoryKey, optionId);
|
|
239
|
+
if (!option) continue;
|
|
240
|
+
const source = getOptionSource(option, format);
|
|
241
|
+
if (source) paths.push(source);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return paths;
|
|
246
|
+
}
|
|
@@ -15,10 +15,12 @@ import {
|
|
|
15
15
|
getSkillsDirForAgent,
|
|
16
16
|
getCommandsDirForAgent,
|
|
17
17
|
getSkillsSupportedAgents,
|
|
18
|
-
getCommandsSupportedAgents
|
|
18
|
+
getCommandsSupportedAgents,
|
|
19
|
+
getCommandFileExtension
|
|
19
20
|
} from '../config/ai-agent-paths.js';
|
|
20
21
|
import { computeDirectoryHashes, computeFileHash } from './hasher.js';
|
|
21
22
|
import { isLocalizedLocale } from './locale.js';
|
|
23
|
+
import { getSkillsSourceDir } from './skills-source.js';
|
|
22
24
|
|
|
23
25
|
// Get the CLI package root directory
|
|
24
26
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -26,19 +28,8 @@ const __dirname = dirname(__filename);
|
|
|
26
28
|
const CLI_ROOT = join(__dirname, '..', '..');
|
|
27
29
|
const BUNDLED_DIR = join(CLI_ROOT, 'bundled');
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
* Prioritizes bundled directory (npm install), falls back to development path.
|
|
32
|
-
* @returns {string} Path to skills source directory
|
|
33
|
-
*/
|
|
34
|
-
function getSkillsSourceDir() {
|
|
35
|
-
const bundledPath = join(BUNDLED_DIR, 'skills');
|
|
36
|
-
if (existsSync(bundledPath)) {
|
|
37
|
-
return bundledPath;
|
|
38
|
-
}
|
|
39
|
-
// Development environment fallback
|
|
40
|
-
return join(CLI_ROOT, '..', 'skills');
|
|
41
|
-
}
|
|
31
|
+
// getSkillsSourceDir now lives in skills-source.js so github.js resolves the same path.
|
|
32
|
+
// Its previous private copy here was the half that worked; the copy in github.js was not.
|
|
42
33
|
|
|
43
34
|
/**
|
|
44
35
|
* Get the localized Skills source directory for a given locale.
|
|
@@ -108,23 +99,56 @@ export function getAvailableSkillNames() {
|
|
|
108
99
|
return [];
|
|
109
100
|
}
|
|
110
101
|
|
|
111
|
-
const NON_SKILL_ITEMS = [
|
|
112
|
-
'README.md', 'CONTRIBUTING.template.md',
|
|
113
|
-
'commands', '.manifest.json', '.DS_Store'
|
|
114
|
-
];
|
|
115
|
-
|
|
116
102
|
try {
|
|
117
103
|
return readdirSync(SKILLS_LOCAL_DIR)
|
|
118
104
|
.filter(item => {
|
|
119
|
-
if (NON_SKILL_ITEMS.includes(item)) return false;
|
|
120
105
|
const itemPath = join(SKILLS_LOCAL_DIR, item);
|
|
121
|
-
|
|
106
|
+
if (!statSync(itemPath).isDirectory()) return false;
|
|
107
|
+
// A skill is a directory containing SKILL.md. Defining it positively rather than
|
|
108
|
+
// by a deny-list matters: `skills/` also holds agents/, workflows/, ai/, tools/
|
|
109
|
+
// and _shared/, which belong to other installers. The old deny-list named only
|
|
110
|
+
// some of them, so the rest were treated as skills whose SKILL.md "failed to
|
|
111
|
+
// install" — 5 phantom failures that failed the whole install transaction.
|
|
112
|
+
// A new sibling directory now costs nothing; under a deny-list it would break
|
|
113
|
+
// installation until someone remembered to add it.
|
|
114
|
+
return existsSync(join(itemPath, 'SKILL.md'));
|
|
122
115
|
});
|
|
123
116
|
} catch {
|
|
124
117
|
return [];
|
|
125
118
|
}
|
|
126
119
|
}
|
|
127
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Every directory name in the skills source tree, including the ones that are not
|
|
123
|
+
* skills (`_shared`, `agents`, `ai`, `commands`, `tools`, `workflows`).
|
|
124
|
+
*
|
|
125
|
+
* Used as a provenance test: if a directory in an adopter's skills folder has a
|
|
126
|
+
* counterpart here, UDS put it there. If it does not, UDS did not — it is either
|
|
127
|
+
* an adopter's own skill or an artefact of a UDS version too old to reason about,
|
|
128
|
+
* and deleting it is not this tool's call to make.
|
|
129
|
+
*
|
|
130
|
+
* The distinction matters because an older CLI copied the non-skill siblings in
|
|
131
|
+
* by mistake, so a strict "is it a skill we ship" test would leave those behind
|
|
132
|
+
* while a naive "is it in the skills folder" test deletes hand-written skills.
|
|
133
|
+
* dev-platform has fourteen of those. (XSPEC-343 R2)
|
|
134
|
+
*
|
|
135
|
+
* @returns {Set<string>} Directory names present in the skills source tree
|
|
136
|
+
*/
|
|
137
|
+
export function getSkillsSourceEntryNames() {
|
|
138
|
+
if (!existsSync(SKILLS_LOCAL_DIR)) {
|
|
139
|
+
return new Set();
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
return new Set(
|
|
143
|
+
readdirSync(SKILLS_LOCAL_DIR, { withFileTypes: true })
|
|
144
|
+
.filter((e) => e.isDirectory())
|
|
145
|
+
.map((e) => e.name)
|
|
146
|
+
);
|
|
147
|
+
} catch {
|
|
148
|
+
return new Set();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
128
152
|
/**
|
|
129
153
|
* Get list of available command names from local directory
|
|
130
154
|
* @returns {string[]} Array of command names (without .md extension)
|
|
@@ -550,19 +574,10 @@ export async function installCommandsForAgent(agent, level = 'project', commandN
|
|
|
550
574
|
return results;
|
|
551
575
|
}
|
|
552
576
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
*/
|
|
558
|
-
function getCommandFileExtension(agent) {
|
|
559
|
-
// Gemini CLI uses TOML format
|
|
560
|
-
if (agent === 'gemini-cli') {
|
|
561
|
-
return '.toml';
|
|
562
|
-
}
|
|
563
|
-
// Most agents use markdown
|
|
564
|
-
return '.md';
|
|
565
|
-
}
|
|
577
|
+
// getCommandFileExtension moved to config/ai-agent-paths.js — the reconciler's
|
|
578
|
+
// scanner needs the same mapping, and a second private copy is how the writer
|
|
579
|
+
// and the reader drift apart. It now reads `commandFormat` from the agent config
|
|
580
|
+
// instead of hard-coding one agent id. (XSPEC-343 R2)
|
|
566
581
|
|
|
567
582
|
/**
|
|
568
583
|
* Install a single command to target directory
|