universal-dev-standards 6.3.7 → 6.3.8
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/ai/standards/design-document-standards.ai.yaml +129 -1
- package/bundled/ai/standards/estimation-standards.ai.yaml +123 -1
- package/bundled/ai/standards/privacy-standards.ai.yaml +148 -2
- package/bundled/ai/standards/supply-chain-security-standards.ai.yaml +157 -9
- package/bundled/locales/zh-CN/CHANGELOG.md +12 -3
- package/bundled/locales/zh-CN/README.md +1 -1
- package/bundled/locales/zh-TW/CHANGELOG.md +12 -3
- package/bundled/locales/zh-TW/README.md +1 -1
- package/package.json +1 -4
- package/src/commands/check.js +18 -3
- package/src/commands/config.js +5 -1
- package/src/commands/deps.js +31 -1
- package/src/commands/update.js +16 -3
- package/src/utils/dependency-resolution.js +34 -2
- package/src/utils/integration-generator.js +103 -27
- package/src/utils/registry.js +45 -0
- package/standards-registry.json +7 -7
- package/src/schemas/standard.schema.json +0 -117
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "universal-dev-standards",
|
|
3
|
-
"version": "6.3.
|
|
3
|
+
"version": "6.3.8",
|
|
4
4
|
"description": "CLI tool for adopting Universal Development Standards",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"documentation",
|
|
@@ -69,10 +69,7 @@
|
|
|
69
69
|
"devDependencies": {
|
|
70
70
|
"@eslint/js": "^10.0.1",
|
|
71
71
|
"@vitest/coverage-v8": "^4.1.5",
|
|
72
|
-
"ajv": "^8.20.0",
|
|
73
|
-
"ajv-formats": "^3.0.1",
|
|
74
72
|
"eslint": "10.7.0",
|
|
75
|
-
"glob": "^13.0.1",
|
|
76
73
|
"globals": "17.7.0",
|
|
77
74
|
"husky": "^9.1.7",
|
|
78
75
|
"lint-staged": "^17.0.3",
|
package/src/commands/check.js
CHANGED
|
@@ -7,8 +7,7 @@ import { execSync } from 'child_process';
|
|
|
7
7
|
import { readManifest, writeManifest, isInitialized, copyStandard, copyIntegration } from '../utils/copier.js';
|
|
8
8
|
import {
|
|
9
9
|
getAllStandards,
|
|
10
|
-
getRepositoryInfo
|
|
11
|
-
} from '../utils/registry.js';
|
|
10
|
+
getRepositoryInfo, resolveStandardFilename } from '../utils/registry.js';
|
|
12
11
|
import {
|
|
13
12
|
computeFileHash,
|
|
14
13
|
compareFileHash,
|
|
@@ -1244,7 +1243,23 @@ function checkAgentsMdSync(manifest, projectPath, msg) {
|
|
|
1244
1243
|
}
|
|
1245
1244
|
|
|
1246
1245
|
const content = readFileSync(agentsMdPath, 'utf-8');
|
|
1247
|
-
|
|
1246
|
+
// Resolved through the registry, not basename()d.
|
|
1247
|
+
//
|
|
1248
|
+
// This check reported "standards synced (7/7)" for a project with seventy
|
|
1249
|
+
// standards in its manifest. basename() leaves a registry ID unchanged, the
|
|
1250
|
+
// `.ai.yaml` filter below then drops every ID, and only the seven option
|
|
1251
|
+
// entries — which are stored as paths — survived to become the denominator.
|
|
1252
|
+
// Seven of seven were present, so it ticked, both before the AGENTS.md block
|
|
1253
|
+
// listed the other sixty-three and after, when seven of its paths pointed at
|
|
1254
|
+
// nothing. A drift check blind to ninety percent of the content.
|
|
1255
|
+
//
|
|
1256
|
+
// The AI-tool integration check ~170 lines above already builds this exact
|
|
1257
|
+
// mapping and its comment names the exact case (`error-code-standards` ->
|
|
1258
|
+
// `error-codes.ai.yaml`). The knowledge was in the file; it had not reached
|
|
1259
|
+
// its sibling.
|
|
1260
|
+
const installedStandards = (manifest.standards || [])
|
|
1261
|
+
.map(entry => resolveStandardFilename(entry, manifest.format || 'ai'))
|
|
1262
|
+
.filter(Boolean);
|
|
1248
1263
|
|
|
1249
1264
|
// Same as the AI-tool integration check above: an index-mode block declares a
|
|
1250
1265
|
// count instead of listing names, so the name grep below cannot apply to it.
|
package/src/commands/config.js
CHANGED
|
@@ -917,7 +917,10 @@ export async function runProjectConfiguration(options) {
|
|
|
917
917
|
const intSpinner = ora(msgObj.regeneratingIntegrations).start();
|
|
918
918
|
|
|
919
919
|
// Build installed standards list
|
|
920
|
-
|
|
920
|
+
// Raw, not basename()d. Resolution needs the registry (an ID is not a
|
|
921
|
+
// filename) and the `/options/` segment (it is what classifies an entry).
|
|
922
|
+
// basename() removes both. See resolveStandardFilename.
|
|
923
|
+
const installedStandardsList = manifest.standards || [];
|
|
921
924
|
|
|
922
925
|
// Determine language setting
|
|
923
926
|
let commonLanguage = 'en';
|
|
@@ -941,6 +944,7 @@ export async function runProjectConfiguration(options) {
|
|
|
941
944
|
categories: ['anti-hallucination', 'commit-standards', 'code-review'],
|
|
942
945
|
language: commonLanguage,
|
|
943
946
|
installedStandards: installedStandardsList,
|
|
947
|
+
standardsFormat: manifest.format || 'ai',
|
|
944
948
|
contentMode: newContentMode,
|
|
945
949
|
// Pass output_language for dynamic commit standards generation
|
|
946
950
|
outputLanguage: newOptions.output_language || 'english'
|
package/src/commands/deps.js
CHANGED
|
@@ -47,7 +47,37 @@ export function render(result) {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
if (!result.hasLockfile) {
|
|
50
|
-
lines.push(
|
|
50
|
+
lines.push(
|
|
51
|
+
result.foreignLockfile
|
|
52
|
+
? chalk.yellow(
|
|
53
|
+
` found ${result.foreignLockfile}, and this command reads package-lock.json — the` +
|
|
54
|
+
' tested column cannot be filled in',
|
|
55
|
+
)
|
|
56
|
+
: chalk.yellow(' no package-lock.json — nothing to compare the registry against'),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Zero examined is reported before anything else can be said about the
|
|
61
|
+
// result, because everything else would be said about nothing. The three
|
|
62
|
+
// problem lists are empty here for the same reason an empty room has no
|
|
63
|
+
// untidy corners, and the tick below used to fire on exactly that.
|
|
64
|
+
if (result.examined === 0) {
|
|
65
|
+
lines.push('');
|
|
66
|
+
lines.push(chalk.yellow(' Nothing was examined, so nothing is being asserted.'));
|
|
67
|
+
lines.push(
|
|
68
|
+
chalk.dim(
|
|
69
|
+
result.hasLockfile
|
|
70
|
+
? ' This package declares no runtime dependencies. If you expected some,'
|
|
71
|
+
: ' Without a readable lockfile there is no tested column. If you expected',
|
|
72
|
+
),
|
|
73
|
+
);
|
|
74
|
+
lines.push(
|
|
75
|
+
chalk.dim(
|
|
76
|
+
result.hasLockfile
|
|
77
|
+
? ' check that they are in "dependencies" rather than "devDependencies".'
|
|
78
|
+
: ' dependencies here, check the manifest and the lockfile format.',
|
|
79
|
+
),
|
|
80
|
+
);
|
|
51
81
|
}
|
|
52
82
|
|
|
53
83
|
if (result.drifted.length > 0) {
|
package/src/commands/update.js
CHANGED
|
@@ -589,7 +589,10 @@ export async function updateCommand(options) {
|
|
|
589
589
|
const intSpinner = ora(msg.syncingIntegrations).start();
|
|
590
590
|
|
|
591
591
|
// Build installed standards list
|
|
592
|
-
|
|
592
|
+
// Raw, not basename()d. Resolution needs the registry (an ID is not a
|
|
593
|
+
// filename) and the `/options/` segment (it is what classifies an entry).
|
|
594
|
+
// basename() removes both. See resolveStandardFilename.
|
|
595
|
+
const installedStandardsList = manifest.standards || [];
|
|
593
596
|
|
|
594
597
|
// Determine language setting
|
|
595
598
|
let commonLanguage = 'en';
|
|
@@ -617,6 +620,7 @@ export async function updateCommand(options) {
|
|
|
617
620
|
categories: ['anti-hallucination', 'commit-standards', 'code-review'],
|
|
618
621
|
language: commonLanguage,
|
|
619
622
|
installedStandards: installedStandardsList,
|
|
623
|
+
standardsFormat: manifest.format || 'ai',
|
|
620
624
|
contentMode: resolved.contentMode,
|
|
621
625
|
level: resolved.level,
|
|
622
626
|
// Pass output_language for dynamic commit standards generation
|
|
@@ -645,6 +649,7 @@ export async function updateCommand(options) {
|
|
|
645
649
|
if (manifest.generateAgentsMd && !generatedFiles.has('AGENTS.md')) {
|
|
646
650
|
const summaryConfig = {
|
|
647
651
|
installedStandards: installedStandardsList,
|
|
652
|
+
standardsFormat: manifest.format || 'ai',
|
|
648
653
|
language: manifest.options?.display_language || 'en',
|
|
649
654
|
outputLanguage: manifest.options?.output_language || manifest.options?.commit_language || 'english',
|
|
650
655
|
standardOptions: manifest.options || {}
|
|
@@ -1404,7 +1409,8 @@ export function regenerateIntegrations(projectPath, manifest) {
|
|
|
1404
1409
|
// Regenerate universal AGENTS.md if enabled and not already covered
|
|
1405
1410
|
if (manifest.generateAgentsMd && !generatedFiles.has('AGENTS.md')) {
|
|
1406
1411
|
const summaryConfig = {
|
|
1407
|
-
|
|
1412
|
+
// Raw — writeAgentsMdSummary resolves via the registry now.
|
|
1413
|
+
installedStandards: manifest.standards || [],
|
|
1408
1414
|
language: manifest.options?.display_language || 'en',
|
|
1409
1415
|
outputLanguage: manifest.options?.output_language || manifest.options?.commit_language || 'english',
|
|
1410
1416
|
standardOptions: manifest.options || {}
|
|
@@ -1469,7 +1475,14 @@ async function updateIntegrationsOnly(projectPath, manifest, options = {}) {
|
|
|
1469
1475
|
? 'bilingual'
|
|
1470
1476
|
: (manifest.options?.output_language || manifest.options?.commit_language) === 'traditional-chinese'
|
|
1471
1477
|
? 'zh-tw' : 'en',
|
|
1472
|
-
|
|
1478
|
+
// Passed raw. `basename()` here destroyed the two things the
|
|
1479
|
+
// generator needs: a registry ID cannot be turned back into a
|
|
1480
|
+
// filename once it has been through basename() (it comes out
|
|
1481
|
+
// unchanged, and an ID is not a filename), and an option entry loses
|
|
1482
|
+
// the `/options/` segment that classifies it. Resolution belongs
|
|
1483
|
+
// where the registry is consulted, not at the call site.
|
|
1484
|
+
installedStandards: manifest.standards || [],
|
|
1485
|
+
standardsFormat: manifest.format || 'ai',
|
|
1473
1486
|
contentMode: resolvedMode.contentMode,
|
|
1474
1487
|
level: resolvedMode.level,
|
|
1475
1488
|
outputLanguage: manifest.options?.output_language || manifest.options?.commit_language || 'english'
|
|
@@ -298,6 +298,15 @@ export async function measureResolutionDrift(root, options = {}) {
|
|
|
298
298
|
const lockPath = join(root, 'package-lock.json');
|
|
299
299
|
const lock = existsSync(lockPath) ? JSON.parse(readFileSync(lockPath, 'utf8')) : null;
|
|
300
300
|
|
|
301
|
+
// Which lockfile is present, when it is not npm's. Only package-lock.json is
|
|
302
|
+
// understood, and "not understood" has to be reported as a different thing
|
|
303
|
+
// from "not there" — a pnpm project told it had no lockfile will reasonably
|
|
304
|
+
// conclude the command is confused, and stop reading.
|
|
305
|
+
const foreignLockfile =
|
|
306
|
+
lock === null
|
|
307
|
+
? ['pnpm-lock.yaml', 'yarn.lock', 'bun.lockb', 'bun.lock'].find((f) => existsSync(join(root, f))) ?? null
|
|
308
|
+
: null;
|
|
309
|
+
|
|
301
310
|
// Workspaces are examined too. A monorepo that declares its shipped
|
|
302
311
|
// front-end in a workspace and its server at the root has two manifests, and
|
|
303
312
|
// reading only the root reports a clean subset as if it were the whole — the
|
|
@@ -375,7 +384,30 @@ export async function measureResolutionDrift(root, options = {}) {
|
|
|
375
384
|
drifted,
|
|
376
385
|
unverifiable,
|
|
377
386
|
unpinnedNative,
|
|
378
|
-
/**
|
|
379
|
-
|
|
387
|
+
/**
|
|
388
|
+
* A lockfile in a format this command cannot read, when there is no
|
|
389
|
+
* package-lock.json. `hasLockfile: false` alone reads as "this project has
|
|
390
|
+
* no lockfile", which for a pnpm or yarn project is simply untrue, and the
|
|
391
|
+
* report said so out loud: "no package-lock.json — nothing to compare"
|
|
392
|
+
* printed against a repo holding a perfectly good pnpm-lock.yaml.
|
|
393
|
+
*/
|
|
394
|
+
foreignLockfile,
|
|
395
|
+
/**
|
|
396
|
+
* True when every dependency was checked, agreed, and needs no pinning.
|
|
397
|
+
*
|
|
398
|
+
* `examined === 0` is excluded, and that exclusion is the point. Run
|
|
399
|
+
* against asiaostrich-telemetry-client — zero runtime dependencies, a
|
|
400
|
+
* pnpm lockfile this command cannot read — the three problem lists came
|
|
401
|
+
* back empty and the report printed "✓ every dependency resolves to the
|
|
402
|
+
* version you test against". Nothing had been examined. The tick was
|
|
403
|
+
* true and it was also the exact shape this command exists to refuse:
|
|
404
|
+
* "nothing to check" and "everything checked out" arriving as the same
|
|
405
|
+
* green line.
|
|
406
|
+
*/
|
|
407
|
+
clean:
|
|
408
|
+
rows.length > 0 &&
|
|
409
|
+
drifted.length === 0 &&
|
|
410
|
+
unverifiable.length === 0 &&
|
|
411
|
+
unpinnedNative.length === 0,
|
|
380
412
|
};
|
|
381
413
|
}
|
|
@@ -3,7 +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
|
+
import { resolveSelectedOptionSources, resolveStandardFilename } from './registry.js';
|
|
7
7
|
import { getAgentConfig, getAgentTier } from '../config/ai-agent-paths.js';
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -101,8 +101,14 @@ const STANDARD_TASK_MAPPING = {
|
|
|
101
101
|
'test-completeness-dimensions.md': { task: 'Test coverage', when: 'When evaluating tests', priority: 'SHOULD' },
|
|
102
102
|
'git-workflow.ai.yaml': { task: 'Git workflow', when: 'Branch/merge decisions', priority: 'SHOULD' },
|
|
103
103
|
'developer-memory.ai.yaml': { task: 'Developer memory', when: 'Always (protocol)', priority: 'SHOULD' },
|
|
104
|
-
'project-context-memory.ai.yaml': { task: 'Project context', when: 'Planning & Coding', priority: 'MUST' }
|
|
105
|
-
'workflow-enforcement.ai.yaml'
|
|
104
|
+
'project-context-memory.ai.yaml': { task: 'Project context', when: 'Planning & Coding', priority: 'MUST' }
|
|
105
|
+
// 'workflow-enforcement.ai.yaml' was removed in 6.0.0 (MIGRATION-v6 §2) along
|
|
106
|
+
// with seven other machine-readable standards whose runtime moved to the
|
|
107
|
+
// adoption layer. Its entry here pointed a task mapping at
|
|
108
|
+
// `.standards/workflow-enforcement.ai.yaml`, a file that has not shipped
|
|
109
|
+
// since. The human-readable core/workflow-enforcement.md was deliberately
|
|
110
|
+
// kept upstream, but adopters receive .standards/, not core/, so it is not a
|
|
111
|
+
// substitute path.
|
|
106
112
|
};
|
|
107
113
|
|
|
108
114
|
/**
|
|
@@ -2399,18 +2405,39 @@ All responses should be in **Traditional Chinese (繁體中文)**, with technica
|
|
|
2399
2405
|
* @param {string} language - Language: 'en', 'zh-tw', or 'bilingual'
|
|
2400
2406
|
* @returns {string} Generated minimal reference
|
|
2401
2407
|
*/
|
|
2402
|
-
export function generateMinimalStandardsReference(
|
|
2408
|
+
export function generateMinimalStandardsReference(
|
|
2409
|
+
installedStandards,
|
|
2410
|
+
format,
|
|
2411
|
+
language = 'zh-tw',
|
|
2412
|
+
standardsFormat = 'ai',
|
|
2413
|
+
) {
|
|
2403
2414
|
if (!installedStandards || installedStandards.length === 0) {
|
|
2404
2415
|
return '';
|
|
2405
2416
|
}
|
|
2406
2417
|
|
|
2407
2418
|
const coreStandards = [];
|
|
2408
2419
|
const optionStandards = [];
|
|
2409
|
-
|
|
2410
|
-
|
|
2420
|
+
const unresolved = [];
|
|
2421
|
+
|
|
2422
|
+
// Separate core standards from options.
|
|
2423
|
+
//
|
|
2424
|
+
// `basename(entry)` used to serve for both, and it is only right for one:
|
|
2425
|
+
// option entries are paths, core entries are registry IDs, and an ID is not
|
|
2426
|
+
// a filename. That produced `.standards/error-code-standards` for a file
|
|
2427
|
+
// installed as `error-codes.ai.yaml` — seven dead paths out of seventy in a
|
|
2428
|
+
// real adopter's AGENTS.md, directly under a line telling the agent it must
|
|
2429
|
+
// read them.
|
|
2411
2430
|
for (const standardPath of installedStandards) {
|
|
2412
|
-
const filename = basename(standardPath);
|
|
2413
2431
|
const isOption = standardPath.includes('/options/') || standardPath.includes('\\options\\');
|
|
2432
|
+
const filename = resolveStandardFilename(standardPath, standardsFormat);
|
|
2433
|
+
|
|
2434
|
+
// An entry we cannot resolve is not written out as if it were a path. A
|
|
2435
|
+
// listed path that does not exist is worse than an omission: the reader
|
|
2436
|
+
// cannot tell it apart from the ones that do, so it discredits the list.
|
|
2437
|
+
if (!filename) {
|
|
2438
|
+
unresolved.push(standardPath);
|
|
2439
|
+
continue;
|
|
2440
|
+
}
|
|
2414
2441
|
|
|
2415
2442
|
if (isOption) {
|
|
2416
2443
|
optionStandards.push({ filename, path: standardPath });
|
|
@@ -2446,6 +2473,15 @@ export function generateMinimalStandardsReference(installedStandards, format, la
|
|
|
2446
2473
|
}
|
|
2447
2474
|
sections.push('');
|
|
2448
2475
|
}
|
|
2476
|
+
|
|
2477
|
+
// Said out loud rather than dropped quietly. A shorter list and a complete
|
|
2478
|
+
// one read the same, and only the manifest knows which this is.
|
|
2479
|
+
if (unresolved.length > 0) {
|
|
2480
|
+
sections.push(language === 'en'
|
|
2481
|
+
? `> ${unresolved.length} manifest entr${unresolved.length === 1 ? 'y' : 'ies'} could not be matched to an installed file and ${unresolved.length === 1 ? 'is' : 'are'} not listed above: ${unresolved.join(', ')}. Run \`uds update\` — if that does not clear it, the manifest and the registry disagree.`
|
|
2482
|
+
: `> 有 ${unresolved.length} 筆 manifest 項目對不到已安裝的檔案,未列於上方:${unresolved.join('、')}。請執行 \`uds update\`;若仍未消失,代表 manifest 與 registry 不一致。`);
|
|
2483
|
+
sections.push('');
|
|
2484
|
+
}
|
|
2449
2485
|
} else {
|
|
2450
2486
|
// Plaintext format
|
|
2451
2487
|
sections.push(language === 'en'
|
|
@@ -2468,6 +2504,13 @@ export function generateMinimalStandardsReference(installedStandards, format, la
|
|
|
2468
2504
|
sections.push(`- .standards/options/${std.filename}`);
|
|
2469
2505
|
}
|
|
2470
2506
|
}
|
|
2507
|
+
|
|
2508
|
+
if (unresolved.length > 0) {
|
|
2509
|
+
sections.push('');
|
|
2510
|
+
sections.push(language === 'en'
|
|
2511
|
+
? `NOTE: ${unresolved.length} manifest entries could not be matched to an installed file and are not listed: ${unresolved.join(', ')}`
|
|
2512
|
+
: `注意:有 ${unresolved.length} 筆 manifest 項目對不到已安裝的檔案,未列出:${unresolved.join('、')}`);
|
|
2513
|
+
}
|
|
2471
2514
|
sections.push('');
|
|
2472
2515
|
}
|
|
2473
2516
|
|
|
@@ -2562,7 +2605,7 @@ function generateWorkflowGateContent(language) {
|
|
|
2562
2605
|
sections.push('| BDD | AUTOMATION | `.feature` file exists | → FORMULATION |');
|
|
2563
2606
|
sections.push('| Commit | feat/fix | Check active specs | → Suggest `Refs: SPEC-XXX` |');
|
|
2564
2607
|
sections.push('');
|
|
2565
|
-
|
|
2608
|
+
|
|
2566
2609
|
} else if (language === 'zh-cn') {
|
|
2567
2610
|
sections.push('## 工作流程强制闸门');
|
|
2568
2611
|
sections.push('');
|
|
@@ -2585,7 +2628,7 @@ function generateWorkflowGateContent(language) {
|
|
|
2585
2628
|
sections.push('| BDD | AUTOMATION | `.feature` 文件存在 | → FORMULATION |');
|
|
2586
2629
|
sections.push('| Commit | feat/fix | 检查活跃 spec | → 建议 `Refs: SPEC-XXX` |');
|
|
2587
2630
|
sections.push('');
|
|
2588
|
-
|
|
2631
|
+
|
|
2589
2632
|
} else {
|
|
2590
2633
|
sections.push('## Workflow Enforcement Gates');
|
|
2591
2634
|
sections.push('');
|
|
@@ -2608,7 +2651,7 @@ function generateWorkflowGateContent(language) {
|
|
|
2608
2651
|
sections.push('| BDD | AUTOMATION | `.feature` file exists | → FORMULATION |');
|
|
2609
2652
|
sections.push('| Commit | feat/fix | Check active specs | → Suggest `Refs: SPEC-XXX` |');
|
|
2610
2653
|
sections.push('');
|
|
2611
|
-
|
|
2654
|
+
|
|
2612
2655
|
}
|
|
2613
2656
|
|
|
2614
2657
|
return sections.join('\n');
|
|
@@ -2631,6 +2674,10 @@ export function generateIntegrationContent(config) {
|
|
|
2631
2674
|
// New fields for enhanced standards compliance
|
|
2632
2675
|
installedStandards = [],
|
|
2633
2676
|
contentMode = 'minimal',
|
|
2677
|
+
// Content format of the installed standards ('ai' -> *.ai.yaml,
|
|
2678
|
+
// 'human' -> *.md). Needed to turn a manifest's registry IDs into the
|
|
2679
|
+
// filenames actually on disk; without it the block prints IDs as paths.
|
|
2680
|
+
standardsFormat = 'ai',
|
|
2634
2681
|
// Output language option for dynamic commit standards generation
|
|
2635
2682
|
outputLanguage = 'english'
|
|
2636
2683
|
} = config;
|
|
@@ -2691,7 +2738,8 @@ export function generateIntegrationContent(config) {
|
|
|
2691
2738
|
standardsContent += generateMinimalStandardsReference(
|
|
2692
2739
|
installedStandards,
|
|
2693
2740
|
format,
|
|
2694
|
-
language
|
|
2741
|
+
language,
|
|
2742
|
+
standardsFormat
|
|
2695
2743
|
);
|
|
2696
2744
|
} else {
|
|
2697
2745
|
// Index/Full mode: detailed compliance instructions + index
|
|
@@ -2699,7 +2747,8 @@ export function generateIntegrationContent(config) {
|
|
|
2699
2747
|
installedStandards,
|
|
2700
2748
|
contentMode,
|
|
2701
2749
|
format,
|
|
2702
|
-
language
|
|
2750
|
+
language,
|
|
2751
|
+
standardsFormat
|
|
2703
2752
|
);
|
|
2704
2753
|
|
|
2705
2754
|
const standardsIndex = generateStandardsIndex(
|
|
@@ -2718,8 +2767,20 @@ export function generateIntegrationContent(config) {
|
|
|
2718
2767
|
sections.push('\n---\n');
|
|
2719
2768
|
}
|
|
2720
2769
|
|
|
2721
|
-
// Add workflow enforcement gates if
|
|
2722
|
-
|
|
2770
|
+
// Add workflow enforcement gates if the project still declares the standard.
|
|
2771
|
+
//
|
|
2772
|
+
// Matched against both forms because a manifest can hold either: the legacy
|
|
2773
|
+
// path (pre-3.4.0) or the ID. vibeops and dev-platform both still carry
|
|
2774
|
+
// `workflow-enforcement` as an ID today, two majors after 6.0.0 removed the
|
|
2775
|
+
// file — checking only the filename silently stopped honouring their
|
|
2776
|
+
// declaration. The section's content is self-contained; what it no longer
|
|
2777
|
+
// does is point at `.standards/workflow-enforcement.ai.yaml`, which has not
|
|
2778
|
+
// existed since 6.0.0 and was being written into adopters' instruction
|
|
2779
|
+
// files regardless (asiaostrich-telemetry-client/CLAUDE.md:145).
|
|
2780
|
+
if (installedStandards.some(s => {
|
|
2781
|
+
const name = basename(s);
|
|
2782
|
+
return name === 'workflow-enforcement.ai.yaml' || name === 'workflow-enforcement';
|
|
2783
|
+
})) {
|
|
2723
2784
|
const workflowGateContent = generateWorkflowGateContent(language);
|
|
2724
2785
|
sections.push(workflowGateContent);
|
|
2725
2786
|
sections.push('\n---\n');
|
|
@@ -2868,7 +2929,13 @@ export function integrationFileExists(tool, projectPath) {
|
|
|
2868
2929
|
* @param {string} language - Language: 'en', 'zh-tw', or 'bilingual'
|
|
2869
2930
|
* @returns {string} Generated compliance instructions
|
|
2870
2931
|
*/
|
|
2871
|
-
export function generateComplianceInstructions(
|
|
2932
|
+
export function generateComplianceInstructions(
|
|
2933
|
+
installedStandards,
|
|
2934
|
+
mode,
|
|
2935
|
+
format,
|
|
2936
|
+
language = 'zh-tw',
|
|
2937
|
+
standardsFormat = 'ai',
|
|
2938
|
+
) {
|
|
2872
2939
|
if (mode === 'minimal' || !installedStandards || installedStandards.length === 0) {
|
|
2873
2940
|
return '';
|
|
2874
2941
|
}
|
|
@@ -2876,9 +2943,16 @@ export function generateComplianceInstructions(installedStandards, mode, format,
|
|
|
2876
2943
|
const mustFollow = [];
|
|
2877
2944
|
const shouldFollow = [];
|
|
2878
2945
|
|
|
2879
|
-
// Categorize standards by priority
|
|
2946
|
+
// Categorize standards by priority.
|
|
2947
|
+
//
|
|
2948
|
+
// Keyed by installed filename, so a registry ID has to be resolved first —
|
|
2949
|
+
// `basename()` on an ID returns the ID, which matches nothing in the table,
|
|
2950
|
+
// and the standard silently gets no task mapping. No error, no shorter
|
|
2951
|
+
// output anyone would notice: just a standard the agent is never told when
|
|
2952
|
+
// to apply.
|
|
2880
2953
|
for (const standardPath of installedStandards) {
|
|
2881
|
-
const filename =
|
|
2954
|
+
const filename = resolveStandardFilename(standardPath, standardsFormat);
|
|
2955
|
+
if (!filename) continue;
|
|
2882
2956
|
const mapping = STANDARD_TASK_MAPPING[filename];
|
|
2883
2957
|
if (mapping) {
|
|
2884
2958
|
const entry = {
|
|
@@ -3155,7 +3229,6 @@ export function getToolFilePath(tool) {
|
|
|
3155
3229
|
}
|
|
3156
3230
|
|
|
3157
3231
|
|
|
3158
|
-
|
|
3159
3232
|
/**
|
|
3160
3233
|
* Get default commands based on ecosystem
|
|
3161
3234
|
* @param {'node'|'python'|'go'|'java_maven'|'java_gradle'|'rust'|'ruby'} ecosystem
|
|
@@ -3327,15 +3400,18 @@ export function generateAgentsMdSummary(config = {}) {
|
|
|
3327
3400
|
lines.push('All standards are in `.standards/`. Installed standards:');
|
|
3328
3401
|
lines.push('');
|
|
3329
3402
|
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3403
|
+
// Resolved before filtering, not after. The filter asks for an `.ai.yaml`
|
|
3404
|
+
// suffix and a manifest's core entries are registry IDs, which carry no
|
|
3405
|
+
// suffix — so every core standard failed the test and the block listed
|
|
3406
|
+
// only the option files. On one adopter that meant seven lines standing in
|
|
3407
|
+
// for seventy, under a heading reading "Installed Standards", with nothing
|
|
3408
|
+
// anywhere reporting the sixty-three that had been dropped.
|
|
3409
|
+
const resolved = installedStandards
|
|
3410
|
+
.map((s) => resolveStandardFilename(s, 'ai'))
|
|
3411
|
+
.filter((name) => name?.endsWith('.ai.yaml'));
|
|
3412
|
+
|
|
3413
|
+
for (const filename of resolved) {
|
|
3414
|
+
lines.push(`- \`.standards/${filename}\` — ${filename.replace('.ai.yaml', '')}`);
|
|
3339
3415
|
}
|
|
3340
3416
|
} else {
|
|
3341
3417
|
lines.push('No standards installed yet. Run `npx uds init` to install.');
|
package/src/utils/registry.js
CHANGED
|
@@ -244,3 +244,48 @@ export function resolveSelectedOptionSources(manifestOptions, format = 'ai') {
|
|
|
244
244
|
|
|
245
245
|
return paths;
|
|
246
246
|
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* The filename a manifest entry has on disk under `.standards/`.
|
|
250
|
+
*
|
|
251
|
+
* The `standards` array in a manifest is deliberately mixed: core standards are
|
|
252
|
+
* stored as registry IDs (v3.4.0 onwards), option files as their upstream
|
|
253
|
+
* source path, because options have no registry ID. Anything that turns those
|
|
254
|
+
* entries into paths has to handle both, and three places in
|
|
255
|
+
* integration-generator.js did not — each failing differently and silently:
|
|
256
|
+
*
|
|
257
|
+
* - minimal mode printed `.standards/<id>`, and an ID is not a filename.
|
|
258
|
+
* `error-code-standards` installs as `error-codes.ai.yaml`, `ai-agreement`
|
|
259
|
+
* as `ai-agreement-standards.ai.yaml`. Seven of seventy paths in a real
|
|
260
|
+
* adopter's AGENTS.md pointed at nothing, in a block whose first line tells
|
|
261
|
+
* the agent it must read them.
|
|
262
|
+
* - the index block filtered entries by an `.ai.yaml` suffix, which no ID
|
|
263
|
+
* has, so every core standard was dropped. That is why the same adopter's
|
|
264
|
+
* previous block listed seven options and none of its sixty-three core
|
|
265
|
+
* standards.
|
|
266
|
+
* - the task-mapping lookup is keyed by filename, so an ID missed every
|
|
267
|
+
* time and the standard quietly got no mapping.
|
|
268
|
+
*
|
|
269
|
+
* Returns null when the entry cannot be resolved — a caller must not fall back
|
|
270
|
+
* to printing the raw entry, which is the bug above.
|
|
271
|
+
*
|
|
272
|
+
* @param {string} entry - A manifest `standards` entry: registry ID or path
|
|
273
|
+
* @param {string} format - Content format: 'ai' or 'human'
|
|
274
|
+
* @returns {string|null} Basename as installed, or null if unresolvable
|
|
275
|
+
*/
|
|
276
|
+
export function resolveStandardFilename(entry, format = 'ai') {
|
|
277
|
+
if (typeof entry !== 'string' || entry.length === 0) return null;
|
|
278
|
+
|
|
279
|
+
// A path (option files, and legacy pre-3.4.0 manifests) already names itself.
|
|
280
|
+
if (entry.includes('/') || entry.includes('.')) {
|
|
281
|
+
const parts = entry.split(/[/\\]/);
|
|
282
|
+
return parts[parts.length - 1] || null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const found = getAllStandards().find((s) => s.id === entry);
|
|
286
|
+
if (!found) return null;
|
|
287
|
+
const source = getStandardSource(found, format);
|
|
288
|
+
if (!source) return null;
|
|
289
|
+
const parts = source.split(/[/\\]/);
|
|
290
|
+
return parts[parts.length - 1] || null;
|
|
291
|
+
}
|
package/standards-registry.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
-
"version": "6.3.
|
|
3
|
+
"version": "6.3.8",
|
|
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.3.
|
|
61
|
+
"version": "6.3.8"
|
|
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.3.
|
|
68
|
+
"version": "6.3.8",
|
|
69
69
|
"note": "Skills are now included in the main repository under skills/"
|
|
70
70
|
}
|
|
71
71
|
},
|
|
@@ -2236,7 +2236,7 @@
|
|
|
2236
2236
|
"id": "license-compliance",
|
|
2237
2237
|
"name": "License Compliance Standards",
|
|
2238
2238
|
"nameZh": "授權合規標準",
|
|
2239
|
-
"version": "6.3.
|
|
2239
|
+
"version": "6.3.8",
|
|
2240
2240
|
"source": {
|
|
2241
2241
|
"human": "core/license-compliance.md",
|
|
2242
2242
|
"ai": "ai/standards/license-compliance.ai.yaml"
|
|
@@ -2248,7 +2248,7 @@
|
|
|
2248
2248
|
"id": "verification-oracle",
|
|
2249
2249
|
"name": "Verification Oracle Standards",
|
|
2250
2250
|
"nameZh": "驗證 Oracle 標準",
|
|
2251
|
-
"version": "6.3.
|
|
2251
|
+
"version": "6.3.8",
|
|
2252
2252
|
"source": {
|
|
2253
2253
|
"human": "core/verification-oracle.md",
|
|
2254
2254
|
"ai": "ai/standards/verification-oracle.ai.yaml"
|
|
@@ -2260,7 +2260,7 @@
|
|
|
2260
2260
|
"id": "model-provenance",
|
|
2261
2261
|
"name": "Model Provenance Policy Standards",
|
|
2262
2262
|
"nameZh": "模型來源政策標準",
|
|
2263
|
-
"version": "6.3.
|
|
2263
|
+
"version": "6.3.8",
|
|
2264
2264
|
"source": {
|
|
2265
2265
|
"human": "core/model-provenance.md",
|
|
2266
2266
|
"ai": "ai/standards/model-provenance.ai.yaml"
|
|
@@ -2272,7 +2272,7 @@
|
|
|
2272
2272
|
"id": "resource-cost-boundary",
|
|
2273
2273
|
"name": "Resource / Cost Boundary Declaration Standards",
|
|
2274
2274
|
"nameZh": "資源/成本邊界宣告標準",
|
|
2275
|
-
"version": "6.3.
|
|
2275
|
+
"version": "6.3.8",
|
|
2276
2276
|
"source": {
|
|
2277
2277
|
"human": "core/resource-cost-boundary.md",
|
|
2278
2278
|
"ai": "ai/standards/resource-cost-boundary.ai.yaml"
|