@ttsc/unplugin 0.30.3 → 0.30.4

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.
@@ -118,6 +118,8 @@ const TERMINAL_TRANSFORM_GENERATIONS = new WeakMap();
118
118
  const MAX_GENERATION_PROOF_FAILURES = 8;
119
119
  /** Maximum exact mutation paths kept after a tracker already proved a change. */
120
120
  const MAX_GENERATION_MUTATION_PATHS = 8;
121
+ /** Maximum unrelated roots watched before snapshot validation takes over. */
122
+ const MAX_HOST_INPUT_WATCH_SCOPES = 16;
121
123
  /** One retry absorbs a transient watch write without admitting an infinite loop. */
122
124
  const TRANSFORM_GENERATION_ATTEMPTS = 2;
123
125
  const DEFAULT_FILESYSTEM_OPERATIONS = Object.freeze({
@@ -380,6 +382,11 @@ async function transformTtsc(id, source, options, aliases, cache, hooks) {
380
382
  deliveryEpoch: epoch,
381
383
  filesystem,
382
384
  plugins: options.plugins,
385
+ // One bounded recursive project observer witnesses content restored
386
+ // during the compile itself. Build-scoped adapters close it with the
387
+ // attempt; persistent adapters retain it to make later validations
388
+ // constant-cost while the generation remains live.
389
+ retainProjectMembership: cache !== undefined && epoch === undefined,
383
390
  trackProjectMembership: cache !== undefined,
384
391
  tsconfig,
385
392
  });
@@ -1677,6 +1684,24 @@ function matchesNarrowPersistentInputs(cached, file) {
1677
1684
  }
1678
1685
  return true;
1679
1686
  }
1687
+ /** Whether a healthy notification scope proves one exact input unchanged. */
1688
+ function trackerProvesInputUnchanged(tracker, input) {
1689
+ if (tracker === undefined || tracker.failed || tracker.changesOmitted) {
1690
+ return false;
1691
+ }
1692
+ if (tracker.contentAuthoritative !== true)
1693
+ return false;
1694
+ const absolute = path.resolve(input);
1695
+ if (tracker.covered?.has(absolute) !== true)
1696
+ return false;
1697
+ for (const changed of tracker.changes) {
1698
+ if (tracker.overlaps?.(absolute, changed) ??
1699
+ (pathIsWithin(absolute, changed) || pathIsWithin(changed, absolute))) {
1700
+ return false;
1701
+ }
1702
+ }
1703
+ return true;
1704
+ }
1680
1705
  /**
1681
1706
  * Validate one derived input against the generation, skipping the content read
1682
1707
  * while the recorded metadata signature still holds and its freshly minted
@@ -1700,6 +1725,10 @@ function matchesNarrowPersistentInputs(cached, file) {
1700
1725
  * junction moves it and the skipped realpath comparison cannot be evaded.
1701
1726
  */
