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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "universal-dev-standards",
3
- "version": "6.3.7",
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",
@@ -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
- const installedStandards = (manifest.standards || []).map(s => basename(s));
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.
@@ -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
- const installedStandardsList = manifest.standards?.map(s => basename(s)) || [];
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'
@@ -47,7 +47,37 @@ export function render(result) {
47
47
  }
48
48
 
49
49
  if (!result.hasLockfile) {
50
- lines.push(chalk.yellow(' no package-lock.json — nothing to compare the registry against'));
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) {
@@ -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
- const installedStandardsList = manifest.standards?.map(s => basename(s)) || [];
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
- installedStandards: (manifest.standards || []).map(s => basename(s)),
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
- installedStandards: manifest.standards?.map((x) => basename(x)) || [],
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
- /** True when every dependency was checked, agreed, and needs no pinning. */
379
- clean: drifted.length === 0 && unverifiable.length === 0 && unpinnedNative.length === 0,
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': { task: 'Workflow gates', when: 'Before any workflow phase', priority: 'MUST' }
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(installedStandards, format, language = 'zh-tw') {
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
- // Separate core standards from options
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
- sections.push('Reference: `.standards/workflow-enforcement.ai.yaml`');
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
- sections.push('参考: `.standards/workflow-enforcement.ai.yaml`');
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
- sections.push('Reference: `.standards/workflow-enforcement.ai.yaml`');
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 workflow-enforcement standard is installed
2722
- if (installedStandards.some(s => basename(s) === 'workflow-enforcement.ai.yaml')) {
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(installedStandards, mode, format, language = 'zh-tw') {
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 = basename(standardPath);
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
- const keyStandards = installedStandards
3331
- .filter(s => {
3332
- const name = basename(s);
3333
- return name.endsWith('.ai.yaml');
3334
- });
3335
-
3336
- for (const std of keyStandards) {
3337
- const name = basename(std).replace('.ai.yaml', '');
3338
- lines.push(`- \`.standards/${basename(std)}\` — ${name}`);
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.');
@@ -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
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "version": "6.3.7",
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.7"
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.7",
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.7",
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.7",
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.7",
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.7",
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"