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