1702
1727
  function matchesProvenInput(cached, state, input) {
1728
+ if (trackerProvesInputUnchanged(cached.projectMutationTracker, input) ||
1729
+ trackerProvesInputUnchanged(cached.hostInputMutationTracker, input)) {
1730
+ return true;
1731
+ }
1703
1732
  const observation = cached.externalInputObservations?.[path.resolve(input)];
1704
1733
  if (observation !== undefined) {
1705
1734
  if (observation.fileExists === false &&
@@ -1707,7 +1736,25 @@ function matchesProvenInput(cached, state, input) {
1707
1736
  notifiesAbsence(cached, input)) {
1708
1737
  return true;
1709
1738
  }
1710
- return matchesRecordedInput(cached, input);
1739
+ const filesystem = resultFilesystem(cached.result);
1740
+ const spelling = path.resolve(input);
1741
+ const before = inputMetadataEvidence(input, filesystem);
1742
+ if (before !== undefined &&
1743
+ before.separable &&
1744
+ cached.externalInputSignatures?.[spelling] === before.signature) {
1745
+ return true;
1746
+ }
1747
+ if (!matchesRecordedInput(cached, input))
1748
+ return false;
1749
+ const after = inputMetadataSignature(input, filesystem);
1750
+ const signatures = (cached.externalInputSignatures ??= {});
1751
+ if (before?.separable === true && before.signature === after) {
1752
+ signatures[spelling] = after;
1753
+ }
1754
+ else {
1755
+ delete signatures[spelling];
1756
+ }
1757
+ return true;
1711
1758
  }
1712
1759
  const slot = inputSignatureSlot(cached, state, input);
1713
1760
  if (slot === undefined) {
@@ -1755,10 +1802,7 @@ function matchesProvenInput(cached, state, input) {
1755
1802
  * the whole generation back to complete-snapshot validation.
1756
1803
  */
1757
1804
  function notifiesAbsence(cached, input) {
1758
- const tracker = cached.candidateMutationTracker;
1759
- return (tracker !== undefined &&
1760
- !tracker.failed &&
1761
- tracker.covered?.has(path.resolve(input)) === true);
1805
+ return trackerProvesInputUnchanged(cached.candidateMutationTracker, input);
1762
1806
  }
1763
1807
  /**
1764
1808
  * Locate the signature manifest that owns one recorded input, mirroring
@@ -1804,6 +1848,15 @@ function inputSignatureSlot(cached, state, input) {
1804
1848
  * existing directory and checked through one exact membership listing.
1805
1849
  */
1806
1850
  function matchesUniversalHostInputs(cached, validation) {
1851
+ let notificationsProveAll = true;
1852
+ for (const input of validation.covered) {
1853
+ if (!trackerProvesInputUnchanged(cached.hostInputMutationTracker, input)) {
1854
+ notificationsProveAll = false;
1855
+ break;
1856
+ }
1857
+ }
1858
+ if (notificationsProveAll)
1859
+ return true;
1807
1860
  return (matchesUniversalHostInputEntries(cached, validation) &&
1808
1861
  matchesUniversalHostInputProbes(cached, validation));
1809
1862
  }
@@ -1831,6 +1884,11 @@ function matchesUniversalHostInputEntries(cached, validation) {
1831
1884
  if (!matchesRecordedInput(cached, entry.path)) {
1832
1885
  return false;
1833
1886
  }
1887
+ if (entry.readable === false) {
1888
+ const slot = inputSignatureSlot(cached, envelopeDerivation(cached), entry.path);
1889
+ entry.readable =
1890
+ slot !== undefined && slot.recorded !== MISSING_INPUT_STATE;
1891
+ }
1834
1892
  if (evidence === undefined)
1835
1893
  return false;
1836
1894
  // Re-earn the proof under the rules the capture applies: an entry whose
@@ -2039,6 +2097,37 @@ function captureUniversalHostInputValidation(cached, currentFile) {
2039
2097
  cached.hostInputValidation = validation;
2040
2098
  return { failures, validation };
2041
2099
  }
2100
+ /**
2101
+ * Keep watcher silence as a content proof only for inputs whose bytes capture
2102
+ * actually proved.
2103
+ *
2104
+ * The observers must open before the post-compile snapshots so they can witness
2105
+ * an A-B-A race. Their exact coverage can only be narrowed after those reads.
2106
+ * Existing-but-unreadable and absent inputs record the same `missing` state;
2107
+ * neither may inherit a content shortcut because its metadata can stay fixed
2108
+ * while bytes become readable. Missing resolver candidates inside the project
2109
+ * retain their separate component-aware candidate tracker, while every other
2110
+ * such input continues through the recorded predicate or directory-list proof.
2111
+ */
2112
+ function restrictNotificationCoverageToProvenInputs(tracker, cached, hostValidation) {
2113
+ if (!(tracker?.covered instanceof Set))
2114
+ return;
2115
+ const covered = tracker.covered;
2116
+ const state = envelopeDerivation(cached);
2117
+ for (const input of [...covered]) {
2118
+ const absolute = path.resolve(input);
2119
+ const hostEntry = hostValidation?.entries.get(absolute);
2120
+ if ((hostValidation?.covered.has(absolute) === true &&
2121
+ (hostEntry === undefined ||
2122
+ (hostEntry.readable === false && hostEntry.strict !== true))) ||
2123
+ cached.externalInputHashes?.[derivationIdentity(state, absolute)] ===
2124
+ MISSING_INPUT_STATE ||
2125
+ cached.externalInputObservations?.[absolute]?.readFile?.ok === false ||
2126
+ cached.externalInputObservations?.[absolute]?.fileExists === false) {
2127
+ covered.delete(input);
2128
+ }
2129
+ }
2130
+ }
2042
2131
  /**
2043
2132
  * The recorded state of an input the generation read nothing from: absent, or
2044
2133
  * present but unreadable. It is deliberately not a hash, so no signature may
@@ -2185,6 +2274,7 @@ function inputMetadataEvidence(file, filesystem = DEFAULT_FILESYSTEM_OPERATIONS)
2185
2274
  link.ctimeNs,
2186
2275
  "missing-target",
2187
2276
  ].join(":"),
2277
+ notificationAuthoritative: false,
2188
2278
  separable: false,
2189
2279
  };
2190
2280
  }
@@ -2193,17 +2283,25 @@ function inputMetadataEvidence(file, filesystem = DEFAULT_FILESYSTEM_OPERATIONS)
2193
2283
  signature: [
2194
2284
  link.dev,
2195
2285
  link.ino,
2286
+ link.nlink,
2196
2287
  link.mode,
2197
2288
  link.size,
2198
2289
  link.mtimeNs,
2199
2290
  link.ctimeNs,
2200
2291
  target.dev,
2201
2292
  target.ino,
2293
+ target.nlink,
2202
2294
  target.mode,
2203
2295
  target.size,
2204
2296
  target.mtimeNs,
2205
2297
  target.ctimeNs,
2206
2298
  ].join(":"),
2299
+ // A write through a hardlink outside the watched tree changes this inode
2300
+ // without notifying either the original directory or a watcher opened on
2301
+ // the original path. Keep such files on metadata validation. Symlinks are
2302
+ // likewise governed by the target path, outside the lexical observer.
2303
+ notificationAuthoritative: !link.isSymbolicLink() &&
2304
+ !(link.isFile() && (link.nlink > 1n || target.nlink > 1n)),
2207
2305
  // Both halves must be separable: a write remints the target's stamp, a
2208
2306
  // link retarget the link's own, and either one hiding inside its recorded
2209
2307
  // tick would evade the skipped content and realpath comparisons.
@@ -2598,7 +2696,9 @@ function captureExternalInputSnapshot(cached, paths) {
2598
2696
  });
2599
2697
  continue;
2600
2698
  }
2699
+ const before = inputMetadataEvidence(input, filesystem);
2601
2700
  const mismatches = graphInputObservationFailures(input, predicateObservation, filesystem, state.identityContext);
2701
+ const after = inputMetadataSignature(input, filesystem);
2602
2702
  if (mismatches.length !== 0)
2603
2703
  complete = false;
2604
2704
  for (const kind of mismatches) {
@@ -2608,6 +2708,8 @@ function captureExternalInputSnapshot(cached, paths) {
2608
2708
  path: input,
2609
2709
  });
2610
2710
  }
2711
+ if (mismatches.length === 0)
2712
+ record(input, before, after);
2611
2713
  observations[spelling] = predicateObservation;
2612
2714
  continue;
2613
2715
  }
@@ -2875,6 +2977,7 @@ function collectProjectInputHashSnapshot(projectRoot, identities = createHostPat
2875
2977
  /** Hash project files and snapshot the directory topology in one walk. */
2876
2978
  function collectProjectInputSnapshot(projectRoot, identities, filesystem = DEFAULT_FILESYSTEM_OPERATIONS, proven, options) {
2877
2979
  const hashes = {};
2980
+ const notificationUnsafeInputs = new Set();
2878
2981
  const fileSignatures = {};
2879
2982
  const provenSignatures = {};
2880
2983
  const unstableFiles = new Set();
@@ -2897,6 +3000,9 @@ function collectProjectInputSnapshot(projectRoot, identities, filesystem = DEFAU
2897
3000
  continue;
2898
3001
  }
2899
3002
  const before = inputMetadataEvidence(file, filesystem);
3003
+ if (before?.notificationAuthoritative !== true) {
3004
+ notificationUnsafeInputs.add(key);
3005
+ }
2900
3006
  // A file whose signature still equals the one captured around the read
2901
3007
  // that produced the recorded hash carries that content, so the whole
2902
3008
  // project does not have to be re-read to prove one delivery. Recheck the
@@ -2913,23 +3019,26 @@ function collectProjectInputSnapshot(projectRoot, identities, filesystem = DEFAU
2913
3019
  continue;
2914
3020
  }
2915
3021
  const contents = filesystem.readFile(file);
2916
- const after = inputMetadataSignature(file, filesystem);
3022
+ const after = inputMetadataEvidence(file, filesystem);
3023
+ if (after?.notificationAuthoritative !== true) {
3024
+ notificationUnsafeInputs.add(key);
3025
+ }
2917
3026
  hashes[key] = hashText(contents);
2918
3027
  if (before === undefined ||
2919
3028
  after === undefined ||
2920
- before.signature !== after) {
3029
+ before.signature !== after.signature) {
2921
3030
  complete = false;
2922
3031
  unstableFiles.add(key);
2923
3032
  walkFailures.push({ kind: "file-changed-during-read", path: file });
2924
3033
  }
2925
3034
  else {
2926
- fileSignatures[key] = after;
3035
+ fileSignatures[key] = after.signature;
2927
3036
  // Only a signature whose stamp's tick the filesystem's clock provably
2928
3037
  // left before this read may later stand in for the content comparison
2929
3038
  // ({@link stampSeparable}); the raw signature above still participates
2930
3039
  // in the generation-time stability comparison.
2931
3040
  if (before.separable) {
2932
- provenSignatures[key] = after;
3041
+ provenSignatures[key] = after.signature;
2933
3042
  }
2934
3043
  }
2935
3044
  }
@@ -2953,6 +3062,7 @@ function collectProjectInputSnapshot(projectRoot, identities, filesystem = DEFAU
2953
3062
  directoryComplete: walked.complete && attributed,
2954
3063
  fileSignatures,
2955
3064
  hashes,
3065
+ notificationUnsafeInputs,
2956
3066
  projectDirectories: walked.directories,
2957
3067
  provenSignatures,
2958
3068
  unstableFiles,
@@ -3147,11 +3257,11 @@ function sameProjectDirectories(left, right) {
3147
3257
  * underlying watch does, so callers classify a registration failure
3148
3258
  * themselves.
3149
3259
  */
3150
- function openDirectoryWatch(filesystem, directory, listener, onError) {
3260
+ function openDirectoryWatch(filesystem, directory, listener, onError, recursive = false) {
3151
3261
  if (filesystem.watch !== undefined) {
3152
- return filesystem.watch(directory, listener, onError);
3262
+ return filesystem.watch(directory, listener, onError, recursive);
3153
3263
  }
3154
- const watcher = fs.watch(directory, { persistent: false }, (eventType, filename) => listener(eventType, filename === null ? null : String(filename)));
3264
+ const watcher = fs.watch(directory, { persistent: false, recursive }, (eventType, filename) => listener(eventType, filename === null ? null : String(filename)));
3155
3265
  watcher.on("error", onError);
3156
3266
  return { close: () => watcher.close() };
3157
3267
  }
@@ -3175,106 +3285,223 @@ function closeDirectoryWatches(watchers) {
3175
3285
  throw failure;
3176
3286
  }
3177
3287
  /** Watch every walked directory for membership changes after generation. */
3178
- async function createProjectMutationTracker(directories, filesystem = DEFAULT_FILESYSTEM_OPERATIONS, policy = PERMISSIVE_PROJECT_MEMBERSHIP_POLICY) {
3288
+ async function createProjectMutationTracker(directories, covered, filesystem = DEFAULT_FILESYSTEM_OPERATIONS, policy = PERMISSIVE_PROJECT_MEMBERSHIP_POLICY) {
3289
+ const identities = createHostPathIdentityContext(filesystem);
3290
+ const root = commonDirectoryRoot(directories.map((directory) => directory.path));
3291
+ const authoritative = root !== undefined &&
3292
+ filesystem.watch === undefined &&
3293
+ pathTraversesSymbolicLink(path.join(root, ".ttsc-notification-authority"), filesystem, new Map())
3294
+ ? new Set()
3295
+ : covered;
3179
3296
  const tracker = {
3180
3297
  changes: new Set(),
3181
3298
  changesOmitted: false,
3182
- close: () => undefined,
3299
+ close: () => {
3300
+ tracker.failed = true;
3301
+ },
3302
+ covered: authoritative,
3183
3303
  failed: false,
3184
3304
  membershipChanged: false,
3305
+ overlaps: (input, changed) => identities.isWithin(input, changed) ||
3306
+ identities.isWithin(changed, input),
3307
+ contentAuthoritative: filesystem.watch === undefined,
3308
+ };
3309
+ if (root === undefined)
3310
+ return tracker;
3311
+ const knownDirectories = new Set(directories
3312
+ .filter((directory) => directory.relevant)
3313
+ .map((directory) => path.resolve(directory.path)));
3314
+ const reportsMembership = (location, filename) => {
3315
+ const changed = path.join(location, filename);
3316
+ return (knownDirectories.has(path.resolve(changed)) ||
3317
+ reportsProgramMembership(changed, path.basename(filename), policy, filesystem));
3318
+ };
3319
+ const reportsNewMembership = (location, filename) => {
3320
+ const changed = path.resolve(location, filename);
3321
+ return (!knownDirectories.has(changed) && tracker.covered?.has(changed) !== true);
3185
3322
  };
3186
3323
  if (process.platform === "win32" && filesystem.watch === undefined) {
3187
- await registerWindowsProjectMutationTracker(tracker, directories.map((directory) => ({ directory: directory.path })), false, filesystem, (location, filename) => reportsProgramMembership(path.join(location, filename), filename, policy, filesystem));
3324
+ await registerWindowsProjectMutationTracker(tracker, [{ directory: root, recursive: true }], false, filesystem, reportsMembership, (_location, filename) => isPossibleProgramFileName(path.basename(filename), policy), reportsNewMembership);
3188
3325
  return tracker;
3189
3326
  }
3190
3327
  const watchers = [];
3191
- tracker.close = () => closeDirectoryWatches(watchers);
3192
- for (const directory of directories) {
3193
- try {
3194
- watchers.push(openDirectoryWatch(filesystem, directory.path, (eventType, filename) => {
3195
- if (eventType !== "rename") {
3196
- return;
3197
- }
3198
- if (filename !== null &&
3199
- !reportsProgramMembership(path.join(directory.path, filename), filename, policy, filesystem)) {
3200
- return;
3201
- }
3202
- recordProjectMutation(tracker, filename === null
3203
- ? directory.path
3204
- : path.join(directory.path, filename));
3205
- }, () => {
3206
- tracker.failed = true;
3207
- }));
3208
- }
3209
- catch {
3328
+ tracker.close = () => {
3329
+ tracker.failed = true;
3330
+ closeDirectoryWatches(watchers);
3331
+ };
3332
+ try {
3333
+ watchers.push(openDirectoryWatch(filesystem, root, (eventType, filename) => {
3334
+ const changed = filename === null ? root : path.join(root, filename);
3335
+ const membership = filename === null || reportsMembership(root, filename);
3336
+ if (membership &&
3337
+ (eventType === "rename" ||
3338
+ reportsNewMembership(root, filename ?? ""))) {
3339
+ recordProjectMutation(tracker, changed);
3340
+ }
3341
+ else if (filename === null ||
3342
+ isPossibleProgramFileName(path.basename(filename), policy)) {
3343
+ recordProjectChange(tracker, changed);
3344
+ }
3345
+ }, () => {
3210
3346
  tracker.failed = true;
3211
- }
3347
+ }, true));
3348
+ }
3349
+ catch {
3350
+ tracker.failed = true;
3212
3351
  }
3213
3352
  return tracker;
3214
3353
  }
3354
+ /** Common ancestor owned by every project directory snapshot. */
3355
+ function commonDirectoryRoot(directories) {
3356
+ if (directories.length === 0)
3357
+ return undefined;
3358
+ let root = path.resolve(directories[0]);
3359
+ for (const directory of directories.slice(1)) {
3360
+ const absolute = path.resolve(directory);
3361
+ while (!pathIsWithin(absolute, root)) {
3362
+ const parent = path.dirname(root);
3363
+ if (parent === root)
3364
+ return root;
3365
+ root = parent;
3366
+ }
3367
+ }
3368
+ return root;
3369
+ }
3215
3370
  /** Watch exact universal inputs, or their nearest existing parent if missing. */
3216
- async function createHostInputMutationTracker(inputs, filesystem, covered, events = "all") {
3371
+ async function createHostInputMutationTracker(inputs, filesystem, covered, events = "all", preferredRoot) {
3217
3372
  const identities = createHostPathIdentityContext(filesystem);
3218
- const namesByDirectory = new Map();
3373
+ const linkedAncestors = new Map();
3374
+ const authoritative = new Set([...covered]
3375
+ .map((input) => path.resolve(input))
3376
+ .filter((input) => {
3377
+ if (pathTraversesSymbolicLink(input, filesystem, linkedAncestors)) {
3378
+ return false;
3379
+ }
3380
+ try {
3381
+ // A lexical symlink can keep its own directory silent while a target
3382
+ // elsewhere changes or appears. Its joined metadata proof must stay
3383
+ // on the validation path, including while the link is broken.
3384
+ const lexical = filesystem.lstat(input);
3385
+ if (lexical.isSymbolicLink() ||
3386
+ (lexical.isFile() && lexical.nlink > 1n))
3387
+ return false;
3388
+ }
3389
+ catch {
3390
+ // A genuinely absent lexical path is covered by its nearest existing
3391
+ // ancestor and remains eligible for notification proof.
3392
+ }
3393
+ const target = hostInputRealpath(input, filesystem);
3394
+ return (target === null ||
3395
+ pathIdentityKey(target, identities) ===
3396
+ pathIdentityKey(input, identities));
3397
+ }));
3398
+ const locationsByDirectory = new Map();
3399
+ const internalRoot = preferredRoot === undefined ? undefined : path.resolve(preferredRoot);
3219
3400
  for (const input of inputs) {
3220
3401
  const absolute = path.resolve(input);
3221
3402
  const probe = filesystem.exists(absolute)
3222
3403
  ? { directory: path.dirname(absolute), name: path.basename(absolute) }
3223
3404
  : missingPathProbe(absolute, filesystem);
3224
- const directoryIdentity = identities.resolve(probe.directory);
3225
- let location = namesByDirectory.get(directoryIdentity.key);
3405
+ const internal = internalRoot !== undefined && pathIsWithin(absolute, internalRoot);
3406
+ const directory = internal ? internalRoot : probe.directory;
3407
+ const directoryIdentity = identities.resolve(directory);
3408
+ let location = locationsByDirectory.get(directoryIdentity.key);
3226
3409
  if (location === undefined) {
3227
3410
  location = {
3228
3411
  directory: directoryIdentity.path,
3229
3412
  names: new Set(),
3413
+ ...(internal ? { paths: new Set(), recursive: true } : {}),
3230
3414
  };
3231
- namesByDirectory.set(directoryIdentity.key, location);
3415
+ locationsByDirectory.set(directoryIdentity.key, location);
3416
+ }
3417
+ if (location.paths !== undefined) {
3418
+ location.paths.add(pathIdentityKey(path.resolve(probe.directory, probe.name), identities));
3419
+ }
3420
+ else {
3421
+ location.names.add(normalizeHostInputName(probe.name, identities.caseSensitive(directoryIdentity.path)));
3232
3422
  }
3233
- location.names.add(normalizeHostInputName(probe.name, identities.caseSensitive(directoryIdentity.path)));
3234
3423
  }
3235
- const locations = [...namesByDirectory.values()].map((location) => ({
3236
- directory: location.directory,
3237
- names: [...location.names],
3238
- }));
3424
+ const locations = [...locationsByDirectory.values()];
3239
3425
  const tracker = {
3240
3426
  changes: new Set(),
3241
3427
  changesOmitted: false,
3242
- close: () => undefined,
3428
+ close: () => {
3429
+ tracker.failed = true;
3430
+ },
3243
3431
  // Coverage is the caller's claim, and it is required rather than derived
3244
3432
  // from the input list: an input is watched by its exact name here, but only
3245
3433
  // the caller knows whether the path leading to it is watched as well, which
3246
3434
  // is what a later validation needs before it trusts the watcher instead of
3247
3435
  // probing the path again. Deriving it here would hand that claim to every
3248
3436
  // future caller by default (samchon/ttsc#1261).
3249
- covered,
3437
+ covered: authoritative,
3438
+ contentAuthoritative: filesystem.watch === undefined,
3250
3439
  failed: false,
3251
3440
  membershipChanged: false,
3441
+ overlaps: (input, changed) => identities.isWithin(input, changed) ||
3442
+ identities.isWithin(changed, input),
3443
+ };
3444
+ if (locations.length > MAX_HOST_INPUT_WATCH_SCOPES) {
3445
+ // A graph spread over unrelated external roots cannot be folded into one
3446
+ // recursive observer without watching an arbitrarily broad filesystem
3447
+ // ancestor. Decline the notification proof and use the recorded snapshots;
3448
+ // descriptor count must never scale with an adversarial input graph.
3449
+ tracker.failed = true;
3450
+ return tracker;
3451
+ }
3452
+ const matches = (directory, filename) => {
3453
+ const location = locationsByDirectory.get(identities.resolve(directory).key);
3454
+ if (location === undefined)
3455
+ return false;
3456
+ if (location.paths !== undefined) {
3457
+ let changed = path.resolve(directory, filename);
3458
+ for (;;) {
3459
+ if (location.paths.has(pathIdentityKey(changed, identities))) {
3460
+ return true;
3461
+ }
3462
+ const parent = path.dirname(changed);
3463
+ if (parent === changed || !pathIsWithin(parent, location.directory)) {
3464
+ return false;
3465
+ }
3466
+ changed = parent;
3467
+ }
3468
+ }
3469
+ return location.names.has(normalizeHostInputName(filename, identities.caseSensitive(directory)));
3252
3470
  };
3253
3471
  if (process.platform === "win32" && filesystem.watch === undefined) {
3254
- await registerWindowsProjectMutationTracker(tracker, locations, events === "all", filesystem);
3472
+ await registerWindowsProjectMutationTracker(tracker, locations.map((location) => ({
3473
+ directory: location.directory,
3474
+ ...(location.recursive === true
3475
+ ? { recursive: true }
3476
+ : { names: [...location.names] }),
3477
+ })), events === "all", filesystem, matches, matches);
3255
3478
  return tracker;
3256
3479
  }
3257
3480
  const watchers = [];
3258
- tracker.close = () => closeDirectoryWatches(watchers);
3481
+ tracker.close = () => {
3482
+ tracker.failed = true;
3483
+ closeDirectoryWatches(watchers);
3484
+ };
3259
3485
  for (const location of locations) {
3260
3486
  try {
3261
- const names = new Set(location.names);
3262
- const caseSensitive = identities.caseSensitive(location.directory);
3263
3487
  watchers.push(openDirectoryWatch(filesystem, location.directory, (eventType, filename) => {
3264
3488
  if (events === "rename" && eventType !== "rename") {
3265
3489
  return;
3266
3490
  }
3267
- const reported = filename === null
3268
- ? null
3269
- : normalizeHostInputName(filename, caseSensitive);
3270
- if (reported === null || names.has(reported)) {
3271
- recordProjectMutation(tracker, filename === null
3491
+ if (filename === null || matches(location.directory, filename)) {
3492
+ const changed = filename === null
3272
3493
  ? location.directory
3273
- : path.join(location.directory, filename));
3494
+ : path.join(location.directory, filename);
3495
+ if (eventType === "rename") {
3496
+ recordProjectMutation(tracker, changed);
3497
+ }
3498
+ else {
3499
+ recordProjectChange(tracker, changed);
3500
+ }
3274
3501
  }
3275
3502
  }, () => {
3276
3503
  tracker.failed = true;
3277
- }));
3504
+ }, location.recursive === true));
3278
3505
  }
3279
3506
  catch {
3280
3507
  tracker.failed = true;
@@ -3282,6 +3509,45 @@ async function createHostInputMutationTracker(inputs, filesystem, covered, event
3282
3509
  }
3283
3510
  return tracker;
3284
3511
  }
3512
+ /**
3513
+ * Whether any lexical component above an input is a symbolic link or junction.
3514
+ *
3515
+ * A recursive observer follows the component to its current physical target.
3516
+ * Retargeting it can therefore move the input without producing an event on the
3517
+ * old target. Keep those spellings on metadata/realpath validation. The memo
3518
+ * makes this one lstat per distinct directory component for the generation,
3519
+ * rather than one walk per graph input.
3520
+ */
3521
+ function pathTraversesSymbolicLink(input, filesystem, memo) {
3522
+ const visited = [];
3523
+ let current = path.dirname(path.resolve(input));
3524
+ let linked = false;
3525
+ for (;;) {
3526
+ const known = memo.get(current);
3527
+ if (known !== undefined) {
3528
+ linked = known;
3529
+ break;
3530
+ }
3531
+ visited.push(current);
3532
+ try {
3533
+ if (filesystem.lstat(current).isSymbolicLink()) {
3534
+ linked = true;
3535
+ break;
3536
+ }
3537
+ }
3538
+ catch {
3539
+ // A missing component is not a link. Its nearest existing ancestor will
3540
+ // still be inspected before the walk reaches the volume root.
3541
+ }
3542
+ const parent = path.dirname(current);
3543
+ if (parent === current)
3544
+ break;
3545
+ current = parent;
3546
+ }
3547
+ for (const directory of visited)
3548
+ memo.set(directory, linked);
3549
+ return linked;
3550
+ }
3285
3551
  /**
3286
3552
  * Whether a path lies inside a directory the configuration excludes.
3287
3553
  *
@@ -3357,6 +3623,10 @@ function reportsProgramMembership(location, filename, policy, filesystem) {
3357
3623
  /** Record enough exact mutation evidence without retaining an event stream. */
3358
3624
  function recordProjectMutation(tracker, changed) {
3359
3625
  tracker.membershipChanged = true;
3626
+ recordProjectChange(tracker, changed);
3627
+ }
3628
+ /** Record a content event without classifying it as a membership change. */
3629
+ function recordProjectChange(tracker, changed) {
3360
3630
  if (tracker.changes.has(changed))
3361
3631
  return;
3362
3632
  if (tracker.changes.size < MAX_GENERATION_MUTATION_PATHS) {
@@ -3380,7 +3650,7 @@ async function registerWindowsProjectMutationTracker(tracker, locations, allEven
3380
3650
  * narrowed to program membership exactly as the in-process watcher's are. The
3381
3651
  * name-watching trackers pass none, since they already watch exact names.
3382
3652
  */
3383
- membership) {
3653
+ membership, content, changeAddsMembership) {
3384
3654
  const broker = getWindowsProjectMutationBroker();
3385
3655
  // The child watches canonical directories, and reports its events under that
3386
3656
  // spelling. Everything else in the adapter speaks the walk's own spelling,
@@ -3401,6 +3671,7 @@ membership) {
3401
3671
  return {
3402
3672
  directory,
3403
3673
  ...(location.names === undefined ? {} : { names: location.names }),
3674
+ ...(location.recursive === true ? { recursive: true } : {}),
3404
3675
  };
3405
3676
  });
3406
3677
  broker.pendingRegistrations += 1;
@@ -3414,13 +3685,16 @@ membership) {
3414
3685
  resolveReady = resolve;
3415
3686
  });
3416
3687
  broker.trackers.set(id, {
3417
- membership,
3688
+ ...(changeAddsMembership === undefined ? {} : { changeAddsMembership }),
3689
+ ...(content === undefined ? {} : { content }),
3690
+ ...(membership === undefined ? {} : { membership }),
3418
3691
  ready: resolveReady,
3419
3692
  spellings,
3420
3693
  tracker,
3421
3694
  });
3422
3695
  tracker.drain = () => drainWindowsProjectMutationBroker(broker);
3423
3696
  tracker.close = () => {
3697
+ tracker.failed = true;
3424
3698
  const active = broker.trackers.get(id);
3425
3699
  if (active === undefined)
3426
3700
  return;
@@ -3542,14 +3816,26 @@ function getWindowsProjectMutationBroker() {
3542
3816
  // The walk's spelling for this directory, which is what every
3543
3817
  // comparison and every recorded witness downstream expects.
3544
3818
  const reported = registration.spellings.get(record.directory) ?? record.directory;
3819
+ const changed = typeof record.filename === "string"
3820
+ ? path.join(reported, record.filename)
3821
+ : reported;
3545
3822
  if (typeof record.filename === "string" &&
3546
- registration.membership !== undefined &&
3547
- !registration.membership(reported, record.filename)) {
3823
+ registration.membership !== undefined) {
3824
+ const membership = registration.membership(reported, record.filename);
3825
+ if (membership &&
3826
+ (record.eventType === "rename" ||
3827
+ registration.changeAddsMembership?.(reported, record.filename) ===
3828
+ true)) {
3829
+ recordProjectMutation(registration.tracker, changed);
3830
+ return;
3831
+ }
3832
+ if (record.eventType !== "rename" &&
3833
+ registration.content?.(reported, record.filename) === true) {
3834
+ recordProjectChange(registration.tracker, changed);
3835
+ }
3548
3836
  return;
3549
3837
  }
3550
- recordProjectMutation(registration.tracker, typeof record.filename === "string"
3551
- ? path.join(reported, record.filename)
3552
- : reported);
3838
+ recordProjectMutation(registration.tracker, changed);
3553
3839
  }
3554
3840
  else {
3555
3841
  registration.tracker.membershipChanged = true;
@@ -3635,9 +3921,9 @@ const WINDOWS_WATCH_BROKER_SOURCE = [
3635
3921
  " for (const location of message.locations) {",
3636
3922
  " try {",
3637
3923
  " const names = location.names === undefined ? undefined : new Set(location.names.map((name) => name.toLowerCase()));",
3638
- " const watcher = fs.watch(location.directory, { persistent: false }, (event, filename) => {",
3924
+ " const watcher = fs.watch(location.directory, { persistent: false, recursive: location.recursive === true }, (event, filename) => {",
3639
3925
  " const matches = names === undefined || filename === null || names.has(String(filename).toLowerCase());",
3640
- ' if (matches && (message.allEvents || event === "rename")) process.send?.({ directory: location.directory, filename: filename === null ? null : String(filename), id: message.id });',
3926
+ ' if (matches && (message.allEvents || event === "rename" || location.recursive === true)) process.send?.({ directory: location.directory, eventType: event, filename: filename === null ? null : String(filename), id: message.id });',
3641
3927
  " });",
3642
3928
  ' watcher.on("error", () => process.send?.({ failed: true, id: message.id }));',
3643
3929
  " watchers.push(watcher);",
@@ -3658,14 +3944,18 @@ const WINDOWS_WATCH_BROKER_SOURCE = [
3658
3944
  "}",
3659
3945
  ].join("\n");
3660
3946
  /**
3661
- * Report whether either live notification observed a membership event. This is
3662
- * positive evidence that the generation is stale, so it outranks the question
3663
- * of whether the notifications still work.
3947
+ * Report whether the project walk observed a membership event. This is positive
3948
+ * evidence that the program's root set changed, so it outranks the question of
3949
+ * whether the notifications still work.
3950
+ *
3951
+ * Host and resolution-candidate trackers cover the union of every module's
3952
+ * inputs. Their events remain path witnesses: the requested module's narrow
3953
+ * validation decides whether that path is relevant. Promoting one of them to a
3954
+ * project-wide verdict would discard a generation when an unreachable external
3955
+ * input changes, defeating per-file completeness and doing needless compiles.
3664
3956
  */
3665
3957
  function reportsMembershipChange(cached) {
3666
- return (cached.projectMutationTracker?.membershipChanged === true ||
3667
- cached.hostInputMutationTracker?.membershipChanged === true ||
3668
- cached.candidateMutationTracker?.membershipChanged === true);
3958
+ return cached.projectMutationTracker?.membershipChanged === true;
3669
3959
  }
3670
3960
  /**
3671
3961
  * Report whether the live notifications can still prove membership. A watcher
@@ -3707,11 +3997,15 @@ function drainOnNextTurn() {
3707
3997
  * broker. Concurrent sibling deliveries share the barrier one of them started.
3708
3998
  */
3709
3999
  async function settleProjectMutationEvents(cached) {
3710
- const trackers = [
4000
+ await settleMutationTrackers([
3711
4001
  cached.projectMutationTracker,
3712
4002
  cached.hostInputMutationTracker,
3713
4003
  cached.candidateMutationTracker,
3714
- ].filter((tracker) => tracker !== undefined);
4004
+ ]);
4005
+ }
4006
+ /** Settle one set of optional trackers through one shared barrier each. */
4007
+ async function settleMutationTrackers(candidates) {
4008
+ const trackers = candidates.filter((tracker) => tracker !== undefined);
3715
4009
  await Promise.all(trackers.map(async (tracker) => {
3716
4010
  tracker.settle ??= (tracker.drain ?? drainOnNextTurn)().finally(() => {
3717
4011
  tracker.settle = undefined;
@@ -4017,9 +4311,21 @@ function matchesCachedExternalInputs(cached) {
4017
4311
  const spelling = path.resolve(file);
4018
4312
  const observation = cached.externalInputObservations?.[spelling];
4019
4313
  if (observation !== undefined) {
4020
- if (!matchesGraphInputObservation(file, observation, filesystem, state.identityContext)) {
4314
+ const before = inputMetadataEvidence(file, filesystem);
4315
+ if (before !== undefined &&
4316
+ before.separable &&
4317
+ recordedSignatures[spelling] === before.signature) {
4318
+ signatures[spelling] = before.signature;
4319
+ continue;
4320
+ }
4321
+ const observed = matchesGraphInputObservation(file, observation, filesystem, state.identityContext);
4322
+ const after = inputMetadataSignature(file, filesystem);
4323
+ if (!observed) {
4021
4324
  matches = false;
4022
4325
  }
4326
+ else if (before?.separable === true && before.signature === after) {
4327
+ signatures[spelling] = after;
4328
+ }
4023
4329
  continue;
4024
4330
  }
4025
4331
  // Reuse the recorded hash of an out-of-walk input whose signature still
@@ -4517,6 +4823,7 @@ function declaredProjectInputKeys(state, cached) {
4517
4823
  if (state.declaredInputKeysBuilt !== true) {
4518
4824
  state.declaredInputKeys = selectDeclaredProjectInputKeys({
4519
4825
  identities: state.identityContext,
4826
+ projectInputHashes: cached.inputHashes,
4520
4827
  projectRoot: cached.projectRoot,
4521
4828
  result: cached.result,
4522
4829
  scratchDirectory: cached.scratchDirectory,
@@ -4528,8 +4835,12 @@ function declaredProjectInputKeys(state, cached) {
4528
4835
  /**
4529
4836
  * Project-walk keys of every input the envelope declares: the reference graph's
4530
4837
  * edge endpoints, globals, config chain, and resolution candidates, plus the
4531
- * universal host inputs. Returns `undefined` for an envelope with no graph,
4532
- * which declares no input set and therefore keeps whole-walk comparison.
4838
+ * universal host inputs, intersected with the files the project walk actually
4839
+ * hashed. Out-of-walk declarations and candidates carry their own graph proof;
4840
+ * asking the project observer to witness them as well makes unrelated activity
4841
+ * in ignored directories invalidate an otherwise complete generation. Returns
4842
+ * `undefined` for an envelope with no graph, which declares no input set and
4843
+ * therefore keeps whole-walk comparison.
4533
4844
  */
4534
4845
  function selectDeclaredProjectInputKeys(props) {
4535
4846
  if (props.result.type === "exception" || props.result.graph === undefined) {
@@ -4543,7 +4854,10 @@ function selectDeclaredProjectInputKeys(props) {
4543
4854
  const absolute = path.resolve(props.projectRoot, entry);
4544
4855
  if (isTransformScratchInput(absolute, props.scratchDirectory))
4545
4856
  return;
4546
- keys.add(toProjectKey(props.projectRoot, absolute, props.identities));
4857
+ const key = toProjectKey(props.projectRoot, absolute, props.identities);
4858
+ if (Object.prototype.hasOwnProperty.call(props.projectInputHashes, key)) {
4859
+ keys.add(key);
4860
+ }
4547
4861
  };
4548
4862
  for (const [source, targets] of Object.entries(graph.edges ?? {})) {
4549
4863
  add(source);
@@ -4787,8 +5101,7 @@ async function transformProject(props) {
4787
5101
  for (let attempt = 0; attempt < TRANSFORM_GENERATION_ATTEMPTS; attempt += 1) {
4788
5102
  const cached = await captureTransformGeneration(props);
4789
5103
  if (cached.configStateComplete !== false &&
4790
- (!props.trackProjectMembership ||
4791
- cached.result.type !== "success" ||
5104
+ (cached.result.type !== "success" ||
4792
5105
  cached.projectSnapshotComplete === true)) {
4793
5106
  return cached;
4794
5107
  }
@@ -4820,7 +5133,7 @@ async function captureTransformGeneration(props) {
4820
5133
  let retainCandidateTracker = false;
4821
5134
  let captured;
4822
5135
  try {
4823
- if (props.trackProjectMembership) {
5136
+ if (props.retainProjectMembership) {
4824
5137
  try {
4825
5138
  clockReferenceDirectory = createTransformScratchDirectory(projectRoot, props.filesystem);
4826
5139
  }
@@ -4849,7 +5162,9 @@ async function captureTransformGeneration(props) {
4849
5162
  const membershipPolicy = mergeMembershipPolicyOverlay(tsconfigState.membershipPolicy, props.compilerOptions, projectRoot);
4850
5163
  const before = collectProjectInputSnapshot(projectRoot, identities, props.filesystem, undefined, { policy: membershipPolicy });
4851
5164
  tracker = props.trackProjectMembership
4852
- ? await createProjectMutationTracker(before.projectDirectories, props.filesystem, membershipPolicy)
5165
+ ? await createProjectMutationTracker(before.projectDirectories, new Set(Object.keys(before.hashes)
5166
+ .filter((key) => !before.notificationUnsafeInputs.has(key))
5167
+ .map((key) => path.resolve(projectRoot, key))), props.filesystem, membershipPolicy)
4853
5168
  : undefined;
4854
5169
  const result = withTransformScratchEnvironment(scratchDirectory, () => new TtscCompiler({
4855
5170
  cwd: projectRoot,
@@ -4880,15 +5195,26 @@ async function captureTransformGeneration(props) {
4880
5195
  scratchDirectory,
4881
5196
  temporaryTsconfig,
4882
5197
  });
5198
+ const externalInputPaths = selectExternalInputPaths({
5199
+ filesystem: props.filesystem,
5200
+ membershipPolicy,
5201
+ projectRoot,
5202
+ result,
5203
+ scratchDirectory,
5204
+ temporaryTsconfig,
5205
+ });
5206
+ const persistentValidationInputs = [
5207
+ ...new Set([...persistentHostInputs, ...externalInputPaths]),
5208
+ ];
4883
5209
  // The generation's absent resolution candidates, which get a watcher of
4884
5210
  // their own below; watching one is what lets a delivery stop probing it
4885
5211
  // (samchon/ttsc#1261). The validation manifest stays built from the
4886
5212
  // universal inputs alone, so nothing else about a candidate changes.
4887
5213
  //
4888
- // Derived only where a tracker could carry it: a build-scoped adapter opens
4889
- // no watcher, so probing every candidate's existence here would be work
4890
- // whose answer nothing can read.
4891
- const notifiableAbsence = props.trackProjectMembership
5214
+ // Derived only where a retained tracker could carry it: a build-scoped
5215
+ // adapter keeps only the compile-time project observer, so probing every
5216
+ // candidate here would be work whose answer nothing can later read.
5217
+ const notifiableAbsence = props.retainProjectMembership
4892
5218
  ? selectNotifiableAbsentInputs({
4893
5219
  filesystem: props.filesystem,
4894
5220
  projectRoot,
@@ -4897,12 +5223,12 @@ async function captureTransformGeneration(props) {
4897
5223
  temporaryTsconfig,
4898
5224
  })
4899
5225
  : { candidates: [], watched: [] };
4900
- hostInputTracker = props.trackProjectMembership
4901
- ? await createHostInputMutationTracker(persistentHostInputs, props.filesystem,
5226
+ hostInputTracker = props.retainProjectMembership
5227
+ ? await createHostInputMutationTracker(persistentValidationInputs, props.filesystem,
4902
5228
  // A universal input never reaches the per-input loop that consults a
4903
5229
  // coverage claim: an absent one is proven by its directory listing
4904
5230
  // instead, which re-resolves the spelling every delivery.
4905
- new Set())
5231
+ new Set(persistentValidationInputs.map((input) => path.resolve(input))), "all", projectRoot)
4906
5232
  : undefined;
4907
5233
  // The candidates and the directories carrying them get their own tracker,
4908
5234
  // listening for renames alone. Every event that can make one of these
@@ -4913,16 +5239,8 @@ async function captureTransformGeneration(props) {
4913
5239
  // every time a bundler wrote inside `node_modules`.
4914
5240
  candidateTracker =
4915
5241
  notifiableAbsence.watched.length !== 0
4916
- ? await createHostInputMutationTracker(notifiableAbsence.watched, props.filesystem, new Set(notifiableAbsence.candidates), "rename")
5242
+ ? await createHostInputMutationTracker(notifiableAbsence.watched, props.filesystem, new Set(notifiableAbsence.candidates), "rename", projectRoot)
4917
5243
  : undefined;
4918
- const externalInputPaths = selectExternalInputPaths({
4919
- filesystem: props.filesystem,
4920
- membershipPolicy,
4921
- projectRoot,
4922
- result,
4923
- scratchDirectory,
4924
- temporaryTsconfig,
4925
- });
4926
5244
  const inputSnapshot = collectProjectInputSnapshot(projectRoot, identities, props.filesystem, undefined, { policy: membershipPolicy });
4927
5245
  // Whether the recorded snapshot describes one coherent state of the
4928
5246
  // project. A membership event during the compile taints it exactly like an
@@ -4931,16 +5249,23 @@ async function captureTransformGeneration(props) {
4931
5249
  // provable from its own recorded state.
4932
5250
  const declaredInputs = selectDeclaredProjectInputKeys({
4933
5251
  identities,
5252
+ projectInputHashes: inputSnapshot.hashes,
4934
5253
  projectRoot,
4935
5254
  result,
4936
5255
  scratchDirectory,
4937
5256
  });
5257
+ // The before/after snapshots prove bytes and metadata. Drain the watcher
5258
+ // opened before compilation as the independent A-B-A witness: a producer
5259
+ // can restore both bytes and timestamps before the second walk, but it
5260
+ // cannot withdraw the already queued content event.
5261
+ await settleMutationTrackers([tracker, hostInputTracker, candidateTracker]);
4938
5262
  const walkStable = configStable &&
4939
5263
  walkSnapshotComplete(before, declaredInputs) &&
4940
5264
  walkSnapshotComplete(inputSnapshot, declaredInputs) &&
4941
5265
  sameHashes(before.hashes, inputSnapshot.hashes, declaredInputs) &&
4942
5266
  sameHashes(before.fileSignatures, inputSnapshot.fileSignatures, declaredInputs) &&
4943
5267
  sameProjectDirectories(before.projectDirectories, inputSnapshot.projectDirectories) &&
5268
+ !trackerChangedDeclaredProjectInput(tracker, declaredInputs, projectRoot) &&
4944
5269
  tracker?.membershipChanged !== true &&
4945
5270
  hostInputTracker?.membershipChanged !== true &&
4946
5271
  candidateTracker?.membershipChanged !== true;
@@ -5022,6 +5347,7 @@ async function captureTransformGeneration(props) {
5022
5347
  mergeGenerationProofFailures(failures, graphFailures);
5023
5348
  mergeGenerationProofFailures(failures, externalInputSnapshot.failures);
5024
5349
  const universalInputCapture = captureUniversalHostInputValidation(cached, props.currentFile);
5350
+ restrictNotificationCoverageToProvenInputs(hostInputTracker, cached, universalInputCapture.validation);
5025
5351
  mergeGenerationProofFailures(failures, universalInputCapture.failures);
5026
5352
  const graphProofs = graphFailures.entries.length === 0 && graphFailures.omitted === 0;
5027
5353
  const universalInputs = universalInputCapture.validation !== undefined;
@@ -5044,7 +5370,9 @@ async function captureTransformGeneration(props) {
5044
5370
  // Attach notifications only while they can actually prove membership. A
5045
5371
  // generation that could not open its watchers keeps its recorded snapshot
5046
5372
  // and validates through it, rather than losing the cache entirely.
5047
- const notifying = stableProjectSnapshot && notificationsAvailable;
5373
+ const notifying = props.retainProjectMembership &&
5374
+ stableProjectSnapshot &&
5375
+ notificationsAvailable;
5048
5376
  if (notifying && tracker !== undefined) {
5049
5377
  cached.projectMutationTracker = tracker;
5050
5378
  }
@@ -5136,6 +5464,25 @@ async function captureTransformGeneration(props) {
5136
5464
  }
5137
5465
  return captured;
5138
5466
  }
5467
+ /** Whether a compile-time content event overlaps any declared project input. */
5468
+ function trackerChangedDeclaredProjectInput(tracker, declared, projectRoot) {
5469
+ if (tracker === undefined)
5470
+ return false;
5471
+ if (tracker.changesOmitted)
5472
+ return true;
5473
+ if (tracker.changes.size === 0)
5474
+ return false;
5475
+ if (declared === undefined)
5476
+ return true;
5477
+ const inputs = [...declared].map((input) => path.resolve(projectRoot, input));
5478
+ for (const changed of tracker.changes) {
5479
+ if (inputs.some((input) => tracker.overlaps?.(input, changed) ??
5480
+ (pathIsWithin(input, changed) || pathIsWithin(changed, input)))) {
5481
+ return true;
5482
+ }
5483
+ }
5484
+ return false;
5485
+ }
5139
5486
  /** Exclude disposed transform scratch from live host-input tracking. */
5140
5487
  function selectPersistentHostInputs(props) {
5141
5488
  if (props.result.type === "exception")