universal-dev-standards 6.10.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/locales/zh-CN/CHANGELOG.md +23 -3
- package/bundled/locales/zh-CN/README.md +1 -1
- package/bundled/locales/zh-CN/SECURITY.md +1 -1
- 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-TW/CHANGELOG.md +23 -3
- package/bundled/locales/zh-TW/README.md +1 -1
- package/bundled/locales/zh-TW/SECURITY.md +1 -1
- 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/integrations/claude-code/README.md +14 -5
- package/package.json +1 -1
- package/src/commands/check.js +253 -21
- package/src/commands/config.js +15 -9
- package/src/commands/init.js +24 -3
- package/src/commands/update.js +311 -47
- 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 +3 -3
- package/src/installers/integration-installer.js +13 -6
- package/src/installers/manifest-installer.js +4 -0
- package/src/reconciler/actual-state-scanner.js +29 -2
- package/src/reconciler/desired-state-calculator.js +51 -2
- package/src/reconciler/diff-engine.js +19 -3
- package/src/reconciler/plan-executor.js +17 -16
- package/src/utils/hasher.js +61 -5
- package/src/utils/integration-generator.js +239 -28
- package/src/utils/marker-locator.js +140 -0
- package/src/utils/reference-sync.js +53 -1
- package/standards-registry.json +7 -7
package/src/i18n/messages.js
CHANGED
|
@@ -840,7 +840,7 @@ export const messages = {
|
|
|
840
840
|
refSyncStatus: 'Reference Sync Status:',
|
|
841
841
|
noRefsFound: 'No standard references found',
|
|
842
842
|
refsNotInManifest: 'References not in manifest:',
|
|
843
|
-
standardsNotReferenced: 'Standards
|
|
843
|
+
standardsNotReferenced: 'Standards with no "Reference:"/"參考:" line in this file (optional):',
|
|
844
844
|
refsInSync: '{path}: references in sync ({count} refs)',
|
|
845
845
|
noIntegrationRefs: 'No integration files with standard references found.',
|
|
846
846
|
runSyncRefs: 'Run `uds update --sync-refs` to fix reference issues.',
|
|
@@ -2080,7 +2080,7 @@ export const messages = {
|
|
|
2080
2080
|
refSyncStatus: '參考同步狀態:',
|
|
2081
2081
|
noRefsFound: '找不到標準參考',
|
|
2082
2082
|
refsNotInManifest: '未在 manifest 中的參考:',
|
|
2083
|
-
standardsNotReferenced: '
|
|
2083
|
+
standardsNotReferenced: '本檔案中沒有「Reference:」或「參考:」行提及的標準(選用):',
|
|
2084
2084
|
refsInSync: '{path}:參考已同步({count} 個參考)',
|
|
2085
2085
|
noIntegrationRefs: '找不到有標準參考的整合檔案。',
|
|
2086
2086
|
runSyncRefs: '執行 `uds update --sync-refs` 修復參考問題。',
|
|
@@ -3332,7 +3332,7 @@ export const messages = {
|
|
|
3332
3332
|
refSyncStatus: '参考同步状态:',
|
|
3333
3333
|
noRefsFound: '未找到标准引用',
|
|
3334
3334
|
refsNotInManifest: 'manifest 中无引用:',
|
|
3335
|
-
standardsNotReferenced: '
|
|
3335
|
+
standardsNotReferenced: '本文件中没有"Reference:"或"參考:"行提及的标准(可选):',
|
|
3336
3336
|
refsInSync: '{path}:引用已同步({count} 个引用)',
|
|
3337
3337
|
noIntegrationRefs: '未找到包含标准引用的集成文件。',
|
|
3338
3338
|
runSyncRefs: '执行 `uds update --sync-refs` 修复引用问题。',
|
|
@@ -3,7 +3,7 @@ import { copyIntegration } from '../utils/copier.js';
|
|
|
3
3
|
import {
|
|
4
4
|
writeIntegrationFile,
|
|
5
5
|
integrationFileExists,
|
|
6
|
-
|
|
6
|
+
resolveIntegrationTargetFile,
|
|
7
7
|
writeAgentsMdSummary,
|
|
8
8
|
resolveContentModeForTool
|
|
9
9
|
} from '../utils/integration-generator.js';
|
|
@@ -106,7 +106,9 @@ export async function installIntegrations(config, projectPath) {
|
|
|
106
106
|
|
|
107
107
|
for (const tool of integrations) {
|
|
108
108
|
// Check if this file was already generated (for AGENTS.md sharing)
|
|
109
|
-
|
|
109
|
+
// XSPEC-418 R2: honors config.integrationTargets (set from --claude-target at
|
|
110
|
+
// init time) rather than always assuming each tool's default file.
|
|
111
|
+
const targetFile = resolveIntegrationTargetFile(tool, { integrationTargets: config.integrationTargets });
|
|
110
112
|
if (generatedFiles.has(targetFile)) {
|
|
111
113
|
// Skip - file already generated by another tool (e.g., codex generated AGENTS.md, skip opencode)
|
|
112
114
|
continue;
|
|
@@ -129,7 +131,9 @@ export async function installIntegrations(config, projectPath) {
|
|
|
129
131
|
contentMode: resolved.contentMode,
|
|
130
132
|
level: resolved.level ?? level,
|
|
131
133
|
// Pass output_language for dynamic commit standards generation
|
|
132
|
-
outputLanguage
|
|
134
|
+
outputLanguage,
|
|
135
|
+
// XSPEC-418 R2: forwarded so writeIntegrationFile resolves the actual target.
|
|
136
|
+
integrationTargets: config.integrationTargets
|
|
133
137
|
};
|
|
134
138
|
|
|
135
139
|
// Use dynamic generator
|
|
@@ -218,9 +222,10 @@ export async function generateClaudeMd(config, projectPath) {
|
|
|
218
222
|
|
|
219
223
|
const msg = t().commands.init;
|
|
220
224
|
|
|
221
|
-
// Check if Claude Code is selected and
|
|
225
|
+
// Check if Claude Code is selected and its target file doesn't already exist
|
|
226
|
+
// XSPEC-418 R2: the target may be CLAUDE.local.md, not CLAUDE.md.
|
|
222
227
|
const claudeCodeSelected = aiTools.includes('claude-code');
|
|
223
|
-
if (!claudeCodeSelected || integrationFileExists('claude-code', projectPath)) {
|
|
228
|
+
if (!claudeCodeSelected || integrationFileExists('claude-code', projectPath, { integrationTargets: config.integrationTargets })) {
|
|
224
229
|
return { path: null, error: null };
|
|
225
230
|
}
|
|
226
231
|
|
|
@@ -245,7 +250,9 @@ export async function generateClaudeMd(config, projectPath) {
|
|
|
245
250
|
contentMode: claudeResolved.contentMode,
|
|
246
251
|
level: claudeResolved.level ?? level,
|
|
247
252
|
// Pass output_language for dynamic commit standards generation
|
|
248
|
-
outputLanguage
|
|
253
|
+
outputLanguage,
|
|
254
|
+
// XSPEC-418 R2
|
|
255
|
+
integrationTargets: config.integrationTargets
|
|
249
256
|
};
|
|
250
257
|
|
|
251
258
|
const result = writeIntegrationFile('claude-code', claudeConfig, projectPath);
|
|
@@ -31,6 +31,10 @@ export function writeFinalManifest(config, results, projectPath) {
|
|
|
31
31
|
extensions: results.extensions || [],
|
|
32
32
|
integrations: results.integrations || [],
|
|
33
33
|
integrationConfigs: results.manifestIntegrationConfigs || {},
|
|
34
|
+
// XSPEC-418 R2: only ever present when --claude-target local was chosen.
|
|
35
|
+
// Omitted entirely otherwise so a manifest with no override is byte-for-byte
|
|
36
|
+
// what it was before this feature existed (AC-5).
|
|
37
|
+
...(config.integrationTargets ? { integrationTargets: config.integrationTargets } : {}),
|
|
34
38
|
options: {
|
|
35
39
|
display_language: config.displayLanguage || 'en',
|
|
36
40
|
workflow: config.standardOptions?.workflow || null,
|
|
@@ -13,6 +13,7 @@ import { join, relative } from 'path';
|
|
|
13
13
|
import { readManifest } from '../core/manifest.js';
|
|
14
14
|
import { computeFileHash, computeIntegrationBlockHash, normalizeLineEndings } from '../utils/hasher.js';
|
|
15
15
|
import { SUPPORTED_AI_TOOLS, UDS_MARKERS } from '../core/constants.js';
|
|
16
|
+
import { locateMarkerBlock, AmbiguousMarkerError } from '../utils/marker-locator.js';
|
|
16
17
|
import { getSkillsDirForAgent, getCommandsDirForAgent, getCommandFileExtension } from '../config/ai-agent-paths.js';
|
|
17
18
|
import { getSkillsSourceEntryNames, getAvailableCommandNames } from '../utils/skills-installer.js';
|
|
18
19
|
|
|
@@ -230,7 +231,21 @@ function scanIntegrations(state, projectPath) {
|
|
|
230
231
|
const filePath = join(projectPath, toolConfig.file);
|
|
231
232
|
if (!existsSync(filePath)) continue;
|
|
232
233
|
|
|
233
|
-
|
|
234
|
+
// XSPEC adopter-report Q5: this scan walks every configured tool's file
|
|
235
|
+
// in one pass to build a full picture of the project — one file with an
|
|
236
|
+
// ambiguous marker pair must not abort the whole scan. `uds check`'s
|
|
237
|
+
// dedicated block-integrity pass is what surfaces the ambiguity to the
|
|
238
|
+
// user explicitly, with line numbers; this scan just records "unknown".
|
|
239
|
+
let blockHash;
|
|
240
|
+
try {
|
|
241
|
+
blockHash = computeIntegrationBlockHash(filePath);
|
|
242
|
+
} catch (error) {
|
|
243
|
+
if (error instanceof AmbiguousMarkerError) {
|
|
244
|
+
blockHash = null;
|
|
245
|
+
} else {
|
|
246
|
+
throw error;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
234
249
|
const fileHash = computeFileHash(filePath);
|
|
235
250
|
|
|
236
251
|
state.integrations.set(toolConfig.file, {
|
|
@@ -543,7 +558,19 @@ function hasUDSMarkers(filePath, format) {
|
|
|
543
558
|
try {
|
|
544
559
|
const content = readFileSync(filePath, 'utf-8');
|
|
545
560
|
const markers = UDS_MARKERS[format] || UDS_MARKERS.markdown;
|
|
546
|
-
|
|
561
|
+
// XSPEC adopter-report Q5: `.includes()` matched the marker text
|
|
562
|
+
// anywhere in the file, including inside a sentence that merely mentions
|
|
563
|
+
// it — locateMarkerBlock requires a standalone, non-fenced line. An
|
|
564
|
+
// ambiguous file (two real marker lines) still "has UDS markers" for
|
|
565
|
+
// this boolean's purpose, so that is also true, not an error here.
|
|
566
|
+
try {
|
|
567
|
+
return locateMarkerBlock(content, markers) !== null;
|
|
568
|
+
} catch (error) {
|
|
569
|
+
if (error instanceof AmbiguousMarkerError) {
|
|
570
|
+
return true;
|
|
571
|
+
}
|
|
572
|
+
throw error;
|
|
573
|
+
}
|
|
547
574
|
} catch {
|
|
548
575
|
return false;
|
|
549
576
|
}
|
|
@@ -15,8 +15,15 @@ import {
|
|
|
15
15
|
MANIFEST_OPTION_BINDINGS,
|
|
16
16
|
OPTIONS_INSTALL_DIR
|
|
17
17
|
} from '../core/constants.js';
|
|
18
|
+
import {
|
|
19
|
+
resolveIntegrationTargetFile,
|
|
20
|
+
buildToolIntegrationConfig,
|
|
21
|
+
generateIntegrationContent,
|
|
22
|
+
extractMarkedContent
|
|
23
|
+
} from '../utils/integration-generator.js';
|
|
18
24
|
import { PathResolver } from '../core/paths.js';
|
|
19
25
|
import { computeFileHash } from '../utils/hasher.js';
|
|
26
|
+
import { createHash } from 'crypto';
|
|
20
27
|
import {
|
|
21
28
|
getSkillsDirForAgent,
|
|
22
29
|
getCommandsDirForAgent,
|
|
@@ -261,6 +268,37 @@ function calculateExtensions(state, manifest) {
|
|
|
261
268
|
}
|
|
262
269
|
}
|
|
263
270
|
|
|
271
|
+
/**
|
|
272
|
+
* Compute the block hash generation would produce for this tool right now,
|
|
273
|
+
* using the same builder (`buildToolIntegrationConfig`) and generator
|
|
274
|
+
* (`generateIntegrationContent`) `--apply`/`uds update` themselves use.
|
|
275
|
+
*
|
|
276
|
+
* Hashed the same way `computeIntegrationBlockHash` (hasher.js) hashes an
|
|
277
|
+
* actual on-disk file — extract the marker block, trim, sha256 — so the two
|
|
278
|
+
* are directly comparable. Never throws: generation can fail for reasons
|
|
279
|
+
* outside this module's control (an incomplete/mocked registry, a tool this
|
|
280
|
+
* process cannot resolve, etc.), and the correct response to "cannot compute
|
|
281
|
+
* an expected hash" is `null` — diffIntegrations already knows how to treat
|
|
282
|
+
* that the same as before this fix (always migrate_block), not to crash the
|
|
283
|
+
* whole state calculation over one tool.
|
|
284
|
+
*
|
|
285
|
+
* @param {Object} manifest
|
|
286
|
+
* @param {string} toolName
|
|
287
|
+
* @param {string} format
|
|
288
|
+
* @returns {string|null}
|
|
289
|
+
*/
|
|
290
|
+
function computeExpectedIntegrationBlockHash(manifest, toolName, format) {
|
|
291
|
+
try {
|
|
292
|
+
const config = { ...buildToolIntegrationConfig(manifest, toolName), format: manifest.format || 'ai' };
|
|
293
|
+
const generated = generateIntegrationContent({ ...config, tool: toolName });
|
|
294
|
+
const { content: blockContent } = extractMarkedContent(generated, format);
|
|
295
|
+
if (!blockContent) return null;
|
|
296
|
+
return `sha256:${createHash('sha256').update(blockContent).digest('hex')}`;
|
|
297
|
+
} catch {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
264
302
|
/**
|
|
265
303
|
* Calculate expected integration files.
|
|
266
304
|
* For integrations we track the UDS marker block, not the entire file.
|
|
@@ -281,11 +319,22 @@ function calculateIntegrations(state, manifest) {
|
|
|
281
319
|
const toolConfig = toolName ? SUPPORTED_AI_TOOLS[toolName] : null;
|
|
282
320
|
if (!toolConfig) continue;
|
|
283
321
|
|
|
284
|
-
|
|
322
|
+
// XSPEC-418 R2/R3: the desired path is the tool's ACTUAL target, not always
|
|
323
|
+
// its default file — a local-target claude-code install's desired state
|
|
324
|
+
// must say CLAUDE.local.md, or `--apply`/`--plan` (and their orphan
|
|
325
|
+
// detection) would treat that file as unmanaged and CLAUDE.md as desired.
|
|
326
|
+
const relativePath = resolveIntegrationTargetFile(toolName, manifest) || toolConfig.file;
|
|
285
327
|
|
|
286
328
|
state.integrations.set(relativePath, {
|
|
287
329
|
relativePath,
|
|
288
|
-
|
|
330
|
+
// XSPEC adopter-report Q6: this used to be a hardcoded `null` ("hashes
|
|
331
|
+
// are computed after generation"), which is why diffIntegrations could
|
|
332
|
+
// never tell "content already matches" from "content differs" and
|
|
333
|
+
// unconditionally produced migrate_block — `--plan` never converged,
|
|
334
|
+
// even immediately after `--apply`. Actually generating the content
|
|
335
|
+
// ahead of time (the same generation `--apply` itself runs) gives a
|
|
336
|
+
// real hash to compare against the actual file's block hash.
|
|
337
|
+
hash: computeExpectedIntegrationBlockHash(manifest, toolName, toolConfig.format),
|
|
289
338
|
size: null,
|
|
290
339
|
category: 'integration',
|
|
291
340
|
sourcePath: null, // Generated, not copied from source
|
|
@@ -272,18 +272,34 @@ function diffIntegrations(desiredMap, actualMap, actions, warnings, summary, for
|
|
|
272
272
|
}
|
|
273
273
|
});
|
|
274
274
|
summary.migrate_block++;
|
|
275
|
+
} else if (desiredEntry.hash && actualEntry.hash && desiredEntry.hash === actualEntry.hash) {
|
|
276
|
+
// XSPEC adopter-report Q6: the block on disk already hashes the same
|
|
277
|
+
// as what generation would produce right now — nothing to do. Before
|
|
278
|
+
// this, every migrate_block fired unconditionally ("we always update
|
|
279
|
+
// integrations since content is generated dynamically"), so `--plan`
|
|
280
|
+
// reported `Migrate Block: N` forever, even immediately after
|
|
281
|
+
// `--apply` on an unchanged project. `desiredEntry.hash` is computed
|
|
282
|
+
// by desired-state-calculator.js actually generating the content
|
|
283
|
+
// ahead of time (see calculateIntegrations), the same generation
|
|
284
|
+
// `--apply` itself would run.
|
|
285
|
+
summary.unchanged++;
|
|
275
286
|
} else {
|
|
276
|
-
//
|
|
277
|
-
//
|
|
287
|
+
// Either the hashes genuinely differ, or one side has no hash to
|
|
288
|
+
// compare (generation failed, or an older/mocked desired state that
|
|
289
|
+
// never computed one) — in which case this falls back to the
|
|
290
|
+
// previous unconditional behavior rather than silently doing nothing.
|
|
278
291
|
actions.push({
|
|
279
292
|
type: 'migrate_block',
|
|
280
293
|
category: 'integration',
|
|
281
294
|
path: desiredEntry.relativePath,
|
|
282
|
-
reason:
|
|
295
|
+
reason: (desiredEntry.hash && actualEntry.hash)
|
|
296
|
+
? 'integration content differs from what would be generated'
|
|
297
|
+
: 'integration content may need update (no hash available for comparison)',
|
|
283
298
|
details: {
|
|
284
299
|
toolName: desiredEntry.metadata.toolName,
|
|
285
300
|
format: desiredEntry.metadata.format,
|
|
286
301
|
currentBlockHash: actualEntry.metadata?.blockHash?.blockHash,
|
|
302
|
+
desiredBlockHash: desiredEntry.hash,
|
|
287
303
|
metadata: desiredEntry.metadata
|
|
288
304
|
}
|
|
289
305
|
});
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
import { writeManifest } from '../core/manifest.js';
|
|
23
23
|
import { getRepositoryInfo } from '../utils/registry.js';
|
|
24
24
|
import { displayLanguageToLocale } from '../utils/locale.js';
|
|
25
|
-
import { computeFileHash } from '../utils/hasher.js';
|
|
25
|
+
import { computeFileHash, pruneIntegrationFileHashes } from '../utils/hasher.js';
|
|
26
26
|
import { createBackup, cleanupBackups } from './backup-manager.js';
|
|
27
27
|
|
|
28
28
|
/**
|
|
@@ -179,6 +179,11 @@ export async function executePlan(projectPath, plan, manifest, options = {}) {
|
|
|
179
179
|
// Write updated manifest
|
|
180
180
|
if (!dryRun) {
|
|
181
181
|
try {
|
|
182
|
+
// XSPEC-418 R6: catches any stale whole-file entry for an integration
|
|
183
|
+
// file this particular plan didn't touch (e.g. `--plan --skills` never
|
|
184
|
+
// reaches executeMigrateBlock), not just the one this run might
|
|
185
|
+
// otherwise have added.
|
|
186
|
+
pruneIntegrationFileHashes(updatedManifest);
|
|
182
187
|
writeManifest(updatedManifest, projectPath);
|
|
183
188
|
} catch (err) {
|
|
184
189
|
results.push({
|
|
@@ -351,21 +356,17 @@ function executeMigrateBlock(projectPath, action, manifest) {
|
|
|
351
356
|
if (result.blockHashInfo) {
|
|
352
357
|
manifest.integrationBlockHashes[result.path] = result.blockHashInfo;
|
|
353
358
|
}
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
//
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
installedAt: tracked.installedAt || new Date().toISOString()
|
|
366
|
-
};
|
|
367
|
-
}
|
|
368
|
-
}
|
|
359
|
+
// XSPEC-418 R6 supersedes the XSPEC-343 R2 fix this replaces. That fix
|
|
360
|
+
// refreshed a whole-file `fileHashes` entry here if one already existed,
|
|
361
|
+
// reasoning that leaving it stale would report the file "modified"
|
|
362
|
+
// forever. Refreshing it does stop that — but a whole-file hash for an
|
|
363
|
+
// integration file disagrees with reality the moment the adopter edits
|
|
364
|
+
// anything OUTSIDE the block, which the marker-based update above
|
|
365
|
+
// deliberately leaves alone; "modified" then fires for content UDS itself
|
|
366
|
+
// says it preserves, and contradicts the same run's block-integrity
|
|
367
|
+
// check. The correct fix is for this path not to be in `fileHashes` at
|
|
368
|
+
// all — it is tracked by the block hash above instead.
|
|
369
|
+
if (manifest.fileHashes) delete manifest.fileHashes[result.path];
|
|
369
370
|
return { action, success: true };
|
|
370
371
|
}
|
|
371
372
|
|
package/src/utils/hasher.js
CHANGED
|
@@ -4,6 +4,7 @@ import { join, relative } from 'path';
|
|
|
4
4
|
import { UDS_MARKERS } from '../core/constants.js';
|
|
5
5
|
import { resolveIntegrationFile } from '../core/constants.js';
|
|
6
6
|
import { isProvenanceEstablished } from '../core/manifest.js';
|
|
7
|
+
import { locateMarkerBlock, AmbiguousMarkerError } from './marker-locator.js';
|
|
7
8
|
|
|
8
9
|
// GitHub issue #155. `git config core.autocrlf true` (the common
|
|
9
10
|
// Windows default) rewrites LF to CRLF on checkout. The manifest's stored
|
|
@@ -472,13 +473,16 @@ function detectFormat(filePath) {
|
|
|
472
473
|
*/
|
|
473
474
|
function extractBlockContent(content, format) {
|
|
474
475
|
const markers = UDS_MARKERS[format] || UDS_MARKERS.markdown;
|
|
475
|
-
|
|
476
|
-
|
|
476
|
+
// XSPEC adopter-report Q5: locateMarkerBlock only counts a marker when it
|
|
477
|
+
// occupies a whole line by itself, not merely appears somewhere on one —
|
|
478
|
+
// see marker-locator.js for why raw indexOf broke on real files.
|
|
479
|
+
const block = locateMarkerBlock(content, markers);
|
|
477
480
|
|
|
478
|
-
if (
|
|
481
|
+
if (!block) {
|
|
479
482
|
return { before: content, blockContent: '', after: '' };
|
|
480
483
|
}
|
|
481
484
|
|
|
485
|
+
const { startIdx, endIdx } = block;
|
|
482
486
|
return {
|
|
483
487
|
before: content.substring(0, startIdx),
|
|
484
488
|
blockContent: content.substring(startIdx + markers.start.length, endIdx).trim(),
|
|
@@ -515,7 +519,15 @@ export function computeIntegrationBlockHash(filePath) {
|
|
|
515
519
|
fullHash: `sha256:${fullHash}`,
|
|
516
520
|
fullSize: Buffer.byteLength(content, 'utf-8')
|
|
517
521
|
};
|
|
518
|
-
} catch {
|
|
522
|
+
} catch (error) {
|
|
523
|
+
// XSPEC adopter-report Q5: an ambiguous marker pair (two real START or
|
|
524
|
+
// END lines) is a distinct, reportable condition — not "no markers
|
|
525
|
+
// found". Every other error (unreadable file, etc.) keeps the original
|
|
526
|
+
// silent-null behavior; callers that need to surface the ambiguity to a
|
|
527
|
+
// user must catch AmbiguousMarkerError explicitly (see check.js).
|
|
528
|
+
if (error instanceof AmbiguousMarkerError) {
|
|
529
|
+
throw error;
|
|
530
|
+
}
|
|
519
531
|
return null;
|
|
520
532
|
}
|
|
521
533
|
}
|
|
@@ -631,7 +643,14 @@ export function compareDirectoryHashes(dirPath, storedHashes, baseKey = '') {
|
|
|
631
643
|
|
|
632
644
|
/**
|
|
633
645
|
* Refresh all integrationBlockHashes in manifest by recalculating from disk
|
|
634
|
-
* Ensures manifest hashes always match actual file content
|
|
646
|
+
* Ensures manifest hashes always match actual file content.
|
|
647
|
+
*
|
|
648
|
+
* Also prunes any `fileHashes` entry for the same paths (XSPEC-418 R6) — every
|
|
649
|
+
* call site of this function is a "we just wrote/restored an integration file,
|
|
650
|
+
* about to persist the manifest" checkpoint, which is exactly where a stale
|
|
651
|
+
* whole-file hash for that same path (written by an older CLI, or a write path
|
|
652
|
+
* this fix missed) needs to stop existing. See `pruneIntegrationFileHashes`.
|
|
653
|
+
*
|
|
635
654
|
* @param {Object} manifest - Manifest object (mutated in place)
|
|
636
655
|
* @param {string} projectPath - Project root path
|
|
637
656
|
* @returns {Object} The updated manifest
|
|
@@ -652,5 +671,42 @@ export function refreshIntegrationBlockHashes(manifest, projectPath) {
|
|
|
652
671
|
}
|
|
653
672
|
}
|
|
654
673
|
|
|
674
|
+
pruneIntegrationFileHashes(manifest);
|
|
675
|
+
|
|
655
676
|
return manifest;
|
|
656
677
|
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Remove `fileHashes` entries for files UDS tracks by their UDS block instead
|
|
681
|
+
* (`integrationBlockHashes`) — CLAUDE.md, CLAUDE.local.md, AGENTS.md, GEMINI.md,
|
|
682
|
+
* etc. (XSPEC-418 R6).
|
|
683
|
+
*
|
|
684
|
+
* An integration file's whole-file hash and its block hash disagree the
|
|
685
|
+
* moment an adopter edits anything OUTSIDE the UDS block — exactly the
|
|
686
|
+
* customization UDS's marker-based update promises to preserve. Several write
|
|
687
|
+
* paths (`uds update`, `uds update --integrations-only`, `uds check
|
|
688
|
+
* --restore`, `uds check --migrate`) used to add a whole-file entry for these
|
|
689
|
+
* paths anyway, so `uds check --ci` could report "CLAUDE.md (modified)" from
|
|
690
|
+
* standards-file integrity in the same run its own block-integrity check said
|
|
691
|
+
* the block was intact — the two checks contradicted each other, and the one
|
|
692
|
+
* that failed was the one punishing content UDS says it preserves.
|
|
693
|
+
*
|
|
694
|
+
* `manifest.integrationBlockHashes` is the authoritative registry of which
|
|
695
|
+
* paths are integration files — every writer of it (`writeIntegrationFile`,
|
|
696
|
+
* `writeAgentsMdSummary`) sets an entry there and nowhere else, so keying off
|
|
697
|
+
* its keys needs no second list of "known" integration files to keep in sync.
|
|
698
|
+
*
|
|
699
|
+
* @param {Object} manifest - Manifest object (mutated in place)
|
|
700
|
+
* @returns {string[]} Paths whose stale whole-file hash was removed
|
|
701
|
+
*/
|
|
702
|
+
export function pruneIntegrationFileHashes(manifest) {
|
|
703
|
+
if (!manifest?.fileHashes || !manifest?.integrationBlockHashes) return [];
|
|
704
|
+
const removed = [];
|
|
705
|
+
for (const path of Object.keys(manifest.integrationBlockHashes)) {
|
|
706
|
+
if (path in manifest.fileHashes) {
|
|
707
|
+
delete manifest.fileHashes[path];
|
|
708
|
+
removed.push(path);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
return removed;
|
|
712
|
+
}
|