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.
Files changed (34) hide show
  1. package/bin/uds.js +2 -0
  2. package/bundled/locales/zh-CN/CHANGELOG.md +23 -3
  3. package/bundled/locales/zh-CN/README.md +1 -1
  4. package/bundled/locales/zh-CN/SECURITY.md +1 -1
  5. package/bundled/locales/zh-CN/docs/CHEATSHEET.md +1 -1
  6. package/bundled/locales/zh-CN/docs/CLI-INIT-OPTIONS.md +52 -5
  7. package/bundled/locales/zh-CN/docs/FEATURE-REFERENCE.md +3 -1
  8. package/bundled/locales/zh-TW/CHANGELOG.md +23 -3
  9. package/bundled/locales/zh-TW/README.md +1 -1
  10. package/bundled/locales/zh-TW/SECURITY.md +1 -1
  11. package/bundled/locales/zh-TW/docs/CHEATSHEET.md +1 -1
  12. package/bundled/locales/zh-TW/docs/CLI-INIT-OPTIONS.md +52 -5
  13. package/bundled/locales/zh-TW/docs/FEATURE-REFERENCE.md +3 -1
  14. package/bundled/locales/zh-TW/integrations/claude-code/README.md +14 -5
  15. package/package.json +1 -1
  16. package/src/commands/check.js +253 -21
  17. package/src/commands/config.js +15 -9
  18. package/src/commands/init.js +24 -3
  19. package/src/commands/update.js +311 -47
  20. package/src/core/manifest.js +39 -1
  21. package/src/flows/init-flow.js +9 -1
  22. package/src/generators/layered-claudemd.js +13 -4
  23. package/src/i18n/messages.js +3 -3
  24. package/src/installers/integration-installer.js +13 -6
  25. package/src/installers/manifest-installer.js +4 -0
  26. package/src/reconciler/actual-state-scanner.js +29 -2
  27. package/src/reconciler/desired-state-calculator.js +51 -2
  28. package/src/reconciler/diff-engine.js +19 -3
  29. package/src/reconciler/plan-executor.js +17 -16
  30. package/src/utils/hasher.js +61 -5
  31. package/src/utils/integration-generator.js +239 -28
  32. package/src/utils/marker-locator.js +140 -0
  33. package/src/utils/reference-sync.js +53 -1
  34. package/standards-registry.json +7 -7
@@ -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 not referenced (optional):',
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
- getToolFilePath,
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
- const targetFile = getToolFilePath(tool);
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 CLAUDE.md doesn't exist
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
- const blockHash = computeIntegrationBlockHash(filePath);
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
- return content.includes(markers.start) && content.includes(markers.end);
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
- const relativePath = toolConfig.file;
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
- hash: null, // Integration hashes are computed after generation
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
- // Check if block hash differs from what we'd generate
277
- // We always update integrations since content is generated dynamically
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: 'integration content may need update',
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
- // `init` also records integration files in the whole-file `fileHashes`, which
355
- // is what `uds check`'s File Integrity compares. Rewriting the block without
356
- // refreshing that entry left the file permanently reported as "modified" —
357
- // a successful reconcile that reads, afterwards, as a damaged install.
358
- // (XSPEC-343 R2)
359
- const tracked = manifest.fileHashes?.[result.path];
360
- if (tracked) {
361
- const info = computeFileHash(join(projectPath, result.path));
362
- if (info) {
363
- manifest.fileHashes[result.path] = {
364
- ...info,
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
 
@@ -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
- const startIdx = content.indexOf(markers.start);
476
- const endIdx = content.indexOf(markers.end);
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 (startIdx === -1 || endIdx === -1 || endIdx <= startIdx) {
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
+ }