universal-dev-standards 6.9.0 → 6.11.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.
Files changed (63) hide show
  1. package/bin/uds.js +2 -0
  2. package/bundled/core/agent-communication-protocol.md +8 -0
  3. package/bundled/core/branch-completion.md +8 -0
  4. package/bundled/core/change-batching-standards.md +8 -0
  5. package/bundled/core/execution-history.md +8 -0
  6. package/bundled/core/pipeline-integration-standards.md +8 -0
  7. package/bundled/core/workflow-enforcement.md +8 -0
  8. package/bundled/core/workflow-state-protocol.md +8 -0
  9. package/bundled/locales/zh-CN/CHANGELOG.md +49 -3
  10. package/bundled/locales/zh-CN/README.md +1 -1
  11. package/bundled/locales/zh-CN/SECURITY.md +1 -1
  12. package/bundled/locales/zh-CN/core/agent-communication-protocol.md +7 -0
  13. package/bundled/locales/zh-CN/core/branch-completion.md +7 -0
  14. package/bundled/locales/zh-CN/core/change-batching-standards.md +7 -0
  15. package/bundled/locales/zh-CN/core/execution-history.md +7 -0
  16. package/bundled/locales/zh-CN/core/pipeline-integration-standards.md +7 -0
  17. package/bundled/locales/zh-CN/core/workflow-enforcement.md +7 -0
  18. package/bundled/locales/zh-CN/core/workflow-state-protocol.md +7 -0
  19. package/bundled/locales/zh-CN/docs/CHEATSHEET.md +1 -1
  20. package/bundled/locales/zh-CN/docs/CLI-INIT-OPTIONS.md +52 -5
  21. package/bundled/locales/zh-CN/docs/FEATURE-REFERENCE.md +3 -1
  22. package/bundled/locales/zh-CN/docs/MIGRATION-v6.md +8 -4
  23. package/bundled/locales/zh-TW/CHANGELOG.md +50 -3
  24. package/bundled/locales/zh-TW/README.md +1 -1
  25. package/bundled/locales/zh-TW/SECURITY.md +1 -1
  26. package/bundled/locales/zh-TW/core/agent-communication-protocol.md +7 -0
  27. package/bundled/locales/zh-TW/core/branch-completion.md +7 -0
  28. package/bundled/locales/zh-TW/core/change-batching-standards.md +7 -0
  29. package/bundled/locales/zh-TW/core/execution-history.md +7 -0
  30. package/bundled/locales/zh-TW/core/pipeline-integration-standards.md +7 -0
  31. package/bundled/locales/zh-TW/core/workflow-enforcement.md +7 -0
  32. package/bundled/locales/zh-TW/core/workflow-state-protocol.md +7 -0
  33. package/bundled/locales/zh-TW/docs/CHEATSHEET.md +1 -1
  34. package/bundled/locales/zh-TW/docs/CLI-INIT-OPTIONS.md +52 -5
  35. package/bundled/locales/zh-TW/docs/FEATURE-REFERENCE.md +3 -1
  36. package/bundled/locales/zh-TW/docs/MIGRATION-v6.md +8 -4
  37. package/bundled/locales/zh-TW/integrations/claude-code/README.md +14 -5
  38. package/package.json +7 -6
  39. package/src/commands/check.js +413 -44
  40. package/src/commands/config.js +34 -28
  41. package/src/commands/init.js +37 -7
  42. package/src/commands/spec.js +2 -2
  43. package/src/commands/update.js +560 -74
  44. package/src/core/manifest.js +39 -1
  45. package/src/flows/init-flow.js +9 -1
  46. package/src/generators/layered-claudemd.js +13 -4
  47. package/src/i18n/messages.js +39 -3
  48. package/src/installers/integration-installer.js +17 -10
  49. package/src/installers/manifest-installer.js +4 -0
  50. package/src/installers/skills-installer.js +4 -4
  51. package/src/installers/standards-installer.js +3 -3
  52. package/src/prompts/init.js +33 -4
  53. package/src/reconciler/actual-state-scanner.js +29 -2
  54. package/src/reconciler/desired-state-calculator.js +51 -2
  55. package/src/reconciler/diff-engine.js +76 -5
  56. package/src/reconciler/plan-executor.js +48 -27
  57. package/src/utils/hasher.js +61 -5
  58. package/src/utils/integration-generator.js +431 -92
  59. package/src/utils/marker-locator.js +140 -0
  60. package/src/utils/reference-sync.js +156 -8
  61. package/src/utils/registry.js +57 -0
  62. package/src/utils/spinner.js +31 -0
  63. package/standards-registry.json +8 -8
@@ -1,18 +1,20 @@
1
1
  import chalk from 'chalk';
2
2
  import { select } from '@inquirer/prompts';
3
- import ora from 'ora';
3
+ import { createSpinner } from '../utils/spinner.js';
4
4
  import { existsSync, readFileSync } from 'fs';
5
5
  import { join, basename } from 'path';
6
6
  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, resolveStandardFilename, resolveStandardSourcePath } from '../utils/registry.js';
10
+ getRepositoryInfo, isShippedFilename, resolveStandardFilename, resolveStandardSourcePath } from '../utils/registry.js';
11
11
  import {
12
12
  computeFileHash,
13
13
  compareFileHash,
14
14
  hasFileHashes,
15
- compareIntegrationBlockHash
15
+ compareIntegrationBlockHash,
16
+ computeIntegrationBlockHash,
17
+ pruneIntegrationFileHashes
16
18
  } from '../utils/hasher.js';
17
19
  import { downloadFromGitHub, getMarketplaceSkillsInfo } from '../utils/github.js';
