@ttsc/unplugin 0.31.3 → 0.31.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/core/transform/session/claimSharedCompile.js +6 -4
- package/lib/core/transform/session/claimSharedCompile.js.map +1 -1
- package/lib/core/transform/session/claimSharedCompile.mjs +6 -4
- package/lib/core/transform/session/claimSharedCompile.mjs.map +1 -1
- package/lib/core/transform/session/openTtscTransformSession.d.cts +1 -1
- package/lib/core/transform/session/openTtscTransformSession.d.mts +1 -1
- package/lib/core/transform/session/openTtscTransformSession.d.ts +1 -1
- package/lib/core/transform/session/openTtscTransformSession.js +7 -3
- package/lib/core/transform/session/openTtscTransformSession.js.map +1 -1
- package/lib/core/transform/session/openTtscTransformSession.mjs +7 -3
- package/lib/core/transform/session/openTtscTransformSession.mjs.map +1 -1
- package/lib/core/transform/tracker/broker/sweepAbandonedWatchProbes.d.cts +6 -6
- package/lib/core/transform/tracker/broker/sweepAbandonedWatchProbes.d.mts +6 -6
- package/lib/core/transform/tracker/broker/sweepAbandonedWatchProbes.d.ts +6 -6
- package/lib/core/transform/tracker/broker/sweepAbandonedWatchProbes.js +9 -7
- package/lib/core/transform/tracker/broker/sweepAbandonedWatchProbes.js.map +1 -1
- package/lib/core/transform/tracker/broker/sweepAbandonedWatchProbes.mjs +9 -7
- package/lib/core/transform/tracker/broker/sweepAbandonedWatchProbes.mjs.map +1 -1
- package/lib/core/transform/tracker/broker/watchBrokerSource.js +14 -0
- package/lib/core/transform/tracker/broker/watchBrokerSource.js.map +1 -1
- package/lib/core/transform/tracker/broker/watchBrokerSource.mjs +14 -0
- package/lib/core/transform/tracker/broker/watchBrokerSource.mjs.map +1 -1
- package/lib/core/transform/tracker/createHostInputMutationTracker.js +20 -0
- package/lib/core/transform/tracker/createHostInputMutationTracker.js.map +1 -1
- package/lib/core/transform/tracker/createHostInputMutationTracker.mjs +20 -0
- package/lib/core/transform/tracker/createHostInputMutationTracker.mjs.map +1 -1
- package/lib/core/transform/tracker/createProjectMutationTracker.d.cts +6 -5
- package/lib/core/transform/tracker/createProjectMutationTracker.d.mts +6 -5
- package/lib/core/transform/tracker/createProjectMutationTracker.d.ts +6 -5
- package/lib/core/transform/tracker/createProjectMutationTracker.js +57 -36
- package/lib/core/transform/tracker/createProjectMutationTracker.js.map +1 -1
- package/lib/core/transform/tracker/createProjectMutationTracker.mjs +57 -36
- package/lib/core/transform/tracker/createProjectMutationTracker.mjs.map +1 -1
- package/lib/core/transform/tracker/settleProjectMutationEvents.d.cts +23 -29
- package/lib/core/transform/tracker/settleProjectMutationEvents.d.mts +23 -29
- package/lib/core/transform/tracker/settleProjectMutationEvents.d.ts +23 -29
- package/lib/core/transform/tracker/settleProjectMutationEvents.js +25 -37
- package/lib/core/transform/tracker/settleProjectMutationEvents.js.map +1 -1
- package/lib/core/transform/tracker/settleProjectMutationEvents.mjs +25 -37
- package/lib/core/transform/tracker/settleProjectMutationEvents.mjs.map +1 -1
- package/lib/core/transform/tracker/verifyGenerationLocations.d.cts +41 -0
- package/lib/core/transform/tracker/verifyGenerationLocations.d.mts +41 -0
- package/lib/core/transform/tracker/verifyGenerationLocations.d.ts +41 -0
- package/lib/core/transform/tracker/verifyGenerationLocations.js +54 -0
- package/lib/core/transform/tracker/verifyGenerationLocations.js.map +1 -0
- package/lib/core/transform/tracker/verifyGenerationLocations.mjs +52 -0
- package/lib/core/transform/tracker/verifyGenerationLocations.mjs.map +1 -0
- package/lib/core/transform/utils/isZombieProcess.d.cts +26 -0
- package/lib/core/transform/utils/isZombieProcess.d.mts +26 -0
- package/lib/core/transform/utils/isZombieProcess.d.ts +26 -0
- package/lib/core/transform/utils/isZombieProcess.js +46 -0
- package/lib/core/transform/utils/isZombieProcess.js.map +1 -0
- package/lib/core/transform/utils/isZombieProcess.mjs +44 -0
- package/lib/core/transform/utils/isZombieProcess.mjs.map +1 -0
- package/lib/core/transform/validation/TtscGenerationProof.d.cts +12 -4
- package/lib/core/transform/validation/TtscGenerationProof.d.mts +12 -4
- package/lib/core/transform/validation/TtscGenerationProof.d.ts +12 -4
- package/lib/core/transform/validation/TtscGenerationProof.js +13 -4
- package/lib/core/transform/validation/TtscGenerationProof.js.map +1 -1
- package/lib/core/transform/validation/TtscGenerationProof.mjs +13 -4
- package/lib/core/transform/validation/TtscGenerationProof.mjs.map +1 -1
- package/lib/core/transform/validation/matchesCachedExternalInputs.d.cts +2 -2
- package/lib/core/transform/validation/matchesCachedExternalInputs.d.mts +2 -2
- package/lib/core/transform/validation/matchesCachedExternalInputs.d.ts +2 -2
- package/lib/core/transform/validation/matchesCachedExternalInputs.js +26 -5
- package/lib/core/transform/validation/matchesCachedExternalInputs.js.map +1 -1
- package/lib/core/transform/validation/matchesCachedExternalInputs.mjs +26 -5
- package/lib/core/transform/validation/matchesCachedExternalInputs.mjs.map +1 -1
- package/lib/core/transform/validation/matchesCompleteInputSnapshot.js +1 -1
- package/lib/core/transform/validation/matchesCompleteInputSnapshot.js.map +1 -1
- package/lib/core/transform/validation/matchesCompleteInputSnapshot.mjs +1 -1
- package/lib/core/transform/validation/matchesCompleteInputSnapshot.mjs.map +1 -1
- package/lib/core/transform/validation/matchesUniversalHostInputProbes.d.cts +15 -8
- package/lib/core/transform/validation/matchesUniversalHostInputProbes.d.mts +15 -8
- package/lib/core/transform/validation/matchesUniversalHostInputProbes.d.ts +15 -8
- package/lib/core/transform/validation/matchesUniversalHostInputProbes.js +29 -20
- package/lib/core/transform/validation/matchesUniversalHostInputProbes.js.map +1 -1
- package/lib/core/transform/validation/matchesUniversalHostInputProbes.mjs +29 -20
- package/lib/core/transform/validation/matchesUniversalHostInputProbes.mjs.map +1 -1
- package/lib/core/transform/validation/matchesUniversalHostInputs.js +1 -1
- package/lib/core/transform/validation/matchesUniversalHostInputs.js.map +1 -1
- package/lib/core/transform/validation/matchesUniversalHostInputs.mjs +1 -1
- package/lib/core/transform/validation/matchesUniversalHostInputs.mjs.map +1 -1
- package/package.json +3 -3
- package/src/core/transform/session/claimSharedCompile.ts +6 -4
- package/src/core/transform/session/openTtscTransformSession.ts +7 -3
- package/src/core/transform/tracker/broker/sweepAbandonedWatchProbes.ts +8 -7
- package/src/core/transform/tracker/broker/watchBrokerSource.ts +14 -0
- package/src/core/transform/tracker/createHostInputMutationTracker.ts +18 -0
- package/src/core/transform/tracker/createProjectMutationTracker.ts +61 -40
- package/src/core/transform/tracker/settleProjectMutationEvents.ts +25 -36
- package/src/core/transform/tracker/verifyGenerationLocations.ts +53 -0
- package/src/core/transform/utils/isZombieProcess.ts +39 -0
- package/src/core/transform/validation/TtscGenerationProof.ts +20 -4
- package/src/core/transform/validation/matchesCachedExternalInputs.ts +28 -5
- package/src/core/transform/validation/matchesCompleteInputSnapshot.ts +1 -1
- package/src/core/transform/validation/matchesUniversalHostInputProbes.ts +27 -16
- package/src/core/transform/validation/matchesUniversalHostInputs.ts +1 -1
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
+
import { isZombieProcess } from "../../utils/isZombieProcess";
|
|
4
5
|
import { WATCH_PROBE_DIRECTORY_PREFIX } from "./WATCH_PROBE_DIRECTORY_PREFIX";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Attempt removal of recognized probe-name entries below `parent` when their
|
|
8
|
-
* named process reports ESRCH.
|
|
9
|
+
* named process reports ESRCH or has exited uncollected.
|
|
9
10
|
*
|
|
10
11
|
* A process removes its own probe directory when it exits, but a killed process
|
|
11
12
|
* may not. Only a namespace entry named with a process id that the operating
|
|
12
|
-
* system reports as gone (`ESRCH`)
|
|
13
|
-
* may not signal, and a name without
|
|
14
|
-
* adapter's probe convention; this scan
|
|
15
|
-
* prove filesystem ownership, and
|
|
16
|
-
* operation.
|
|
13
|
+
* system reports as gone (`ESRCH`), or as a zombie its parent never collected,
|
|
14
|
+
* is removed: a live owner, one this process may not signal, and a name without
|
|
15
|
+
* an id are left alone. Names follow the adapter's probe convention; this scan
|
|
16
|
+
* does not separately stat entry kind or prove filesystem ownership, and
|
|
17
|
+
* liveness/deletion are not one atomic operation.
|
|
17
18
|
*
|
|
18
19
|
* @param parent The tool cache the broker is about to name its own probe
|
|
19
20
|
* directory in.
|
|
@@ -64,7 +65,7 @@ export function sweepAbandonedWatchProbes(parent: string): void {
|
|
|
64
65
|
}
|
|
65
66
|
try {
|
|
66
67
|
process.kill(owner, 0);
|
|
67
|
-
continue;
|
|
68
|
+
if (!isZombieProcess(owner)) continue;
|
|
68
69
|
} catch (error) {
|
|
69
70
|
if ((error as NodeJS.ErrnoException).code !== "ESRCH") continue;
|
|
70
71
|
}
|
|
@@ -119,6 +119,8 @@ const registrations = new Map();
|
|
|
119
119
|
const DROPPED = 0x1 | 0x2 | 0x4 | 0x8 | 0x20 | 0x40 | 0x80;
|
|
120
120
|
const MODIFIED = 0x400 | 0x1000 | 0x2000 | 0x4000 | 0x8000;
|
|
121
121
|
const RENAMED = 0x100 | 0x200 | 0x800;
|
|
122
|
+
const CREATED = 0x100;
|
|
123
|
+
const MOVED = 0x200 | 0x800;
|
|
122
124
|
const fsevents = load();
|
|
123
125
|
let probeSequence = 0;
|
|
124
126
|
|
|
@@ -203,6 +205,8 @@ function stream(location, deliver, registration) {
|
|
|
203
205
|
const root = probe === undefined ? directory : probe.root;
|
|
204
206
|
const within = (parent, file) => file === parent || file.startsWith(parent.endsWith("/") ? parent : parent + "/");
|
|
205
207
|
const opened = { close: undefined, location: location.directory, pending: new Map(), probe, proven: probe === undefined };
|
|
208
|
+
// Anything born before this instant was not created in this stream.
|
|
209
|
+
const openedAt = Date.now();
|
|
206
210
|
const stop = fsevents.watch(root, (file, flags) => {
|
|
207
211
|
// Loss authority is independent of the path, including our own probe.
|
|
208
212
|
if ((flags & DROPPED) !== 0) {
|
|
@@ -232,6 +236,16 @@ function stream(location, deliver, registration) {
|
|
|
232
236
|
}
|
|
233
237
|
const filename = file.slice(directory.length + 1);
|
|
234
238
|
if (location.recursive !== true && filename.includes("/")) return;
|
|
239
|
+
// FSEvents keeps ItemCreated on later writes to a path it saw created, so a
|
|
240
|
+
// plain edit of a file older than this stream arrived as a rename, a
|
|
241
|
+
// membership change that replaced generations for an unrelated write. An
|
|
242
|
+
// object born before the stream opened was not created in it; its other
|
|
243
|
+
// flags decide. A removal or a rename keeps the event structural.
|
|
244
|
+
if ((flags & CREATED) !== 0 && (flags & MOVED) === 0 && (flags & MODIFIED) !== 0) {
|
|
245
|
+
try {
|
|
246
|
+
if (fs.lstatSync(file).birthtimeMs < openedAt) flags &= ~CREATED;
|
|
247
|
+
} catch {}
|
|
248
|
+
}
|
|
235
249
|
deliver((flags & MODIFIED) !== 0 && (flags & RENAMED) === 0 ? "change" : "rename", filename);
|
|
236
250
|
});
|
|
237
251
|
opened.close = () => {
|
|
@@ -399,6 +399,17 @@ export async function createHostInputMutationTracker(
|
|
|
399
399
|
* an event the backend could not attribute to a name may concern any input
|
|
400
400
|
* below the watched directory, so it always counts.
|
|
401
401
|
*/
|
|
402
|
+
// Whether a path names an existing object that no other name can reach:
|
|
403
|
+
// not a link, and not a file with a second hard link, whose writes could
|
|
404
|
+
// land on a tracked input without an event on its name.
|
|
405
|
+
const distinctExistingObject = (location: string): boolean => {
|
|
406
|
+
try {
|
|
407
|
+
const stats = filesystem.lstat(location);
|
|
408
|
+
return !stats.isSymbolicLink() && !(stats.isFile() && stats.nlink > 1n);
|
|
409
|
+
} catch {
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
};
|
|
402
413
|
const classify = (
|
|
403
414
|
directory: string,
|
|
404
415
|
filename: string | null,
|
|
@@ -477,6 +488,13 @@ export async function createHostInputMutationTracker(
|
|
|
477
488
|
return "mutation";
|
|
478
489
|
}
|
|
479
490
|
}
|
|
491
|
+
// An object that exists now resolves to its own identity under every alias,
|
|
492
|
+
// so one that is neither a link nor a second name of a file, and resolves
|
|
493
|
+
// to nothing tracked above, is a different object. A recursive backend
|
|
494
|
+
// (macOS, Windows) hears every write below the project root, and
|
|
495
|
+
// withdrawing authority for each sent every later delivery past its
|
|
496
|
+
// notifications.
|
|
497
|
+
if (distinctExistingObject(changed)) return undefined;
|
|
480
498
|
// A missing native alias cannot be recovered by current realpath. Neither
|
|
481
499
|
// ASCII spelling nor OS name proves this event unrelated to a tracked path.
|
|
482
500
|
// Keep exact metadata validation without inventing a membership mutation.
|
|
@@ -36,9 +36,10 @@ import { watchLocationIdentity } from "./watchLocationIdentity";
|
|
|
36
36
|
* recorded-state validation supplies the actual verdict. Content witnesses
|
|
37
37
|
* preserve captured native input aliases, then reject only an observed normal
|
|
38
38
|
* directory excluded in both lexical and native spellings. Unknown, missing and
|
|
39
|
-
* linked ancestry remains conservative. Membership rejection
|
|
40
|
-
* notification authority
|
|
41
|
-
*
|
|
39
|
+
* linked ancestry remains conservative. Membership rejection withdraws
|
|
40
|
+
* notification authority except below such a proven excluded directory, so a
|
|
41
|
+
* recursive backend's writes to a cache or output tree cost no proof. Named
|
|
42
|
+
* events also compare current native identity and case policy with the retained
|
|
42
43
|
* identity transaction. Retargeting withdraws notification authority without
|
|
43
44
|
* changing the meaning of earlier recorded event spellings.
|
|
44
45
|
*
|
|
@@ -48,8 +49,8 @@ import { watchLocationIdentity } from "./watchLocationIdentity";
|
|
|
48
49
|
* A lexical nonmatch is uncertainty rather than native alias exclusion.
|
|
49
50
|
* Changed native identity premises also withdraw notification proof while
|
|
50
51
|
* membership and content witnesses keep their independent classifications.
|
|
51
|
-
* Content exclusion
|
|
52
|
-
* spellings; captured native input aliases remain content witnesses.
|
|
52
|
+
* Content and membership exclusion require normal directory identity and both
|
|
53
|
+
* policy spellings; captured native input aliases remain content witnesses.
|
|
53
54
|
* @evidence contracts/common.md#clear-and-simple-design
|
|
54
55
|
* One root, directory index and filter set define the tracker; broker and local
|
|
55
56
|
* paths share those decisions rather than duplicating backend-specific policies.
|
|
@@ -145,6 +146,53 @@ export async function createProjectMutationTracker(
|
|
|
145
146
|
.filter((directory) => directory.relevant)
|
|
146
147
|
.map((directory) => paths.resolve(directory.path)),
|
|
147
148
|
);
|
|
149
|
+
// Whether an event path lies below a directory the walk excludes, proven by
|
|
150
|
+
// an observed normal directory excluded in both its lexical and its native
|
|
151
|
+
// spelling. A missing ancestor, one outside the root, or a link before that
|
|
152
|
+
// directory proves nothing. Content stops at the excluded directory, since a
|
|
153
|
+
// captured input reached through a deeper link is already caught by its
|
|
154
|
+
// identity above. Membership has no identity to catch a new file created
|
|
155
|
+
// through such a link (`node_modules/linked -> src`), so for it the whole
|
|
156
|
+
// path must be links-free.
|
|
157
|
+
const excludedFromWalk = (changed: string, wholePath: boolean): boolean => {
|
|
158
|
+
const relative = paths.relative(root, paths.dirname(changed));
|
|
159
|
+
if (
|
|
160
|
+
relative === ".." ||
|
|
161
|
+
relative.startsWith(".." + paths.sep) ||
|
|
162
|
+
paths.isAbsolute(relative)
|
|
163
|
+
)
|
|
164
|
+
return false;
|
|
165
|
+
let directory = root;
|
|
166
|
+
let excluded = false;
|
|
167
|
+
for (const component of relative.split(paths.sep).filter(Boolean)) {
|
|
168
|
+
directory = paths.join(directory, component);
|
|
169
|
+
try {
|
|
170
|
+
const before = filesystem.lstat(directory);
|
|
171
|
+
if (!before.isDirectory() || before.isSymbolicLink()) return false;
|
|
172
|
+
const physical = filesystem.realpath(directory);
|
|
173
|
+
const after = filesystem.lstat(directory);
|
|
174
|
+
if (
|
|
175
|
+
!after.isDirectory() ||
|
|
176
|
+
after.isSymbolicLink() ||
|
|
177
|
+
before.dev !== after.dev ||
|
|
178
|
+
before.ino !== after.ino
|
|
179
|
+
)
|
|
180
|
+
return false;
|
|
181
|
+
if (
|
|
182
|
+
!excluded &&
|
|
183
|
+
!isProjectWalkDirectory(directory, policy, filesystem.platform) &&
|
|
184
|
+
!isProjectWalkDirectory(physical, policy, filesystem.platform)
|
|
185
|
+
) {
|
|
186
|
+
if (!wholePath) return true;
|
|
187
|
+
excluded = true;
|
|
188
|
+
}
|
|
189
|
+
} catch {
|
|
190
|
+
// Missing/deleted ancestors and unknown native spellings remain events.
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return excluded;
|
|
195
|
+
};
|
|
148
196
|
const reportsMembership = (location: string, filename: string): boolean => {
|
|
149
197
|
const changed = paths.join(location, filename);
|
|
150
198
|
const current = createHostPathIdentityContext(filesystem);
|
|
@@ -169,8 +217,13 @@ export async function createProjectMutationTracker(
|
|
|
169
217
|
filesystem,
|
|
170
218
|
);
|
|
171
219
|
// Current realpath cannot identify a deleted alias. Policy checks only
|
|
172
|
-
// lexical spellings, so rejection cannot establish unchanged program state
|
|
173
|
-
|
|
220
|
+
// lexical spellings, so rejection cannot establish unchanged program state,
|
|
221
|
+
// except below a directory the walk provably excludes. A recursive backend
|
|
222
|
+
// (macOS, Windows) hears every write under the root, a cache or an output
|
|
223
|
+
// directory among them, and withdrawing authority for each sent every later
|
|
224
|
+
// delivery to the whole-snapshot proof.
|
|
225
|
+
if (!membership && !excludedFromWalk(changed, true))
|
|
226
|
+
tracker.unverified = true;
|
|
174
227
|
return membership;
|
|
175
228
|
};
|
|
176
229
|
// Content belongs to the walk too. A recursive native backend can report
|
|
@@ -201,39 +254,7 @@ export async function createProjectMutationTracker(
|
|
|
201
254
|
tracker.unverified = true;
|
|
202
255
|
return true;
|
|
203
256
|
}
|
|
204
|
-
|
|
205
|
-
if (
|
|
206
|
-
relative === ".." ||
|
|
207
|
-
relative.startsWith(".." + paths.sep) ||
|
|
208
|
-
paths.isAbsolute(relative)
|
|
209
|
-
)
|
|
210
|
-
return true;
|
|
211
|
-
let directory = root;
|
|
212
|
-
for (const component of relative.split(paths.sep).filter(Boolean)) {
|
|
213
|
-
directory = paths.join(directory, component);
|
|
214
|
-
try {
|
|
215
|
-
const before = filesystem.lstat(directory);
|
|
216
|
-
if (!before.isDirectory() || before.isSymbolicLink()) return true;
|
|
217
|
-
const physical = filesystem.realpath(directory);
|
|
218
|
-
const after = filesystem.lstat(directory);
|
|
219
|
-
if (
|
|
220
|
-
!after.isDirectory() ||
|
|
221
|
-
after.isSymbolicLink() ||
|
|
222
|
-
before.dev !== after.dev ||
|
|
223
|
-
before.ino !== after.ino
|
|
224
|
-
)
|
|
225
|
-
return true;
|
|
226
|
-
if (
|
|
227
|
-
!isProjectWalkDirectory(directory, policy, filesystem.platform) &&
|
|
228
|
-
!isProjectWalkDirectory(physical, policy, filesystem.platform)
|
|
229
|
-
)
|
|
230
|
-
return false;
|
|
231
|
-
} catch {
|
|
232
|
-
// Missing/deleted ancestors and unknown native spellings remain events.
|
|
233
|
-
return true;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
return true;
|
|
257
|
+
return !excludedFromWalk(changed, false);
|
|
237
258
|
};
|
|
238
259
|
const reportsNewMembership = (
|
|
239
260
|
location: string,
|
|
@@ -2,49 +2,43 @@ import type { TtscCachedProjectTransform } from "../cache/TtscCachedProjectTrans
|
|
|
2
2
|
import { settleMutationTrackers } from "./settleMutationTrackers";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Await the cached trackers' drain operations
|
|
6
|
-
*
|
|
5
|
+
* Await the cached trackers' drain operations before persistent validation
|
|
6
|
+
* reads their verdicts.
|
|
7
7
|
*
|
|
8
8
|
* A synchronous edit returns before its watch event is applied, so without this
|
|
9
9
|
* a delivery could validate against a tracker that has not been told yet. Each
|
|
10
10
|
* tracker uses its supplied native drain or the local two-immediate fallback;
|
|
11
11
|
* that fallback is a scheduling opportunity, not independent native delivery
|
|
12
|
-
* proof. Concurrent sibling deliveries share an in-flight tracker drain.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
12
|
+
* proof. Concurrent sibling deliveries share an in-flight tracker drain. Drain
|
|
13
|
+
* rejection aborts the delivery's settlement and does not cancel sibling
|
|
14
|
+
* drains.
|
|
15
|
+
*
|
|
16
|
+
* The watched directories' identities are not checked here but by the
|
|
17
|
+
* delivery's synchronous admission (`verifyGenerationLocations`), after its
|
|
18
|
+
* last await, so a directory replaced while the delivery prepared is still
|
|
19
|
+
* caught and its reading is current when a validator reuses it.
|
|
20
20
|
*
|
|
21
21
|
* @evidence contracts/common.md#principled-implementation
|
|
22
|
-
* Successful aggregate completion precedes
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* Successful aggregate completion precedes admission. Missing backend
|
|
23
|
+
* delivery premises are not supplied by this adapter; the caller still
|
|
24
|
+
* decides whether silence has authority.
|
|
25
25
|
* @evidence contracts/common.md#clear-and-simple-design
|
|
26
|
-
* One three-tracker list drives
|
|
27
|
-
*
|
|
26
|
+
* One three-tracker list drives drainage; directory identity belongs to the
|
|
27
|
+
* synchronous admission that reads it.
|
|
28
28
|
* @evidence contracts/common.md#prohibited-implementation-shortcuts
|
|
29
|
-
*
|
|
30
|
-
*
|
|
29
|
+
* The operation does not patch watcher paths or certify a generation from a
|
|
30
|
+
* timeout alone.
|
|
31
31
|
* @evidence contracts/common.md#meaningful-documentation
|
|
32
|
-
* Native paragraphs explain event ordering
|
|
33
|
-
*
|
|
32
|
+
* Native paragraphs explain event ordering and where location verification
|
|
33
|
+
* moved, under the documentation skill.
|
|
34
34
|
* @evidence contracts/portability.md#os-neutral-implementation
|
|
35
|
-
* OS-neutral delivery uses backend-owned drains
|
|
36
|
-
* identities; lexical aliases cannot prove a replaced directory unchanged.
|
|
35
|
+
* OS-neutral delivery uses backend-owned drains.
|
|
37
36
|
* @evidence contracts/performance.md#efficient-algorithms
|
|
38
|
-
* Three tracker slots are
|
|
39
|
-
*
|
|
40
|
-
* per exact directory spelling; native path traversal, key bytes and bigint
|
|
41
|
-
* identity formatting plus delegated drain work remain variable costs.
|
|
37
|
+
* Three tracker slots are drained concurrently; delegated drain work remains
|
|
38
|
+
* a variable cost.
|
|
42
39
|
* @evidence contracts/performance.md#reuse-equivalent-work
|
|
43
|
-
* In-flight drainage is shared by
|
|
44
|
-
* delivery's map; the map is recreated for every delivery, not cached forever.
|
|
40
|
+
* In-flight drainage is shared by concurrent sibling deliveries.
|
|
45
41
|
* @evidence contracts/performance.md#bound-retention-and-release-resources
|
|
46
|
-
* The identity map is local to the post-drain verification call, retaining
|
|
47
|
-
* queried directory/identity text with no independent population/byte cap.
|
|
48
42
|
* Pending drains retain the cached tracker references until they settle;
|
|
49
43
|
* rejection does not cancel siblings and no aggregate deadline is supplied.
|
|
50
44
|
* Tracker owners still control backend task and watcher closure.
|
|
@@ -52,14 +46,9 @@ import { settleMutationTrackers } from "./settleMutationTrackers";
|
|
|
52
46
|
export async function settleProjectMutationEvents(
|
|
53
47
|
cached: TtscCachedProjectTransform,
|
|
54
48
|
): Promise<void> {
|
|
55
|
-
|
|
49
|
+
await settleMutationTrackers([
|
|
56
50
|
cached.projectMutationTracker,
|
|
57
51
|
cached.hostInputMutationTracker,
|
|
58
52
|
cached.candidateMutationTracker,
|
|
59
|
-
];
|
|
60
|
-
await settleMutationTrackers(trackers);
|
|
61
|
-
// A watch that now observes a replaced directory has nothing left to say
|
|
62
|
-
// about the new one, so it gives up its authority before anything reads it.
|
|
63
|
-
const seen = new Map<string, string | undefined>();
|
|
64
|
-
for (const tracker of trackers) tracker?.verifyLocations?.(seen);
|
|
53
|
+
]);
|
|
65
54
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { TtscCachedProjectTransform } from "../cache/TtscCachedProjectTransform";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Recheck every watched directory's identity against the one its tracker opened
|
|
5
|
+
* on, returning what the check read.
|
|
6
|
+
*
|
|
7
|
+
* A watch that now observes a replaced directory has nothing left to say about
|
|
8
|
+
* the new one, so its tracker gives up its authority before anything reads it.
|
|
9
|
+
* The trackers share what they read, so a directory they all watch, the project
|
|
10
|
+
* root above all, costs one metadata call per delivery rather than one per
|
|
11
|
+
* tracker. The returned readings belong to the synchronous admission that ran
|
|
12
|
+
* the check: a validator in it that must know whether one of these directories
|
|
13
|
+
* is still a directory reads the answer here instead of stating it again
|
|
14
|
+
* (samchon/ttsc#1735). The map assumes the callbacks share a coherent
|
|
15
|
+
* filesystem view; it is not an atomic snapshot.
|
|
16
|
+
*
|
|
17
|
+
* @evidence contracts/common.md#principled-implementation
|
|
18
|
+
* Each verifier compares followed device and file ids with the opened ones,
|
|
19
|
+
* and a mismatch or unreadable location withdraws its tracker.
|
|
20
|
+
* @evidence contracts/common.md#clear-and-simple-design
|
|
21
|
+
* One three-tracker list drives every verifier through one delivery-local
|
|
22
|
+
* identity map, which is also the result.
|
|
23
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts
|
|
24
|
+
* A replaced watched directory withdraws authority; lexical spelling never
|
|
25
|
+
* proves a directory unchanged.
|
|
26
|
+
* @evidence contracts/common.md#meaningful-documentation
|
|
27
|
+
* Native prose explains withdrawal, sharing and the result's synchronous
|
|
28
|
+
* lifetime under the documentation skill.
|
|
29
|
+
* @evidence contracts/portability.md#os-neutral-implementation
|
|
30
|
+
* Identities are the filesystem's own physical identities, followed through
|
|
31
|
+
* links, with no platform case rule.
|
|
32
|
+
* @evidence contracts/performance.md#efficient-algorithms
|
|
33
|
+
* Three tracker slots are visited; existing verifiers scan their retained
|
|
34
|
+
* watched locations, sharing one metadata read per exact directory spelling.
|
|
35
|
+
* @evidence contracts/performance.md#reuse-equivalent-work
|
|
36
|
+
* The map is created for each admission and handed to that admission's
|
|
37
|
+
* validators, never retained across deliveries.
|
|
38
|
+
* @evidence contracts/performance.md#bound-retention-and-release-resources
|
|
39
|
+
* The caller's synchronous transaction owns the map, which retains queried
|
|
40
|
+
* directory and identity text only until that transaction ends.
|
|
41
|
+
*/
|
|
42
|
+
export function verifyGenerationLocations(
|
|
43
|
+
cached: TtscCachedProjectTransform,
|
|
44
|
+
): Map<string, string | undefined> {
|
|
45
|
+
const seen = new Map<string, string | undefined>();
|
|
46
|
+
for (const tracker of [
|
|
47
|
+
cached.projectMutationTracker,
|
|
48
|
+
cached.hostInputMutationTracker,
|
|
49
|
+
cached.candidateMutationTracker,
|
|
50
|
+
])
|
|
51
|
+
tracker?.verifyLocations?.(seen);
|
|
52
|
+
return seen;
|
|
53
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Whether process `pid` has exited and only waits for its parent to collect its
|
|
5
|
+
* status.
|
|
6
|
+
*
|
|
7
|
+
* Signal zero still reaches such a zombie, so a liveness probe built on it
|
|
8
|
+
* alone counts the process alive. A parent that never collects, such as a
|
|
9
|
+
* container's PID 1 that is not an init or a subreaper that collects only when
|
|
10
|
+
* its own command ends, keeps the zombie indefinitely, and the locks and
|
|
11
|
+
* directories it owned would never be reclaimed. Only Linux exposes the state,
|
|
12
|
+
* through `/proc`; elsewhere, and whenever it cannot be read, this answers
|
|
13
|
+
* `false` so the caller's existing probe decides. A leader whose own thread
|
|
14
|
+
* exited also reads as a zombie while its other threads run, so the zombie must
|
|
15
|
+
* be the process's last thread. A zombie keeps its id until collected, so this
|
|
16
|
+
* never names a live process.
|
|
17
|
+
*
|
|
18
|
+
* @param pid A positive process id that signal zero just found.
|
|
19
|
+
* @evidence contracts/common.md#principled-implementation The kernel's own process status, zombie or dead with no other thread, establishes the exit; any unreadable or other status leaves the caller's signal-zero answer standing.
|
|
20
|
+
* @evidence contracts/common.md#clear-and-simple-design One predicate serves the shared-compile lock, session-owner and watch-probe owners alike.
|
|
21
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts No PID list, timeout or error-to-exited fallback substitutes for the kernel status.
|
|
22
|
+
* @evidence contracts/common.md#meaningful-documentation The paragraph explains why signal zero misses a zombie, which parents leave one behind, why a zombie leader with running threads is not one and why its id cannot name a live process.
|
|
23
|
+
* @evidence contracts/portability.md#os-neutral-implementation Only Linux exposes zombies through /proc; other platforms keep their probe's answer.
|
|
24
|
+
* @evidence contracts/performance.md#efficient-algorithms One small /proc read on Linux, nothing elsewhere.
|
|
25
|
+
* @evidenceExclude contracts/performance.md#reuse-equivalent-work Process state is a current observation and is never cached.
|
|
26
|
+
* @evidenceExclude contracts/performance.md#bound-retention-and-release-resources The read retains no handle; the status text becomes reclaimable on return.
|
|
27
|
+
*/
|
|
28
|
+
export function isZombieProcess(pid: number): boolean {
|
|
29
|
+
if (process.platform !== "linux") return false;
|
|
30
|
+
let status: string;
|
|
31
|
+
try {
|
|
32
|
+
status = fs.readFileSync(`/proc/${pid}/status`, "utf8");
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
const state = /^State:\s*(\S)/m.exec(status)?.[1];
|
|
37
|
+
const threads = /^Threads:\s*(\d+)/m.exec(status)?.[1];
|
|
38
|
+
return (state === "Z" || state === "X") && threads === "1";
|
|
39
|
+
}
|
|
@@ -10,6 +10,7 @@ import { selectPluginSourceInputs } from "../envelope/selectPluginSourceInputs";
|
|
|
10
10
|
import { pathIdentityKey } from "../filesystem/pathIdentityKey";
|
|
11
11
|
import { preparePluginBuildEnvironments } from "../inputs/preparePluginBuildEnvironments";
|
|
12
12
|
import { toProjectKey } from "../project/toProjectKey";
|
|
13
|
+
import { verifyGenerationLocations } from "../tracker/verifyGenerationLocations";
|
|
13
14
|
import type { TtscTransformHooks } from "../watch/TtscTransformHooks";
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -56,6 +57,13 @@ export namespace TtscGenerationProof {
|
|
|
56
57
|
|
|
57
58
|
/** Current native predicate verdict, unbuilt until its first gate. */
|
|
58
59
|
nativePredicates?: boolean;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Identities this admission's location verification read, by watched
|
|
63
|
+
* directory, absent when it ran none. An identity proves its directory is
|
|
64
|
+
* one now; an absent or undefined entry proves nothing.
|
|
65
|
+
*/
|
|
66
|
+
readonly locations?: ReadonlyMap<string, string | undefined>;
|
|
59
67
|
}
|
|
60
68
|
|
|
61
69
|
/**
|
|
@@ -86,15 +94,17 @@ export namespace TtscGenerationProof {
|
|
|
86
94
|
|
|
87
95
|
/**
|
|
88
96
|
* Start fresh synchronous observation after notification and preparation
|
|
89
|
-
* awaits, sharing actual SDK queries and native predicate replay locally.
|
|
97
|
+
* awaits, sharing actual SDK queries and native predicate replay locally. A
|
|
98
|
+
* persistent delivery also rechecks its trackers' watched directories here
|
|
99
|
+
* and keeps what that read for the validators that follow.
|
|
90
100
|
*
|
|
91
|
-
* @evidence contracts/common.md#principled-implementation Current environments are batch-qualified after asynchronous work; a qualified pass needs no redundant native environment observation.
|
|
101
|
+
* @evidence contracts/common.md#principled-implementation Current environments are batch-qualified after asynchronous work, and a persistent delivery's watched directories are rechecked after it; a qualified pass needs no redundant native environment observation.
|
|
92
102
|
* @evidence contracts/common.md#clear-and-simple-design One constructor binds observations to their exact generation.
|
|
93
103
|
* @evidence contracts/common.md#prohibited-implementation-shortcuts No current verdict is retained across deliveries or asynchronous boundaries.
|
|
94
104
|
* @evidence contracts/common.md#meaningful-documentation Native prose identifies the mandatory ordering and synchronous lifetime.
|
|
95
105
|
* @evidence contracts/portability.md#os-neutral-implementation Native observation remains with the environment witness owner and actual generation path identity.
|
|
96
|
-
* @evidence contracts/performance.md#efficient-algorithms Plugin selection is generation-indexed; one batch shares variable hashing and distinct SDK metadata queries.
|
|
97
|
-
* @evidence contracts/performance.md#reuse-equivalent-work Only the following synchronous validators borrow this batch; pass sharing uses its independent first-delivery contract.
|
|
106
|
+
* @evidence contracts/performance.md#efficient-algorithms Plugin selection is generation-indexed; one batch shares variable hashing and distinct SDK metadata queries, and location verification reads each watched directory once.
|
|
107
|
+
* @evidence contracts/performance.md#reuse-equivalent-work Only the following synchronous validators borrow this batch and the location identities; pass sharing uses its independent first-delivery contract.
|
|
98
108
|
* @evidence contracts/performance.md#bound-retention-and-release-resources The caller releases the transaction at the end of synchronous admission; no registry or native handle is acquired.
|
|
99
109
|
*/
|
|
100
110
|
export function create(
|
|
@@ -104,6 +114,12 @@ export namespace TtscGenerationProof {
|
|
|
104
114
|
): Transaction {
|
|
105
115
|
return {
|
|
106
116
|
cached,
|
|
117
|
+
// A persistent delivery rechecks its watched directories here, after
|
|
118
|
+
// its last await, so a directory replaced while it prepared withdraws
|
|
119
|
+
// its tracker before admission reads it.
|
|
120
|
+
...(epoch === undefined
|
|
121
|
+
? { locations: verifyGenerationLocations(cached) }
|
|
122
|
+
: {}),
|
|
107
123
|
...(sharesPass(cached, file, epoch)
|
|
108
124
|
? {}
|
|
109
125
|
: {
|
|
@@ -5,6 +5,7 @@ import { resultFilesystem } from "../cache/resultFilesystem";
|
|
|
5
5
|
import { derivationIdentity } from "../envelope/derivationIdentity";
|
|
6
6
|
import { envelopeDerivation } from "../envelope/envelopeDerivation";
|
|
7
7
|
import { graphInputStateHash } from "../inputs/graphInputStateHash";
|
|
8
|
+
import { hostInputRealpath } from "../inputs/hostInputRealpath";
|
|
8
9
|
import { hostInputStateHash } from "../inputs/hostInputStateHash";
|
|
9
10
|
import { inputMetadataEvidence } from "../inputs/inputMetadataEvidence";
|
|
10
11
|
import { inputMetadataSignature } from "../inputs/inputMetadataSignature";
|
|
@@ -20,7 +21,7 @@ import { MISSING_INPUT_STATE } from "./MISSING_INPUT_STATE";
|
|
|
20
21
|
* The caller adopts those signatures only once every input is proven unchanged,
|
|
21
22
|
* so a signature never outlives the content comparison that justified it.
|
|
22
23
|
*
|
|
23
|
-
* @evidence contracts/common.md#principled-implementation Each lexical spelling replays its predicates or owning content codec; only stable separable metadata can replace that comparison, so aliases cannot answer for one another.
|
|
24
|
+
* @evidence contracts/common.md#principled-implementation Each lexical spelling replays its predicates or owning content codec and resolves its own physical file; only stable separable metadata can replace that comparison, so aliases cannot answer for one another. Spellings resolving to one physical file now share that file's content read, which the codecs hash by content alone.
|
|
24
25
|
* @evidence contracts/common.md#clear-and-simple-design One pass returns the aggregate verdict and earned signatures, leaving all-or-nothing adoption to its caller.
|
|
25
26
|
* @evidence contracts/common.md#prohibited-implementation-shortcuts Recorded missing states and absent hash keys remain distinct from successful content proof; newer reads do not rewrite the authoritative recorded hashes.
|
|
26
27
|
* @evidence contracts/common.md#meaningful-documentation Native paragraphs distinguish mixed codecs, clock-qualified reuse and caller-owned signature adoption before tags.
|
|
@@ -34,7 +35,7 @@ import { MISSING_INPUT_STATE } from "./MISSING_INPUT_STATE";
|
|
|
34
35
|
* failures/listings, then recaptures metadata for earned signatures.
|
|
35
36
|
* Earned signature storage grows with successful observed spellings; legacy
|
|
36
37
|
* fallback also materializes the recorded hash key array.
|
|
37
|
-
* @evidence contracts/performance.md#reuse-equivalent-work The generation's unchanged separable signatures share prior content validation; changed metadata or clock ordering requires replay, and adopted signatures come only from a completely successful caller verdict.
|
|
38
|
+
* @evidence contracts/performance.md#reuse-equivalent-work The generation's unchanged separable signatures share prior content validation; changed metadata or clock ordering requires replay, read once per codec and physical file in this pass, and adopted signatures come only from a completely successful caller verdict.
|
|
38
39
|
*/
|
|
39
40
|
export function matchesCachedExternalInputs(
|
|
40
41
|
/** Generation recording lexical inputs, distinct codecs and earned signatures. */
|
|
@@ -53,6 +54,8 @@ export function matchesCachedExternalInputs(
|
|
|
53
54
|
const filesystem = resultFilesystem(cached.result);
|
|
54
55
|
const recordedHashes = cached.externalInputHashes ?? {};
|
|
55
56
|
const recordedSignatures = cached.externalInputSignatures ?? {};
|
|
57
|
+
// Content hashes this pass read, by codec and current physical file.
|
|
58
|
+
const sharedReads = new Map<string, string | null>();
|
|
56
59
|
// Compare each spelling against the recorded state under its own name. Two
|
|
57
60
|
// spellings share one identity exactly when they selected one physical file
|
|
58
61
|
// at generation time, which is the state a retarget ends, so neither may
|
|
@@ -102,9 +105,29 @@ export function matchesCachedExternalInputs(
|
|
|
102
105
|
) {
|
|
103
106
|
continue;
|
|
104
107
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
+
// Each spelling resolves on its own and the first one resolving to a
|
|
109
|
+
// physical file reads it through its own spelling, as the filesystem view
|
|
110
|
+
// answers; a later spelling resolving to that file now reuses the read,
|
|
111
|
+
// since the codecs hash content alone. A metadata-only change otherwise
|
|
112
|
+
// read an alias and its target twice in one delivery.
|
|
113
|
+
const graph = Object.prototype.hasOwnProperty.call(
|
|
114
|
+
graphRealpaths,
|
|
115
|
+
identity,
|
|
116
|
+
);
|
|
117
|
+
const physical = hostInputRealpath(file, filesystem);
|
|
118
|
+
const shared =
|
|
119
|
+
physical === null
|
|
120
|
+
? undefined
|
|
121
|
+
: `${graph ? "graph" : "host"}\0${physical}`;
|
|
122
|
+
let hash: string | null;
|
|
123
|
+
if (shared !== undefined && sharedReads.has(shared)) {
|
|
124
|
+
hash = sharedReads.get(shared)!;
|
|
125
|
+
} else {
|
|
126
|
+
hash = graph
|
|
127
|
+
? graphInputStateHash(file, filesystem)
|
|
128
|
+
: hostInputStateHash(file, filesystem);
|
|
129
|
+
if (shared !== undefined) sharedReads.set(shared, hash);
|
|
130
|
+
}
|
|
108
131
|
const after = inputMetadataSignature(file, filesystem);
|
|
109
132
|
if (
|
|
110
133
|
!Object.prototype.hasOwnProperty.call(recordedHashes, identity) ||
|
|
@@ -73,7 +73,7 @@ export function matchesCompleteInputSnapshot(
|
|
|
73
73
|
if (
|
|
74
74
|
hostValidation === undefined ||
|
|
75
75
|
!matchesUniversalHostInputEntries(cached, hostValidation) ||
|
|
76
|
-
!matchesUniversalHostInputProbes(cached, hostValidation) ||
|
|
76
|
+
!matchesUniversalHostInputProbes(cached, hostValidation, proof) ||
|
|
77
77
|
!matchesUniversalHostInputTrees(cached, hostValidation, proof)
|
|
78
78
|
) {
|
|
79
79
|
return false;
|
|
@@ -2,6 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
|
|
3
3
|
import type { TtscCachedProjectTransform } from "../cache/TtscCachedProjectTransform";
|
|
4
4
|
import { resultFilesystem } from "../cache/resultFilesystem";
|
|
5
|
+
import type { TtscGenerationProof } from "./TtscGenerationProof";
|
|
5
6
|
import type { TtscHostInputValidation } from "./TtscHostInputValidation";
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -17,18 +18,25 @@ import type { TtscHostInputValidation } from "./TtscHostInputValidation";
|
|
|
17
18
|
* Complete validation still compares its recorded byte and membership
|
|
18
19
|
* populations after this independent native absence predicate holds.
|
|
19
20
|
*
|
|
20
|
-
*
|
|
21
|
+
* A group also needs its directory to still be one, which a candidate's own
|
|
22
|
+
* stat cannot say: Windows answers ENOENT below a file just as below nothing.
|
|
23
|
+
* The admission's location verification has already read that of every watched
|
|
24
|
+
* directory, so a group under one reuses it instead of stating the directory a
|
|
25
|
+
* second time in the same delivery (samchon/ttsc#1735).
|
|
26
|
+
*
|
|
27
|
+
* @evidence contracts/common.md#principled-implementation Exact native ENOENT or ENOTDIR observations prove candidate absence, including native Unicode and short-name aliases. A vanished ancestor leaves descendants unreachable; replacement by a file rejects the grouped directory proof, whether this pass or the admission's location verification observed the directory.
|
|
21
28
|
* @evidence contracts/common.md#clear-and-simple-design Missing-name validation has its own native absence boundary shared by narrow and complete proof; byte hashes and project membership remain separate predicates.
|
|
22
|
-
* @evidence contracts/common.md#prohibited-implementation-shortcuts Listing inequality is not native name inequality; permission and transient failures are not fabricated absence. Only ENOENT and ENOTDIR qualify missing-path observations.
|
|
23
|
-
* @evidence contracts/common.md#meaningful-documentation Separate native paragraphs explain inability-to-prove rejection, unreadable-content ambiguity
|
|
24
|
-
* @evidence contracts/portability.md#os-neutral-implementation The injected native stat resolves original candidate spelling, and its platform selects the same win32 or posix joining grammar as capture's ancestor probe. Case flags and JavaScript Unicode folding do not replace native alias resolution.
|
|
25
|
-
* @evidenceExclude contracts/performance.md#bound-retention-and-release-resources The pass borrows generation-owned groups and retains only each current native probe result; no directory listing, historical state or handle is acquired.
|
|
26
|
-
* @evidence contracts/performance.md#efficient-algorithms Each
|
|
27
|
-
* @evidence contracts/performance.md#reuse-equivalent-work Capture shares nearest-ancestor selection and raw candidate groups across consumers; each current absence proof must observe native candidates again unless the owning notification validator establishes unchanged inputs.
|
|
29
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts Listing inequality is not native name inequality; permission and transient failures are not fabricated absence. Only ENOENT and ENOTDIR qualify missing-path observations, and only an identity read by the same synchronous admission stands for a directory stat.
|
|
30
|
+
* @evidence contracts/common.md#meaningful-documentation Separate native paragraphs explain inability-to-prove rejection, unreadable-content ambiguity, both caller routes and the shared directory reading before tags.
|
|
31
|
+
* @evidence contracts/portability.md#os-neutral-implementation The injected native stat resolves original candidate spelling, and its platform selects the same win32 or posix joining grammar as capture's ancestor probe. Case flags and JavaScript Unicode folding do not replace native alias resolution, and the directory check does not rely on ENOTDIR, which Windows does not report.
|
|
32
|
+
* @evidenceExclude contracts/performance.md#bound-retention-and-release-resources The pass borrows generation-owned groups and the admission's identities, and retains only each current native probe result; no directory listing, historical state or handle is acquired.
|
|
33
|
+
* @evidence contracts/performance.md#efficient-algorithms Each candidate requires one native stat, and each group one directory stat unless its admission already read the directory's identity. Work follows candidate count and native path resolution, without enumerating unrelated directory entries; exact probes are necessary because listing names cannot establish all native aliases.
|
|
34
|
+
* @evidence contracts/performance.md#reuse-equivalent-work Capture shares nearest-ancestor selection and raw candidate groups across consumers, and one synchronous admission shares its watched directories' identities; each current absence proof must observe native candidates again unless the owning notification validator establishes unchanged inputs.
|
|
28
35
|
*/
|
|
29
36
|
export function matchesUniversalHostInputProbes(
|
|
30
37
|
cached: TtscCachedProjectTransform,
|
|
31
38
|
validation: TtscHostInputValidation,
|
|
39
|
+
proof?: TtscGenerationProof.Transaction,
|
|
32
40
|
): boolean {
|
|
33
41
|
const filesystem = resultFilesystem(cached.result);
|
|
34
42
|
const paths =
|
|
@@ -44,15 +52,18 @@ export function matchesUniversalHostInputProbes(
|
|
|
44
52
|
}
|
|
45
53
|
}
|
|
46
54
|
for (const [directory, names] of validation.missing) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
// An identity the admission read proves the directory is one now.
|
|
56
|
+
if (proof?.locations?.get(directory) === undefined) {
|
|
57
|
+
try {
|
|
58
|
+
if (!filesystem.stat(directory).isDirectory()) return false;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
// Only a provably absent/non-directory ancestor keeps every descendant
|
|
61
|
+
// unreachable. Permission and transient I/O failures cannot prove that
|
|
62
|
+
// a candidate is still missing, while replacing the proving directory
|
|
63
|
+
// with an exact file can itself redirect module resolution.
|
|
64
|
+
if (!isMissingPathError(error)) return false;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
56
67
|
}
|
|
57
68
|
for (const name of names) {
|
|
58
69
|
try {
|