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.
- package/bin/uds.js +2 -0
- package/bundled/core/agent-communication-protocol.md +8 -0
- package/bundled/core/branch-completion.md +8 -0
- package/bundled/core/change-batching-standards.md +8 -0
- package/bundled/core/execution-history.md +8 -0
- package/bundled/core/pipeline-integration-standards.md +8 -0
- package/bundled/core/workflow-enforcement.md +8 -0
- package/bundled/core/workflow-state-protocol.md +8 -0
- package/bundled/locales/zh-CN/CHANGELOG.md +49 -3
- package/bundled/locales/zh-CN/README.md +1 -1
- package/bundled/locales/zh-CN/SECURITY.md +1 -1
- package/bundled/locales/zh-CN/core/agent-communication-protocol.md +7 -0
- package/bundled/locales/zh-CN/core/branch-completion.md +7 -0
- package/bundled/locales/zh-CN/core/change-batching-standards.md +7 -0
- package/bundled/locales/zh-CN/core/execution-history.md +7 -0
- package/bundled/locales/zh-CN/core/pipeline-integration-standards.md +7 -0
- package/bundled/locales/zh-CN/core/workflow-enforcement.md +7 -0
- package/bundled/locales/zh-CN/core/workflow-state-protocol.md +7 -0
- package/bundled/locales/zh-CN/docs/CHEATSHEET.md +1 -1
- package/bundled/locales/zh-CN/docs/CLI-INIT-OPTIONS.md +52 -5
- package/bundled/locales/zh-CN/docs/FEATURE-REFERENCE.md +3 -1
- package/bundled/locales/zh-CN/docs/MIGRATION-v6.md +8 -4
- package/bundled/locales/zh-TW/CHANGELOG.md +50 -3
- package/bundled/locales/zh-TW/README.md +1 -1
- package/bundled/locales/zh-TW/SECURITY.md +1 -1
- package/bundled/locales/zh-TW/core/agent-communication-protocol.md +7 -0
- package/bundled/locales/zh-TW/core/branch-completion.md +7 -0
- package/bundled/locales/zh-TW/core/change-batching-standards.md +7 -0
- package/bundled/locales/zh-TW/core/execution-history.md +7 -0
- package/bundled/locales/zh-TW/core/pipeline-integration-standards.md +7 -0
- package/bundled/locales/zh-TW/core/workflow-enforcement.md +7 -0
- package/bundled/locales/zh-TW/core/workflow-state-protocol.md +7 -0
- package/bundled/locales/zh-TW/docs/CHEATSHEET.md +1 -1
- package/bundled/locales/zh-TW/docs/CLI-INIT-OPTIONS.md +52 -5
- package/bundled/locales/zh-TW/docs/FEATURE-REFERENCE.md +3 -1
- package/bundled/locales/zh-TW/docs/MIGRATION-v6.md +8 -4
- package/bundled/locales/zh-TW/integrations/claude-code/README.md +14 -5
- package/package.json +7 -6
- package/src/commands/check.js +413 -44
- package/src/commands/config.js +34 -28
- package/src/commands/init.js +37 -7
- package/src/commands/spec.js +2 -2
- package/src/commands/update.js +560 -74
- package/src/core/manifest.js +39 -1
- package/src/flows/init-flow.js +9 -1
- package/src/generators/layered-claudemd.js +13 -4
- package/src/i18n/messages.js +39 -3
- package/src/installers/integration-installer.js +17 -10
- package/src/installers/manifest-installer.js +4 -0
- package/src/installers/skills-installer.js +4 -4
- package/src/installers/standards-installer.js +3 -3
- package/src/prompts/init.js +33 -4
- package/src/reconciler/actual-state-scanner.js +29 -2
- package/src/reconciler/desired-state-calculator.js +51 -2
- package/src/reconciler/diff-engine.js +76 -5
- package/src/reconciler/plan-executor.js +48 -27
- package/src/utils/hasher.js +61 -5
- package/src/utils/integration-generator.js +431 -92
- package/src/utils/marker-locator.js +140 -0
- package/src/utils/reference-sync.js +156 -8
- package/src/utils/registry.js +57 -0
- package/src/utils/spinner.js +31 -0
- package/standards-registry.json +8 -8
package/src/commands/check.js
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { select } from '@inquirer/prompts';
|
|
3
|
-
import
|
|
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,
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
880
|
-
|
|
881
|
-
|
|
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.
|
|
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
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
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
|
-
|
|
1165
|
-
|
|
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
|
-
|
|
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
|
|
1430
|
+
const filenames = idToFilenames.get(stdFile) || [];
|
|
1205
1431
|
const isReferenced = content.includes(stdFile) ||
|
|
1206
|
-
(
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|