18
20
  import {
@@ -27,9 +29,12 @@ import {
27
29
  } from '../config/ai-agent-paths.js';
28
30
  import {
29
31
  parseReferences,
32
+ findBrokenPathMentions,
30
33
  compareStandardsWithReferences
31
34
  } from '../utils/reference-sync.js';
32
- import { extractMarkedContent, getToolFilePath, parseStandardsIndexCount, writeIntegrationFile } from '../utils/integration-generator.js';
35
+ import { extractMarkedContent, resolveIntegrationTargetFile, parseStandardsIndexCount, writeIntegrationFile } from '../utils/integration-generator.js';
36
+ import { AmbiguousMarkerError } from '../utils/marker-locator.js';
37
+ import { bumpManifestVersion } from '../core/manifest.js';
33
38
  import { INTEGRATION_MAPPINGS } from '../installers/integration-installer.js';
34
39
  import { getToolFormat } from '../core/constants.js';
35
40
  import { checkForUpdates } from '../utils/npm-registry.js';
@@ -60,6 +65,19 @@ function displayFileIntegritySummary(fileStatus, msg) {
60
65
  }
61
66
  }
62
67
 
68
+ // Reported apart from `missing`, and in grey rather than red, because the
69
+ // adopter who sees this did what the migration guide asked. The remedy named
70
+ // here is the one that works: `--restore` cannot resolve a source for any of
71
+ // them, and used to be the only thing offered.
72
+ if (fileStatus.retired?.length > 0) {
73
+ console.log();
74
+ console.log(chalk.gray(` ${(msg.retiredHeader || '{count} tracked file(s) are no longer shipped by UDS:').replace('{count}', fileStatus.retired.length)}`));
75
+ for (const file of fileStatus.retired) {
76
+ console.log(chalk.gray(` - ${file} (${msg.retired || 'no longer shipped by UDS'})`));
77
+ }
78
+ console.log(chalk.gray(` ${msg.retiredHint || 'Run `uds update --prune` to drop their manifest entries.'}`));
79
+ }
80
+
63
81
  if (fileStatus.noHash.length > 0) {
64
82
  for (const file of fileStatus.noHash) {
65
83
  console.log(chalk.gray(` ? ${file} (${msg.existsNoHash})`));
@@ -71,10 +89,39 @@ function displayFileIntegritySummary(fileStatus, msg) {
71
89
  .replace('{unchanged}', fileStatus.unchanged.length)
72
90
  .replace('{modified}', fileStatus.modified.length)
73
91
  .replace('{missing}', fileStatus.missing.length)}` +
92
+ (fileStatus.retired?.length > 0 ? `, ${fileStatus.retired.length} ${msg.retired || 'no longer shipped'}` : '') +
74
93
  (fileStatus.noHash.length > 0 ? `, ${fileStatus.noHash.length} no hash` : '')));
75
94
  console.log();
76
95
  }
77
96
 
97
+ /**
98
+ * A file `fileHashes` tracks has gone. Is that a problem, or is it the migration
99
+ * guide's instruction carried out?
100
+ *
101
+ * MIGRATION-v6 §2 told adopters to delete the seven descoped `.ai.yaml` copies
102
+ * by hand. Doing so left the hash entries behind, and this check called each one
103
+ * `missing` and offered `uds check --restore` — a restore that then failed on
104
+ * every one with "could not determine source", because there has been no source
105
+ * upstream since 6.0.0. The tool asked the user to undo its own documentation,
106
+ * using a command it already knew could not work.
107
+ *
108
+ * The distinguishing question is whether UDS still ships a file by that name,
109
+ * which is the registry's to answer, not the manifest's. Anything outside
110
+ * `.standards/` is never retired: integration files are generated, not shipped
111
+ * under a standard's name, so a missing CLAUDE.md is exactly what it looks like.
112
+ *
113
+ * @param {string} relativePath - Path as recorded in fileHashes
114
+ * @param {Object} manifest - Project manifest (unused today; kept so callers
115
+ * need not know whether the answer depends on project state)
116
+ * @returns {'missing'|'retired'}
117
+ */
118
+ export function classifyMissingFile(relativePath, manifest) { // eslint-disable-line no-unused-vars
119
+ const normalized = String(relativePath).replace(/\\/g, '/');
120
+ if (!normalized.startsWith('.standards/')) return 'missing';
121
+ const fileName = normalized.split('/').pop();
122
+ return isShippedFilename(fileName) ? 'missing' : 'retired';
123
+ }
124
+
78
125
  /**
79
126
  * Perform integrity check for standards and integration files
80
127
  * @returns {Object} File status object
@@ -84,12 +131,29 @@ function performFileIntegrityCheck(projectPath, manifest, msg) {
84
131
  unchanged: [],
85
132
  modified: [],
86
133
  missing: [],
134
+ // Tracked, gone, and gone on purpose: UDS stopped shipping it. Counted
135
+ // apart from `missing` so the summary line stops calling a completed
136
+ // migration a fault.
137
+ retired: [],
87
138
  noHash: []
88
139
  };
89
140
 
90
141
  if (hasFileHashes(manifest)) {
91
142
  // Hash-based integrity check
92
143
  for (const [relativePath, hashInfo] of Object.entries(manifest.fileHashes)) {
144
+ // XSPEC-418 R6 gap 2: a manifest written before this fix can still
145
+ // carry a whole-file entry for a path that is ALSO tracked by its UDS
146
+ // block (integrationBlockHashes) — the pruning added for R6 only runs
147
+ // on a write (uds update / check --restore / --migrate), so a project
148
+ // that has not run one of those yet stayed red on `uds check --ci`
149
+ // forever, for content outside the block that the block check itself
150
+ // says is fine. This is a READ-time skip — it does not touch the
151
+ // manifest, so a plain `uds check` alone cannot fix the underlying
152
+ // stale entry; the write paths still do that (see
153
+ // pruneIntegrationFileHashes in hasher.js).
154
+ if (manifest.integrationBlockHashes && relativePath in manifest.integrationBlockHashes) {
155
+ continue;
156
+ }
93
157
  const fullPath = join(projectPath, relativePath);
94
158
  const status = compareFileHash(fullPath, hashInfo);
95
159
 
@@ -101,7 +165,7 @@ function performFileIntegrityCheck(projectPath, manifest, msg) {
101
165
  fileStatus.modified.push(relativePath);
102
166
  break;
103
167
  case 'missing':
104
- fileStatus.missing.push(relativePath);
168
+ fileStatus[classifyMissingFile(relativePath, manifest)].push(relativePath);
105
169
  break;
106
170
  }
107
171
  }
@@ -354,12 +418,38 @@ export async function checkCommand(options = {}) {
354
418
  // Check Commands integrity if commandHashes exist
355
419
  checkCommandsIntegrity(manifest, projectPath, msg);
356
420
 
421
+ // XSPEC adopter-report Q3: neither of the two checks above (content-hash
422
+ // integrity) says anything about an installed Skills/Commands version
423
+ // being behind the latest UDS release — that was only ever computed by
424
+ // `uds update --plan --skills`/`--commands`. A plain `uds check` gave no
425
+ // signal at all that `uds update --skills` had anything to do.
426
+ checkSkillsCommandsVersionStaleness(manifest, projectPath, msg);
427
+
357
428
  // Check Integration blocks integrity if integrationBlockHashes exist
358
- checkIntegrationBlocksIntegrity(manifest, projectPath, msg);
429
+ // XSPEC-418 R1: the return value used to be discarded, so a removed/modified
430
+ // UDS block never affected the final verdict below — `uds check --ci` printed
431
+ // "compliant" and exited 0 with a visible ✗ still on screen.
432
+ const integrationBlockStatus = checkIntegrationBlocksIntegrity(manifest, projectPath, msg);
359
433
 
360
434
  // Handle --restore option
361
435
  if (options.restore) {
362
- await restoreFiles(projectPath, manifest, [...fileStatus.modified, ...fileStatus.missing]);
436
+ // XSPEC-418 R6 gap 1: integration files no longer live in `fileHashes`
437
+ // at all (that was the point of R6), so `fileStatus` — built entirely
438
+ // from `fileHashes` — never contains them any more. Without this,
439
+ // `--restore` silently did nothing for a damaged UDS block: it
440
+ // regenerated zero files and printed "Restored 0 file(s)" while the
441
+ // block-modified/markers-removed content sat untouched.
442
+ // `restoreSingleFile`'s integration-file branch (`manifest.integrationConfigs`)
443
+ // does not depend on fileHashes, so feeding these paths in is enough —
444
+ // it already regenerates just the block and leaves everything else alone.
445
+ const integrationFilesToRestore = [
446
+ ...integrationBlockStatus.modified,
447
+ ...integrationBlockStatus.noMarkers,
448
+ ...integrationBlockStatus.missing
449
+ ];
450
+ await restoreFiles(projectPath, manifest, [
451
+ ...new Set([...fileStatus.modified, ...fileStatus.missing, ...integrationFilesToRestore])
452
+ ]);
363
453
  return;
364
454
  }
365
455
 
@@ -423,8 +513,15 @@ export async function checkCommand(options = {}) {
423
513
  displayWorkflowStatus(projectPath);
424
514
 
425
515
  // Final status
516
+ // XSPEC-418 R1: integration block problems (UDS markers removed, block
517
+ // modified, or the tracked file missing) now feed the verdict — they used to
518
+ // be checked and printed above, then silently dropped here.
426
519
  const allGood = fileStatus.missing.length === 0 &&
427
- fileStatus.modified.length === 0;
520
+ fileStatus.modified.length === 0 &&
521
+ integrationBlockStatus.modified.length === 0 &&
522
+ integrationBlockStatus.missing.length === 0 &&
523
+ integrationBlockStatus.noMarkers.length === 0 &&
524
+ integrationBlockStatus.ambiguous.length === 0;
428
525
  if (allGood) {
429
526
  console.log(chalk.green(msg.projectCompliant));
430
527
  } else {
@@ -537,6 +634,11 @@ async function interactiveMode(projectPath, manifest, fileStatus, msg) {
537
634
  }
538
635
 
539
636
  if (manifestUpdated) {
637
+ // XSPEC-418 R6: interactive mode still has a "keep current content" branch
638
+ // that calls the whole-file updateFileHash(); if issue.file happens to be
639
+ // an integration file left in fileHashes by an older CLI, this is the
640
+ // write point that stops it from surviving another round.
641
+ pruneIntegrationFileHashes(manifest);
540
642
  writeManifest(manifest, projectPath);
541
643
  console.log(chalk.green(msg.manifestUpdated));
542
644
  console.log();
@@ -654,6 +756,9 @@ async function restoreFiles(projectPath, manifest, files) {
654
756
  }
655
757
 
656
758
  // Update manifest
759
+ // XSPEC-418 R6: catches any stale whole-file entry for an integration file
760
+ // this particular restore run didn't touch, not just the ones it did.
761
+ pruneIntegrationFileHashes(manifest);
657
762
  writeManifest(manifest, projectPath);
658
763
  console.log(chalk.gray(` ${msg.manifestUpdatedShort}`));
659
764
  console.log();
@@ -681,7 +786,13 @@ export async function restoreSingleFile(projectPath, manifest, relativePath, msg
681
786
  installedStandards: genConfig.installedStandards || manifest.standards || []
682
787
  }, projectPath);
683
788
  if (result.success) {
684
- updateFileHash(projectPath, manifest, relativePath);
789
+ // XSPEC-418 R6: this rewrites the UDS block, so the BLOCK hash is what
790
+ // must be refreshed — not a whole-file hash. Restoring used to call
791
+ // updateFileHash() here, which recorded the whole file (block +
792
+ // whatever the adopter wrote outside it) while leaving the stale block
793
+ // hash untouched; the next `uds check` then reported "UDS block
794
+ // modified" for a block that had just been correctly restored.
795
+ updateIntegrationBlockHash(manifest, relativePath, result.blockHashInfo);
685
796
  console.log(chalk.green(` ✓ ${relativePath}: ${msg.restored}`));
686
797
  return true;
687
798
  }
@@ -703,7 +814,32 @@ export async function restoreSingleFile(projectPath, manifest, relativePath, msg
703
814
  // Integration file - copy to root
704
815
  const result = await copyIntegration(sourcePath, relativePath, projectPath);
705
816
  if (result.success) {
706
- updateFileHash(projectPath, manifest, relativePath);
817
+ // XSPEC-418 R6: same rule as the genConfig branch above — track the
818
+ // block, not the whole file. This legacy static-copy fallback has no
819
+ // blockHashInfo returned to it, so compute one from what was just
820
+ // written; a template with no UDS markers at all (computeIntegration
821
+ // BlockHash returns null) falls back to the old whole-file behaviour,
822
+ // since there is no block to track instead.
823
+ // XSPEC adopter-report Q5: the file was just overwritten wholesale by
824
+ // copyIntegration (a full template copy, not a marker-preserving
825
+ // merge), so an ambiguous marker pair here cannot be this restore's
826
+ // own doing — fall back to whole-file hashing rather than fail a
827
+ // restore that already succeeded on disk.
828
+ let blockHashInfo;
829
+ try {
830
+ blockHashInfo = computeIntegrationBlockHash(join(projectPath, relativePath));
831
+ } catch (error) {
832
+ if (error instanceof AmbiguousMarkerError) {
833
+ blockHashInfo = null;
834
+ } else {
835
+ throw error;
836
+ }
837
+ }
838
+ if (blockHashInfo) {
839
+ updateIntegrationBlockHash(manifest, relativePath, blockHashInfo);
840
+ } else {
841
+ updateFileHash(projectPath, manifest, relativePath);
842
+ }
707
843
  console.log(chalk.green(` ✓ ${relativePath}: ${msg.restored}`));
708
844
  return true;
709
845
  } else {
@@ -723,6 +859,30 @@ export async function restoreSingleFile(projectPath, manifest, relativePath, msg
723
859
  }
724
860
  }
725
861
 
862
+ /**
863
+ * Update an integration file's UDS BLOCK hash in the manifest — not its
864
+ * whole-file hash (XSPEC-418 R6). Also drops any whole-file `fileHashes`
865
+ * entry for the same path, so restoring an integration file can never leave
866
+ * both records behind at once.
867
+ *
868
+ * @param {Object} manifest - Manifest object (mutated in place)
869
+ * @param {string} relativePath - Path as UDS reports it
870
+ * @param {Object|null|undefined} blockHashInfo - `{ blockHash, blockSize, fullHash, fullSize }`,
871
+ * e.g. from `writeIntegrationFile`'s result or `computeIntegrationBlockHash`
872
+ * @returns {boolean} Whether a block hash was recorded
873
+ */
874
+ export function updateIntegrationBlockHash(manifest, relativePath, blockHashInfo) {
875
+ if (!blockHashInfo) return false;
876
+ const normalizedPath = relativePath.replace(/\\/g, '/');
877
+ if (!manifest.integrationBlockHashes) manifest.integrationBlockHashes = {};
878
+ manifest.integrationBlockHashes[normalizedPath] = {
879
+ ...blockHashInfo,
880
+ installedAt: new Date().toISOString()
881
+ };
882
+ if (manifest.fileHashes) delete manifest.fileHashes[normalizedPath];
883
+ return true;
884
+ }
885
+
726
886
  /**
727
887
  * Update file hash in manifest
728
888
  */
@@ -871,21 +1031,54 @@ async function migrateToHashBasedTracking(projectPath, manifest) {
871
1031
  }
872
1032
  }
873
1033
 
874
- // Process integrations
1034
+ // Process integrations — tracked by their UDS BLOCK hash, not a whole-file
1035
+ // hash (XSPEC-418 R6). This loop used to write the whole file into the same
1036
+ // `fileHashes` map as standards/extensions above, which is the same defect
1037
+ // as every other write site R6 fixes, just reached by `--migrate` instead
1038
+ // of a normal update: any content the adopter had outside the block then
1039
+ // read as "modified" by standards-file integrity, contradicting the block
1040
+ // check in the same `uds check` run.
1041
+ const integrationBlockHashes = { ...(manifest.integrationBlockHashes || {}) };
875
1042
  for (const intEntry of manifest.integrations) {
876
1043
  const int = resolveIntegrationFile(intEntry) || intEntry;
877
1044
  const fullPath = join(projectPath, int);
878
1045
 
879
- const hashInfo = computeFileHash(fullPath);
880
- if (hashInfo) {
881
- fileHashes[int] = { ...hashInfo, installedAt: now };
1046
+ // XSPEC adopter-report Q5: this is a read-only manifest migration over
1047
+ // every tracked integration file at once — one file with an ambiguous
1048
+ // marker pair must not abort migrating the rest. It is simply left
1049
+ // without a block hash, same as a file with no markers at all; `uds
1050
+ // check`'s own block-integrity pass (below) is what surfaces the
1051
+ // ambiguity to the user, with line numbers.
1052
+ let blockHashInfo;
1053
+ try {
1054
+ blockHashInfo = computeIntegrationBlockHash(fullPath);
1055
+ } catch (error) {
1056
+ if (error instanceof AmbiguousMarkerError) {
1057
+ blockHashInfo = null;
1058
+ } else {
1059
+ throw error;
1060
+ }
1061
+ }
1062
+ if (blockHashInfo) {
1063
+ integrationBlockHashes[int] = { ...blockHashInfo, installedAt: now };
882
1064
  count++;
1065
+ } else {
1066
+ // No UDS markers found (plaintext template with none, or a file the
1067
+ // adopter fully rewrote) — fall back to whole-file tracking, same as
1068
+ // before this fix, rather than silently tracking nothing.
1069
+ const hashInfo = computeFileHash(fullPath);
1070
+ if (hashInfo) {
1071
+ fileHashes[int] = { ...hashInfo, installedAt: now };
1072
+ count++;
1073
+ }
883
1074
  }
884
1075
  }
885
1076
 
886
1077
  // Update manifest
887
1078
  manifest.fileHashes = fileHashes;
888
- manifest.version = '3.1.0';
1079
+ manifest.integrationBlockHashes = integrationBlockHashes;
1080
+ bumpManifestVersion(manifest);
1081
+ pruneIntegrationFileHashes(manifest);
889
1082
  writeManifest(manifest, projectPath);
890
1083
 
891
1084
  console.log(chalk.green(msg.migratedCount.replace('{count}', count)));
@@ -1151,19 +1344,39 @@ function checkIntegrationFiles(manifest, projectPath, msg) {
1151
1344
  // After migrateStandardsPathsToIds(), manifest.standards contains IDs, so a plain
1152
1345
  // content.includes(id) check would fail for these mismatched entries.
1153
1346
  const allRegistryStds = getAllStandards();
1154
- const idToAiFilename = new Map(
1155
- allRegistryStds
1156
- .filter(s => s.source?.ai)
1157
- .map(s => [s.id, basename(s.source.ai)])
1347
+ // 🔴 `source` is a string for some entries and an object for others. Reading
1348
+ // `s.source.ai` on a string yields undefined, so `zh-tw-locale`
1349
+ // (source: 'extensions/locales/zh-tw.md') fell out of this map and the file it
1350
+ // installs — `.standards/zh-tw.md` — was never recognised: "67/68 項標準已參考,
1351
+ // 缺少: zh-tw-locale" on a project whose index lists it (reported 2026-09-16).
1352
+ const idToFilenames = new Map(
1353
+ allRegistryStds.map(s => {
1354
+ const sources = typeof s.source === 'string'
1355
+ ? [s.source]
1356
+ : [s.source?.ai, s.source?.human].filter(Boolean);
1357
+ return [s.id, sources.map(src => basename(src))];
1358
+ })
1158
1359
  );
1159
1360
 
1160
1361
  let hasIssues = false;
1161
1362
  let checkedCount = 0;
1162
1363
 
1364
+ // codex and opencode both write AGENTS.md, so iterating tools printed the same
1365
+ // file twice with identical verdicts (reported 2026-09-16). One file, one
1366
+ // verdict — the tools that share it are named on the line instead.
1367
+ const toolsByFile = new Map();
1163
1368
  for (const tool of manifest.aiTools) {
1164
- const toolFile = getToolFilePath(tool);
1165
- if (!toolFile) continue;
1369
+ // XSPEC-418 R3: check must inspect the actual target file (e.g. CLAUDE.local.md),
1370
+ // not the tool's hardcoded default.
1371
+ const file = resolveIntegrationTargetFile(tool, manifest);
1372
+ if (!file) continue;
1373
+ if (!toolsByFile.has(file)) toolsByFile.set(file, []);
1374
+ toolsByFile.get(file).push(tool);
1375
+ }
1166
1376
 
1377
+ for (const [toolFile, sharingTools] of toolsByFile) {
1378
+ const tool = sharingTools[0];
1379
+ const sharedSuffix = sharingTools.length > 1 ? ` (${sharingTools.join(', ')})` : '';
1167
1380
  const fullPath = join(projectPath, toolFile);
1168
1381
 
1169
1382
  // Check if file exists
@@ -1186,7 +1399,20 @@ function checkIntegrationFiles(manifest, projectPath, msg) {
1186
1399
 
1187
1400
  // Check for standards index marker
1188
1401
  const format = getToolFormat(tool);
1189
- const { content: markedContent } = extractMarkedContent(content, format);
1402
+ let markedContent;
1403
+ try {
1404
+ ({ content: markedContent } = extractMarkedContent(content, format));
1405
+ } catch (error) {
1406
+ if (error instanceof AmbiguousMarkerError) {
1407
+ // XSPEC adopter-report Q5: report explicitly with line numbers
1408
+ // instead of guessing which marker pair is real, or silently
1409
+ // treating the file as if it had no UDS block at all.
1410
+ console.log(chalk.red(` ✗ ${toolFile}: ${error.message}`));
1411
+ hasIssues = true;
1412
+ continue;
1413
+ }
1414
+ throw error;
1415
+ }
1190
1416
  const hasStandardsIndex = markedContent.length > 0 ||
1191
1417
  content.includes('Standards Index') ||
1192
1418
  content.includes('Standards Compliance');
@@ -1201,9 +1427,9 @@ function checkIntegrationFiles(manifest, projectPath, msg) {
1201
1427
  // migration. For standards where the ID doesn't match the .ai.yaml basename
1202
1428
  // (e.g. ID "error-code-standards" → file "error-codes.ai.yaml"), we must
1203
1429
  // also check the actual filename so those aren't falsely reported as missing.
1204
- const aiFilename = idToAiFilename.get(stdFile);
1430
+ const filenames = idToFilenames.get(stdFile) || [];
1205
1431
  const isReferenced = content.includes(stdFile) ||
1206
- (aiFilename !== undefined && aiFilename !== stdFile && content.includes(aiFilename)) ||
1432
+ filenames.some(name => name !== stdFile && content.includes(name)) ||
1207
1433
  content.includes(`.standards/${stdFile}`) ||
1208
1434
  content.includes(`standards/${stdFile}`);
1209
1435
 
@@ -1224,7 +1450,7 @@ function checkIntegrationFiles(manifest, projectPath, msg) {
1224
1450
  const declaredCount = parseStandardsIndexCount(content);
1225
1451
  if (declaredCount !== null) {
1226
1452
  if (declaredCount === totalTrackable) {
1227
- console.log(chalk.green(` ✓ ${toolFile}:`));
1453
+ console.log(chalk.green(` ✓ ${toolFile}${sharedSuffix}:`));
1228
1454
  console.log(chalk.gray(` ${msg.standardsIndexPresent}`));
1229
1455
  console.log(chalk.gray(` ${msg.standardsIndexCount
1230
1456
  ? msg.standardsIndexCount.replace('{count}', declaredCount)
@@ -1404,9 +1630,12 @@ function checkReferenceSync(manifest, projectPath, msg) {
1404
1630
  }
1405
1631
 
1406
1632
  const references = parseReferences(content);
1407
- if (references.length === 0) continue;
1633
+ // Whole-file scan, separate from reference sync: the paths that matter here
1634
+ // are the ones outside the UDS block, which no generation ever revisits.
1635
+ const brokenMentions = findBrokenPathMentions(content, projectPath);
1636
+ if (references.length === 0 && brokenMentions.length === 0) continue;
1408
1637
 
1409
- results.push({ integrationPath, references });
1638
+ results.push({ integrationPath, references, brokenMentions });
1410
1639
  }
1411
1640
 
1412
1641
  // Skip entire section if no files have references
@@ -1416,14 +1645,42 @@ function checkReferenceSync(manifest, projectPath, msg) {
1416
1645
 
1417
1646
  let hasIssues = false;
1418
1647
 
1419
- for (const { integrationPath, references } of results) {
1648
+ for (const { integrationPath, references, brokenMentions } of results) {
1649
+ // Paths mentioned anywhere in the file that resolve to nothing. Reported
1650
+ // first and separately from reference sync, because these are usually
1651
+ // outside the UDS markers — left by an older `uds init`, never revisited by
1652
+ // any regeneration, and followed by an AI tool that then finds nothing.
1653
+ if (brokenMentions?.length > 0) {
1654
+ hasIssues = true;
1655
+ console.log(chalk.red(` ✗ ${integrationPath}:`));
1656
+ console.log(chalk.red(` ${msg.brokenMentions || 'mentions paths that do not exist in this project:'}`));
1657
+ for (const path of brokenMentions) {
1658
+ console.log(chalk.red(` - ${path}`));
1659
+ }
1660
+ console.log(chalk.gray(` ${msg.brokenMentionsFix || 'These are usually outside the UDS block and must be corrected by hand; UDS does not rewrite text it did not write.'}`));
1661
+ }
1662
+
1420
1663
  // Compare with manifest standards
1421
- const { orphanedRefs, missingRefs, syncedRefs } = compareStandardsWithReferences(
1664
+ const { orphanedRefs, missingRefs, syncedRefs, danglingRefs } = compareStandardsWithReferences(
1422
1665
  manifest.standards,
1423
- references
1666
+ references,
1667
+ { projectPath, options: manifest.options }
1424
1668
  );
1425
1669
 
1426
1670
  // Report results
1671
+ // 🔴 A reference to a file that is not on disk is the one failure an AI tool
1672
+ // cannot work around: it follows the link and finds nothing. Reported before
1673
+ // the manifest-level findings because it needs no interpretation.
1674
+ if (danglingRefs.length > 0) {
1675
+ hasIssues = true;
1676
+ console.log(chalk.red(` ✗ ${integrationPath}:`));
1677
+ console.log(chalk.red(` ${msg.danglingRefs || 'references files that do not exist:'}`));
1678
+ for (const ref of danglingRefs) {
1679
+ console.log(chalk.red(` - .standards/${ref}`));
1680
+ }
1681
+ console.log(chalk.gray(` ${msg.danglingRefsFix || 'Run `uds update --integrations-only` to regenerate them.'}`));
1682
+ }
1683
+
1427
1684
  if (orphanedRefs.length > 0) {
1428
1685
  hasIssues = true;
1429
1686
  console.log(chalk.yellow(` ⚠ ${integrationPath}:`));
@@ -1442,14 +1699,21 @@ function checkReferenceSync(manifest, projectPath, msg) {
1442
1699
  }
1443
1700
  }
1444
1701
 
1445
- if (orphanedRefs.length === 0 && missingRefs.length === 0) {
1702
+ if (orphanedRefs.length === 0 && missingRefs.length === 0 && danglingRefs.length === 0) {
1446
1703
  console.log(chalk.green(` ✓ ${msg.refsInSync.replace('{path}', integrationPath).replace('{count}', syncedRefs.length)}`));
1447
1704
  }
1448
1705
  }
1449
1706
 
1450
1707
  if (hasIssues) {
1451
1708
  console.log();
1452
- console.log(chalk.yellow(` ${msg.runSyncRefs}`));
1709
+ // `uds update --sync-refs` refuses to run without `integrationConfigs`, and a
1710
+ // project initialised non-interactively never had that key — so this hint
1711
+ // named a command that could not work (reproduced on a clean 6.9.0 project,
1712
+ // 2026-09-16). Name the one that regenerates from the manifest instead.
1713
+ const canSyncRefs = manifest.integrationConfigs && Object.keys(manifest.integrationConfigs).length > 0;
1714
+ console.log(chalk.yellow(` ${canSyncRefs
1715
+ ? msg.runSyncRefs
1716
+ : (msg.runIntegrationsOnly || 'Run `uds update --integrations-only` to regenerate integration files.')}`));
1453
1717
  }
1454
1718
 
1455
1719
  console.log();
@@ -1461,7 +1725,7 @@ function checkReferenceSync(manifest, projectPath, msg) {
1461
1725
  */
1462
1726
  async function checkCliVersion(bundledVersion) {
1463
1727
  const msg = t().commands.check;
1464
- const spinner = ora({ text: msg.checkingCliUpdates, spinner: 'dots' }).start();
1728
+ const spinner = createSpinner({ text: msg.checkingCliUpdates, spinner: 'dots' }).start();
1465
1729
 
1466
1730
  try {
1467
1731
  const result = await checkForUpdates(bundledVersion, {
@@ -1504,6 +1768,59 @@ async function checkCliVersion(bundledVersion) {
1504
1768
  // Enhanced Integrity Check Functions (v3.3.0+)
1505
1769
  // ============================================================
1506
1770
 
1771
+ /**
1772
+ * Warn when an installed Skills or Commands version is behind the latest
1773
+ * UDS release. This is deliberately modeled on the existing top-level
1774
+ * "Version: X → Y ⚠" row (see the `hasUpdate` check in the summary
1775
+ * dashboard below) rather than on `checkIntegrationBlocksIntegrity`
1776
+ * (XSPEC-418 R1): a version simply being behind the latest release is
1777
+ * ambient, expected state until the adopter chooses to update — not a
1778
+ * compliance defect the way a modified/missing/ambiguous UDS block is — so
1779
+ * it is reported but, unlike a block problem, does not fail `--ci`.
1780
+ *
1781
+ * Skills staleness is read per-installation from what is actually on disk
1782
+ * (`getInstalledSkillsInfoForAgent`), the same source `uds update --plan
1783
+ * --skills` uses, because different agents/levels can be out of sync
1784
+ * independently. Commands have no per-installation version on disk (only a
1785
+ * file count), so Commands staleness is read from the one version the
1786
+ * manifest itself records (`manifest.commands.version`).
1787
+ * // implements XSPEC adopter-report Q3
1788
+ *
1789
+ * @param {Object} manifest
1790
+ * @param {string} projectPath
1791
+ * @param {Object} msg - Localized messages (unused today; kept for symmetry
1792
+ * with the other Enhanced Integrity Check functions, which all take one)
1793
+ */
1794
+ function checkSkillsCommandsVersionStaleness(manifest, projectPath, msg) { // eslint-disable-line no-unused-vars
1795
+ const repoInfo = getRepositoryInfo();
1796
+ const latestVersion = repoInfo.skills.version;
1797
+ const stale = [];
1798
+
1799
+ const skillsInstallations = (manifest.skills?.installations || []).filter((i) => i.level !== 'marketplace');
1800
+ for (const inst of skillsInstallations) {
1801
+ const info = getInstalledSkillsInfoForAgent(inst.agent, inst.level, projectPath);
1802
+ const current = info?.version;
1803
+ if (current && current !== latestVersion) {
1804
+ stale.push(`${getAgentDisplayName(inst.agent)} (${inst.level}): Skills v${current} → v${latestVersion}`);
1805
+ }
1806
+ }
1807
+
1808
+ if (manifest.commands?.installed && (manifest.commands?.installations || []).length > 0) {
1809
+ const current = manifest.commands.version;
1810
+ if (current && current !== latestVersion) {
1811
+ stale.push(`Commands: v${current} → v${latestVersion}`);
1812
+ }
1813
+ }
1814
+
1815
+ if (stale.length === 0) return;
1816
+
1817
+ for (const line of stale) {
1818
+ console.log(chalk.yellow(` ⚠ ${line}`));
1819
+ }
1820
+ console.log(chalk.gray(' Run `uds update --plan --skills` / `--commands` for details, then `--apply` to update.'));
1821
+ console.log();
1822
+ }
1823
+
1507
1824
  /**
1508
1825
  * Check Skills files integrity against stored hashes
1509
1826
  * @param {Object} manifest - Manifest object
@@ -1582,17 +1899,17 @@ function checkSkillsIntegrity(manifest, projectPath, msg) {
1582
1899
  * @param {Object} msg - Localized messages
1583
1900
  * @returns {Object} Status { unchanged: [], modified: [], missing: [] }
1584
1901
  */
1585
- function checkCommandsIntegrity(manifest, projectPath, msg) {
1902
+ export function checkCommandsIntegrity(manifest, projectPath, msg) {
1586
1903
  const commandHashes = manifest.commandHashes;
1587
1904
 
1588
1905
  // Skip if no command hashes tracked
1589
1906
  if (!commandHashes || Object.keys(commandHashes).length === 0) {
1590
- return { unchanged: [], modified: [], missing: [], tracked: false };
1907
+ return { unchanged: [], modified: [], missing: [], untracked: [], tracked: false };
1591
1908
  }
1592
1909
 
1593
1910
  console.log(chalk.cyan(msg.commandsIntegrityCheck || 'Commands File Integrity'));
1594
1911
 
1595
- const status = { unchanged: [], modified: [], missing: [], tracked: true };
1912
+ const status = { unchanged: [], modified: [], missing: [], untracked: [], tracked: true };
1596
1913
 
1597
1914
  for (const [hashKey, hashInfo] of Object.entries(commandHashes)) {
1598
1915
  // Parse key format: agent/filename.md
@@ -1632,8 +1949,42 @@ function checkCommandsIntegrity(manifest, projectPath, msg) {
1632
1949
  }
1633
1950
  }
1634
1951
 
1952
+ // 🔴 The loop above measures only what the manifest happens to track. When a
1953
+ // partial apply dropped 48 of 51 entries (6.9.0, fixed in plan-executor.js),
1954
+ // this section printed "All command files intact (3 files)" while the adoption
1955
+ // summary printed "Commands: 51 installed" from a directory listing — two
1956
+ // numbers, two sources, never compared, so the gap read as a pass.
1957
+ const trackedKeys = new Set(Object.keys(commandHashes));
1958
+ const agentsToScan = new Set(Object.keys(commandHashes).map(k => k.split('/')[0]));
1959
+ for (const entry of manifest.commands?.installations || []) {
1960
+ const agentName = typeof entry === 'string' ? entry : entry?.agent;
1961
+ if (agentName) agentsToScan.add(agentName);
1962
+ }
1963
+ for (const agentName of agentsToScan) {
1964
+ // Reuse the installer's own listing so this file filter cannot drift from it.
1965
+ const info = getInstalledCommandsForAgent(agentName, 'project', projectPath);
1966
+ if (!info?.installed) continue;
1967
+ const ext = agentName === 'gemini-cli' ? '.toml' : '.md';
1968
+ for (const name of info.commands || []) {
1969
+ const key = `${agentName}/${name}${ext}`;
1970
+ if (!trackedKeys.has(key)) status.untracked.push(key);
1971
+ }
1972
+ }
1973
+ if (status.untracked.length > 0) {
1974
+ console.log(chalk.yellow(` ⚠ ${(msg.commandsUntracked || '{count} installed command file(s) are covered by no hash — integrity is not checked for them')
1975
+ .replace('{count}', status.untracked.length)}`));
1976
+ for (const key of status.untracked.slice(0, 5)) {
1977
+ console.log(chalk.yellow(` - ${key}`));
1978
+ }
1979
+ if (status.untracked.length > 5) {
1980
+ console.log(chalk.yellow(` … +${status.untracked.length - 5}`));
1981
+ }
1982
+ console.log(chalk.gray(` ${msg.commandsUntrackedFix || 'Run `uds update --commands` to record them again.'}`));
1983
+ }
1984
+
1985
+
1635
1986
  // Summary
1636
- if (status.modified.length === 0 && status.missing.length === 0) {
1987
+ if (status.modified.length === 0 && status.missing.length === 0 && status.untracked.length === 0) {
1637
1988
  console.log(chalk.green(` ✓ ${msg.allCommandsIntact || 'All command files intact'} (${status.unchanged.length} files)`));
1638
1989
  } else {
1639
1990
  console.log(chalk.gray(` ${(msg.commandsIntegritySummary || '{unchanged} unchanged, {modified} modified, {missing} missing')
@@ -1652,19 +2003,19 @@ function checkCommandsIntegrity(manifest, projectPath, msg) {
1652
2003
  * @param {Object} manifest - Manifest object
1653
2004
  * @param {string} projectPath - Project root path
1654
2005
  * @param {Object} msg - Localized messages
1655
- * @returns {Object} Status { unchanged: [], modified: [], missing: [], noMarkers: [] }
2006
+ * @returns {Object} Status { unchanged: [], modified: [], missing: [], noMarkers: [], ambiguous: [] }
1656
2007
  */
1657
2008
  function checkIntegrationBlocksIntegrity(manifest, projectPath, msg) {
1658
2009
  const blockHashes = manifest.integrationBlockHashes;
1659
2010
 
1660
2011
  // Skip if no block hashes tracked
1661
2012
  if (!blockHashes || Object.keys(blockHashes).length === 0) {
1662
- return { unchanged: [], modified: [], missing: [], noMarkers: [], tracked: false };
2013
+ return { unchanged: [], modified: [], missing: [], noMarkers: [], ambiguous: [], tracked: false };
1663
2014
  }
1664
2015
 
1665
2016
  console.log(chalk.cyan(msg.integrationBlocksCheck || 'Integration UDS Block Integrity'));
1666
2017
 
1667
- const status = { unchanged: [], modified: [], missing: [], noMarkers: [], tracked: true };
2018
+ const status = { unchanged: [], modified: [], missing: [], noMarkers: [], ambiguous: [], tracked: true };
1668
2019
 
1669
2020
  for (const [filePath, hashInfo] of Object.entries(blockHashes)) {
1670
2021
  const fullPath = join(projectPath, filePath);
@@ -1676,7 +2027,21 @@ function checkIntegrationBlocksIntegrity(manifest, projectPath, msg) {
1676
2027
  }
1677
2028
 
1678
2029
  // Compare block hash
1679
- const blockStatus = compareIntegrationBlockHash(fullPath, hashInfo);
2030
+ // XSPEC adopter-report Q5: a file with two real START or END marker
2031
+ // lines is not "modified" and not "no markers" — it is ambiguous, and
2032
+ // the whole point of this fix is to report that explicitly (with line
2033
+ // numbers) instead of silently picking one and guessing.
2034
+ let blockStatus;
2035
+ try {
2036
+ blockStatus = compareIntegrationBlockHash(fullPath, hashInfo);
2037
+ } catch (error) {
2038
+ if (error instanceof AmbiguousMarkerError) {
2039
+ status.ambiguous.push(filePath);
2040
+ console.log(chalk.red(` ✗ ${filePath}: ${error.message}`));
2041
+ continue;
2042
+ }
2043
+ throw error;
2044
+ }
1680
2045
 
1681
2046
  switch (blockStatus) {
1682
2047
  case 'unchanged':
@@ -1698,17 +2063,21 @@ function checkIntegrationBlocksIntegrity(manifest, projectPath, msg) {
1698
2063
  }
1699
2064
 
1700
2065
  // Summary
1701
- if (status.modified.length === 0 && status.missing.length === 0 && status.noMarkers.length === 0) {
2066
+ if (status.modified.length === 0 && status.missing.length === 0 && status.noMarkers.length === 0 && status.ambiguous.length === 0) {
1702
2067
  console.log(chalk.green(` ✓ ${msg.allBlocksIntact || 'All UDS blocks intact'} (${status.unchanged.length} files)`));
1703
2068
  console.log(chalk.gray(` ${msg.userContentPreserved || 'User customizations outside UDS blocks are preserved'}`));
1704
2069
  } else {
1705
2070
  console.log(chalk.gray(` ${(msg.blocksIntegritySummary || '{unchanged} intact, {modified} modified, {missing} missing')
1706
2071
  .replace('{unchanged}', status.unchanged.length)
1707
2072
  .replace('{modified}', status.modified.length)
1708
- .replace('{missing}', status.missing.length + status.noMarkers.length)}`));
2073
+ .replace('{missing}', status.missing.length + status.noMarkers.length + status.ambiguous.length)}`));
1709
2074
 
1710
2075
  if (status.modified.length > 0 || status.noMarkers.length > 0) {
1711
- console.log(chalk.yellow(` ${msg.runUpdateIntegrations || 'Run "uds update --integrations-only" to restore UDS content'}`));
2076
+ // XSPEC-418 R6 gap 1: `uds check --restore` now regenerates just the
2077
+ // UDS block for these files too (it used to do nothing for them — see
2078
+ // the --restore handler above), so it is named alongside `uds update
2079
+ // --integrations-only` rather than as a second-choice remedy.
2080
+ console.log(chalk.yellow(` ${msg.runUpdateIntegrations || 'Run "uds check --restore" or "uds update --integrations-only" to restore UDS content'}`));
1712
2081
  }
1713
2082
  }
1714
2083