@openclaw/signal 2026.11.3 → 2026.11.5-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/dist/.setup/{accounts-B0Pba1-q.mjs → accounts-enJKL3ci.mjs} +4 -8
  2. package/dist/.setup/api-CrasQEKI.mjs +14 -0
  3. package/dist/.setup/{api-CbN3mye4.mjs → api-DljZsfUn.mjs} +9 -11
  4. package/dist/.setup/{approval-auth-DhUKkSkA.mjs → approval-auth-B_5Ytx8K.mjs} +7 -11
  5. package/dist/.setup/{approval-handler.runtime-CsEcBZoc.mjs → approval-handler.runtime-0RbVmTxx.mjs} +7 -8
  6. package/dist/.setup/{approval-reactions-JsqCEOHK.mjs → approval-reactions-CvqVSSx8.mjs} +16 -24
  7. package/dist/.setup/{channel-CM-rLLPN.mjs → channel-ByJj8fSh.mjs} +417 -372
  8. package/dist/.setup/{client-Duwqbgqi.mjs → client-CesmEjRc.mjs} +170 -186
  9. package/dist/.setup/{config-schema-DvxZ18SD.mjs → config-schema-BCmW0BLB.mjs} +2 -5
  10. package/dist/.setup/{doctor-contract-api-BIdVW0PF.mjs → doctor-contract-api-CcLwIhsD.mjs} +88 -95
  11. package/dist/.setup/{identity-B3JyPqGO.mjs → identity-vKtR3DrM.mjs} +39 -2
  12. package/dist/.setup/{install-signal-cli-hYVLYGXe.mjs → install-signal-cli-DvLDZfpP.mjs} +95 -109
  13. package/dist/.setup/{transport-url-CAC0JcuI.mjs → legacy-transport-D_cZC3bS.mjs} +18 -1
  14. package/dist/.setup/{monitor-C6u1JV2C.mjs → monitor-Cf5sihwl.mjs} +166 -239
  15. package/dist/.setup/{probe-D_93QCe0.mjs → probe-Caq5Y4kA.mjs} +3 -3
  16. package/dist/.setup/{send-CM7-016e.mjs → send-BJuOWLky.mjs} +34 -66
  17. package/dist/.setup/{setup-surface-DvKLwYrl.mjs → setup-surface-DQ1yFZmY.mjs} +3 -3
  18. package/dist/.setup/{transport-detection-CmlgOybe.mjs → transport-detection-DvR6X_UQ.mjs} +3 -3
  19. package/dist/.setup/{transport-policy-B6C_LogW.mjs → transport-policy-zIR3lRUj.mjs} +1 -1
  20. package/dist/.setup/transport-probes.runtime-CoH6UFtc.mjs +1 -0
  21. package/dist/.source-module-urls/b02c526df8b90083105b63e5b51d5c8bbfe02676df72f6be8dd8f825817b32b9.json +1 -0
  22. package/dist/api.js +9 -11
  23. package/dist/channel-config-api.js +1 -2
  24. package/dist/channel-plugin-api.js +1 -2
  25. package/dist/contract-api.js +3 -4
  26. package/dist/doctor-contract-api.js +1 -2
  27. package/dist/runtime-api.js +1 -2
  28. package/dist/setup-entry.js +1 -1
  29. package/node_modules/@openclaw/fs-safe/CHANGELOG.md +94 -0
  30. package/node_modules/@openclaw/fs-safe/dist/archive-parser.wasm +0 -0
  31. package/node_modules/@openclaw/fs-safe/dist/copy-file-input.d.ts +1 -1
  32. package/node_modules/@openclaw/fs-safe/dist/copy-file-input.js +3 -1
  33. package/node_modules/@openclaw/fs-safe/dist/copy-path-admission.js +2 -2
  34. package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.js +8 -1
  35. package/node_modules/@openclaw/fs-safe/dist/file-open-error.d.ts +2 -0
  36. package/node_modules/@openclaw/fs-safe/dist/file-open-error.js +14 -0
  37. package/node_modules/@openclaw/fs-safe/dist/file-sync.d.ts +2 -1
  38. package/node_modules/@openclaw/fs-safe/dist/file-sync.js +6 -1
  39. package/node_modules/@openclaw/fs-safe/dist/guest-native-python.js +3 -0
  40. package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +1 -0
  41. package/node_modules/@openclaw/fs-safe/dist/native-config.js +14 -3
  42. package/node_modules/@openclaw/fs-safe/dist/native-operations.js +2 -0
  43. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write-windows.js +9 -2
  44. package/node_modules/@openclaw/fs-safe/dist/native-staged-file.js +40 -32
  45. package/node_modules/@openclaw/fs-safe/dist/output.js +6 -0
  46. package/node_modules/@openclaw/fs-safe/dist/owner-dacl.d.ts +2 -0
  47. package/node_modules/@openclaw/fs-safe/dist/owner-dacl.js +1 -0
  48. package/node_modules/@openclaw/fs-safe/dist/path.js +5 -1
  49. package/node_modules/@openclaw/fs-safe/dist/pinned-write-staged.js +25 -2
  50. package/node_modules/@openclaw/fs-safe/dist/publication-handle.d.ts +4 -0
  51. package/node_modules/@openclaw/fs-safe/dist/publication-handle.js +32 -0
  52. package/node_modules/@openclaw/fs-safe/dist/publish-copy-stage.d.ts +2 -1
  53. package/node_modules/@openclaw/fs-safe/dist/publish-copy-stage.js +19 -9
  54. package/node_modules/@openclaw/fs-safe/dist/publish-file.js +23 -24
  55. package/node_modules/@openclaw/fs-safe/dist/publish-rename-fallback.d.ts +21 -0
  56. package/node_modules/@openclaw/fs-safe/dist/publish-rename-fallback.js +102 -0
  57. package/node_modules/@openclaw/fs-safe/dist/root-create-native.js +2 -0
  58. package/node_modules/@openclaw/fs-safe/dist/root-directory-entry.d.ts +3 -1
  59. package/node_modules/@openclaw/fs-safe/dist/root-directory-entry.js +8 -5
  60. package/node_modules/@openclaw/fs-safe/dist/root-directory-list-types.d.ts +3 -0
  61. package/node_modules/@openclaw/fs-safe/dist/root-directory-list.d.ts +2 -1
  62. package/node_modules/@openclaw/fs-safe/dist/root-directory-list.js +19 -9
  63. package/node_modules/@openclaw/fs-safe/dist/root-file-final-admission.js +3 -3
  64. package/node_modules/@openclaw/fs-safe/dist/root-path-existing.d.ts +2 -2
  65. package/node_modules/@openclaw/fs-safe/dist/root-path-existing.js +27 -13
  66. package/node_modules/@openclaw/fs-safe/dist/root-path.js +2 -2
  67. package/node_modules/@openclaw/fs-safe/dist/sibling-staged-file.d.ts +3 -0
  68. package/node_modules/@openclaw/fs-safe/dist/sibling-staged-file.js +18 -3
  69. package/node_modules/@openclaw/fs-safe/dist/watch-hints.d.ts +1 -1
  70. package/node_modules/@openclaw/fs-safe/dist/watch-hints.js +68 -21
  71. package/node_modules/@openclaw/fs-safe/dist/watch-io-error.d.ts +2 -0
  72. package/node_modules/@openclaw/fs-safe/dist/watch-io-error.js +27 -0
  73. package/node_modules/@openclaw/fs-safe/dist/watch-io-worker.d.ts +1 -0
  74. package/node_modules/@openclaw/fs-safe/dist/watch-io-worker.js +93 -0
  75. package/node_modules/@openclaw/fs-safe/dist/watch-io.d.ts +47 -0
  76. package/node_modules/@openclaw/fs-safe/dist/watch-io.js +236 -0
  77. package/node_modules/@openclaw/fs-safe/dist/watch-native.d.ts +1 -1
  78. package/node_modules/@openclaw/fs-safe/dist/watch-native.js +3 -3
  79. package/node_modules/@openclaw/fs-safe/dist/watch-probe.d.ts +16 -0
  80. package/node_modules/@openclaw/fs-safe/dist/watch-probe.js +135 -0
  81. package/node_modules/@openclaw/fs-safe/dist/watch-rescan.d.ts +1 -1
  82. package/node_modules/@openclaw/fs-safe/dist/watch-rescan.js +86 -24
  83. package/node_modules/@openclaw/fs-safe/dist/watch-scan.d.ts +4 -0
  84. package/node_modules/@openclaw/fs-safe/dist/watch-scan.js +78 -16
  85. package/node_modules/@openclaw/fs-safe/dist/watch-stream.d.ts +2 -0
  86. package/node_modules/@openclaw/fs-safe/dist/watch-stream.js +23 -0
  87. package/node_modules/@openclaw/fs-safe/dist/watch-types.d.ts +3 -1
  88. package/node_modules/@openclaw/fs-safe/dist/watch.js +60 -13
  89. package/node_modules/@openclaw/fs-safe/dist/windows-security-facts.d.ts +1 -1
  90. package/node_modules/@openclaw/fs-safe/dist/windows-security-facts.js +1 -0
  91. package/node_modules/@openclaw/fs-safe/dist/windows-security-helper.exe +0 -0
  92. package/node_modules/@openclaw/fs-safe/docs/advanced.md +5 -2
  93. package/node_modules/@openclaw/fs-safe/docs/compiler-qualification.md +392 -0
  94. package/node_modules/@openclaw/fs-safe/docs/config.md +7 -1
  95. package/node_modules/@openclaw/fs-safe/docs/contributing.md +25 -5
  96. package/node_modules/@openclaw/fs-safe/docs/copy.md +10 -0
  97. package/node_modules/@openclaw/fs-safe/docs/durability.md +9 -3
  98. package/node_modules/@openclaw/fs-safe/docs/errors.md +13 -0
  99. package/node_modules/@openclaw/fs-safe/docs/migrating-to-0.5.md +6 -4
  100. package/node_modules/@openclaw/fs-safe/docs/native-helper.md +6 -4
  101. package/node_modules/@openclaw/fs-safe/docs/output.md +4 -2
  102. package/node_modules/@openclaw/fs-safe/docs/path.md +5 -2
  103. package/node_modules/@openclaw/fs-safe/docs/permissions.md +15 -0
  104. package/node_modules/@openclaw/fs-safe/docs/security-model.md +9 -0
  105. package/node_modules/@openclaw/fs-safe/docs/staged-file.md +2 -0
  106. package/node_modules/@openclaw/fs-safe/docs/testing.md +1 -1
  107. package/node_modules/@openclaw/fs-safe/docs/watch.md +62 -5
  108. package/node_modules/@openclaw/fs-safe/docs/writing.md +6 -1
  109. package/node_modules/@openclaw/fs-safe/package.json +18 -16
  110. package/node_modules/@openclaw/fs-safe-darwin-arm64/fs-safe-native.node +0 -0
  111. package/node_modules/@openclaw/fs-safe-darwin-arm64/package.json +1 -1
  112. package/node_modules/@openclaw/fs-safe-darwin-x64/fs-safe-native.node +0 -0
  113. package/node_modules/@openclaw/fs-safe-darwin-x64/package.json +1 -1
  114. package/node_modules/@openclaw/fs-safe-freebsd-arm64/fs-safe-native.node +0 -0
  115. package/node_modules/@openclaw/fs-safe-freebsd-arm64/package.json +1 -1
  116. package/node_modules/@openclaw/fs-safe-freebsd-x64/fs-safe-native.node +0 -0
  117. package/node_modules/@openclaw/fs-safe-freebsd-x64/package.json +1 -1
  118. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/fs-safe-native.node +0 -0
  119. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/package.json +1 -1
  120. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/fs-safe-native.node +0 -0
  121. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/package.json +1 -1
  122. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/fs-safe-native.node +0 -0
  123. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/package.json +1 -1
  124. package/node_modules/@openclaw/fs-safe-linux-x64-musl/fs-safe-native.node +0 -0
  125. package/node_modules/@openclaw/fs-safe-linux-x64-musl/package.json +1 -1
  126. package/node_modules/@openclaw/fs-safe-win32-arm64-msvc/fs-safe-native.node +0 -0
  127. package/node_modules/@openclaw/fs-safe-win32-arm64-msvc/package.json +1 -1
  128. package/node_modules/@openclaw/fs-safe-win32-x64-msvc/fs-safe-native.node +0 -0
  129. package/node_modules/@openclaw/fs-safe-win32-x64-msvc/package.json +1 -1
  130. package/node_modules/zod/package.json +1 -1
  131. package/node_modules/zod/src/v4/classic/tests/array.test.ts +27 -0
  132. package/node_modules/zod/src/v4/classic/tests/map.test.ts +7 -0
  133. package/node_modules/zod/src/v4/classic/tests/set.test.ts +6 -0
  134. package/node_modules/zod/src/v4/core/compile.ts +9 -0
  135. package/node_modules/zod/src/v4/core/schemas.ts +35 -1
  136. package/node_modules/zod/src/v4/core/util.ts +13 -0
  137. package/node_modules/zod/src/v4/core/versions.ts +1 -1
  138. package/node_modules/zod/v4/core/compile.cjs +8 -0
  139. package/node_modules/zod/v4/core/compile.js +8 -0
  140. package/node_modules/zod/v4/core/schemas.cjs +30 -0
  141. package/node_modules/zod/v4/core/schemas.d.cts +3 -1
  142. package/node_modules/zod/v4/core/schemas.d.ts +3 -1
  143. package/node_modules/zod/v4/core/schemas.js +30 -0
  144. package/node_modules/zod/v4/core/util.cjs +15 -0
  145. package/node_modules/zod/v4/core/util.d.cts +2 -0
  146. package/node_modules/zod/v4/core/util.d.ts +2 -0
  147. package/node_modules/zod/v4/core/util.js +14 -0
  148. package/node_modules/zod/v4/core/versions.cjs +1 -1
  149. package/node_modules/zod/v4/core/versions.js +1 -1
  150. package/openclaw.plugin.json +0 -30
  151. package/package.json +6 -8
  152. package/dist/.setup/api-CJBtD3lT.mjs +0 -16
  153. package/dist/.setup/client-adapter-Chnbch_z.mjs +0 -30
  154. package/dist/.setup/legacy-transport-QIpaMNST.mjs +0 -18
  155. package/dist/.setup/normalize-D7kSomXt.mjs +0 -39
  156. package/dist/.setup/reaction-runtime-api-mdO4k0yv.mjs +0 -65
  157. package/dist/.setup/reaction-targets-BgPLDHBf.mjs +0 -72
  158. package/dist/.setup/rpc-context-BAXcX7dI.mjs +0 -13
  159. package/dist/.setup/transport-probes.runtime-D-xIMRcn.mjs +0 -2
  160. package/dist/reaction-runtime-api.js +0 -2
  161. package/node_modules/ws/LICENSE +0 -20
  162. package/node_modules/ws/README.md +0 -548
  163. package/node_modules/ws/browser.js +0 -8
  164. package/node_modules/ws/index.js +0 -22
  165. package/node_modules/ws/lib/buffer-util.js +0 -131
  166. package/node_modules/ws/lib/constants.js +0 -19
  167. package/node_modules/ws/lib/event-target.js +0 -292
  168. package/node_modules/ws/lib/extension.js +0 -203
  169. package/node_modules/ws/lib/limiter.js +0 -55
  170. package/node_modules/ws/lib/permessage-deflate.js +0 -530
  171. package/node_modules/ws/lib/receiver.js +0 -743
  172. package/node_modules/ws/lib/sender.js +0 -607
  173. package/node_modules/ws/lib/stream.js +0 -161
  174. package/node_modules/ws/lib/subprotocol.js +0 -62
  175. package/node_modules/ws/lib/validation.js +0 -152
  176. package/node_modules/ws/lib/websocket-server.js +0 -562
  177. package/node_modules/ws/lib/websocket.js +0 -1421
  178. package/node_modules/ws/package.json +0 -74
  179. package/node_modules/ws/wrapper.mjs +0 -21
@@ -1,9 +1,10 @@
1
1
  import { FsSafeError } from "./errors.js";
2
2
  import { assertSynchronousCallbackResult } from "./mutation-authority.js";
3
- import { assertRootIdentityCurrent } from "./root-context.js";
3
+ import { createWatchIo, watchRootCurrent as assertRootIdentityCurrent, watchYield } from "./watch-io.js";
4
4
  import { rootHandleContext } from "./root-handle-context.js";
5
5
  import { createSuppressedError } from "./suppressed-error.js";
6
6
  import { getFsSafeTestHooks } from "./test-hooks.js";
7
+ import { probeWatchSnapshot, verifyWatchReuse } from "./watch-probe.js";
7
8
  import { mergeWatchRescan, watchRescanScopes } from "./watch-rescan.js";
8
9
  import { admittedNativeChanges, changedEntries, excludedWatchPath, guardedHintChanges, scopedChanges } from "./watch-hints.js";
9
10
  import path from "node:path";
@@ -32,7 +33,7 @@ const retired = () => new DOMException("Watch generation retired", "AbortError")
32
33
  const coalesceMs = 25;
33
34
  /** Advisory observation only. Hints never grant filesystem authority. */
34
35
  export function watch(root, input) {
35
- const context = rootHandleContext(root);
36
+ const context = { ...rootHandleContext(root) };
36
37
  const options = { ...input };
37
38
  const persistent = options.persistent !== false;
38
39
  if (!["auto", "events", "poll"].includes(options.mode))
@@ -54,6 +55,9 @@ export function watch(root, input) {
54
55
  throw new RangeError("watch pollIntervalMs must be at least 20");
55
56
  if (mode === "poll")
56
57
  intervalMs = pollIntervalMs;
58
+ const fullReconcileIntervalMs = budget(options.fullReconcileIntervalMs, 300_000, "fullReconcileIntervalMs", 2_147_483_647);
59
+ if (fullReconcileIntervalMs < 20)
60
+ throw new RangeError("watch fullReconcileIntervalMs must be at least 20");
57
61
  const maxDirectories = budget(options.maxDirectories, 4096, "maxDirectories");
58
62
  const maxEntries = budget(options.maxEntries, 100_000, "maxEntries");
59
63
  const maxPendingPaths = budget(options.maxPendingPaths, 256, "maxPendingPaths", 4096);
@@ -74,13 +78,17 @@ export function watch(root, input) {
74
78
  let closing;
75
79
  let active;
76
80
  let backend;
81
+ let closeIo;
77
82
  const registered = new Map();
78
83
  let observedDirectories = 0;
79
84
  let snapshot;
85
+ let hasBeenReady = false;
86
+ let lastFullReconcile = 0;
80
87
  let resetRequested = false;
81
88
  let refreshBackend = false;
82
89
  let pending = false;
83
90
  let fullRequested = false;
91
+ let periodicRequested = false;
84
92
  let pendingUncertain = false;
85
93
  let pendingWaiter;
86
94
  let runningWaiter;
@@ -208,7 +216,7 @@ export function watch(root, input) {
208
216
  const scheduleInterval = () => {
209
217
  if (timer || fullRequested || terminal || failure !== undefined)
210
218
  return;
211
- timer = setTimeout(() => { timer = undefined; void request().catch(() => { }); }, intervalMs);
219
+ timer = setTimeout(() => { timer = undefined; periodicRequested = true; void request(false).catch(() => { }); }, Math.min(intervalMs, fullReconcileIntervalMs));
212
220
  if (!persistent)
213
221
  timer.unref();
214
222
  };
@@ -271,7 +279,7 @@ export function watch(root, input) {
271
279
  hintTimer.unref();
272
280
  };
273
281
  const fallBack = (error) => {
274
- if (options.mode !== "auto" || getFsSafeNativeConfig().mode === "require" ||
282
+ if (hasBeenReady || options.mode !== "auto" || getFsSafeNativeConfig().mode === "require" ||
275
283
  !(error instanceof FsSafeError) || error.code !== "helper-unavailable")
276
284
  return false;
277
285
  mode = "poll";
@@ -281,6 +289,7 @@ export function watch(root, input) {
281
289
  };
282
290
  const observe = async (g) => {
283
291
  check(g);
292
+ closeIo ??= createWatchIo(context, persistent, retainRetirement);
284
293
  if (selectionFailure !== undefined)
285
294
  throw new FsSafeError("helper-unavailable", "native watch events are unavailable", { cause: selectionFailure, details: { operation: "watch" } });
286
295
  if (refreshBackend) {
@@ -298,7 +307,10 @@ export function watch(root, input) {
298
307
  const hadHints = pendingHint;
299
308
  const hadBackendOverflow = pendingBackendOverflow;
300
309
  const hints = pendingChanges;
301
- const full = fullRequested || pendingUncertain;
310
+ const age = performance.now() - lastFullReconcile;
311
+ const full = fullRequested || pendingUncertain || age < 0 || age >= fullReconcileIntervalMs;
312
+ const periodic = periodicRequested;
313
+ periodicRequested = false;
302
314
  fullRequested = false;
303
315
  pendingUncertain = false;
304
316
  pendingHint = false;
@@ -307,7 +319,7 @@ export function watch(root, input) {
307
319
  clearTimeout(hintTimer);
308
320
  hintTimer = undefined;
309
321
  let scanScopes = g.scopes;
310
- if (snapshot && hadHints && hints && !full) {
322
+ if (snapshot && hadHints && hints && !full && !periodic) {
311
323
  try {
312
324
  const admitted = await admittedNativeChanges(context, g.scopes, snapshot, snapshot, {
313
325
  hints: [...hints.values()], overflow: false,
@@ -329,6 +341,19 @@ export function watch(root, input) {
329
341
  state = snapshot ? "reconciling" : "starting";
330
342
  notifyHealth();
331
343
  check(g);
344
+ const probe = snapshot && !full && (!hadHints || periodic)
345
+ ? await probeWatchSnapshot(context, snapshot, g.abort.signal, options.exclude) : undefined;
346
+ check(g);
347
+ let reuse = probe?.reuse;
348
+ if (snapshot && probe?.stable && !hadHints && (mode === "poll" || backend) &&
349
+ await verifyWatchReuse(context, snapshot, probe.reuse, g.abort.signal)) {
350
+ check(g);
351
+ state = "ready";
352
+ notifyHealth();
353
+ check(g);
354
+ scheduleInterval();
355
+ return;
356
+ }
332
357
  if (mode === "events" && !backend && g.scopes.length) {
333
358
  try {
334
359
  const candidate = new NativeWatchBackend(binding, context, batch => {
@@ -337,7 +362,7 @@ export function watch(root, input) {
337
362
  }, maxPendingPaths, persistent);
338
363
  backend = candidate;
339
364
  if (snapshot)
340
- candidate.configure(snapshot, g.scopes);
365
+ await candidate.configure(snapshot, g.scopes);
341
366
  const hookResult = getFsSafeTestHooks()?.afterWatchBackendCreated?.(context.rootReal, batch => {
342
367
  if (backend === candidate)
343
368
  onHint(g, batch);
@@ -357,7 +382,7 @@ export function watch(root, input) {
357
382
  timer = undefined;
358
383
  }
359
384
  try {
360
- next = await scanWatch(context, scanScopes, { exclude: options.exclude, maxDirectories, maxEntries, maxPendingPaths, admitting: !snapshot, previous: snapshot }, g.abort.signal, async (name, identity, guard) => {
385
+ next = await scanWatch(context, scanScopes, { exclude: options.exclude, maxDirectories, maxEntries, maxPendingPaths, admitting: !snapshot, previous: snapshot, reuse }, g.abort.signal, async (name, identity, guard) => {
361
386
  check(g);
362
387
  const existing = registered.get(name);
363
388
  const acquire = !existing || existing.dev !== identity.dev || existing.ino !== identity.ino;
@@ -390,8 +415,13 @@ export function watch(root, input) {
390
415
  throw error;
391
416
  }
392
417
  check(g);
418
+ if (reuse && !await verifyWatchReuse(context, snapshot, reuse, g.abort.signal)) {
419
+ reuse = undefined;
420
+ scanScopes = g.scopes;
421
+ continue;
422
+ }
393
423
  if (scanScopes !== g.scopes) {
394
- const merged = mergeWatchRescan(g.scopes, snapshot, next, scanScopes);
424
+ const merged = await mergeWatchRescan(g.scopes, snapshot, next, scanScopes);
395
425
  if (!merged || merged.scanned > maxEntries || merged.directories.size > maxDirectories || (merged.excluded?.size ?? 0) > maxEntries) {
396
426
  scanScopes = g.scopes;
397
427
  continue;
@@ -400,7 +430,7 @@ export function watch(root, input) {
400
430
  }
401
431
  try {
402
432
  const entriesChanged = backend?.entries(next);
403
- const streamsChanged = backend?.configure(next, g.scopes);
433
+ const streamsChanged = await backend?.configure(next, g.scopes);
404
434
  if (entriesChanged || streamsChanged) {
405
435
  // Complete a guarded pass with the new transport before publishing readiness.
406
436
  // Bound handovers under churn; later passes still reconcile ongoing changes.
@@ -415,6 +445,11 @@ export function watch(root, input) {
415
445
  catch (error) {
416
446
  await assertRootIdentityCurrent(context);
417
447
  check(g);
448
+ if (!snapshot && fallBack(error)) {
449
+ await retireBackend();
450
+ check(g);
451
+ break;
452
+ }
418
453
  if (attempt >= 2 || !isWatchPathError(error))
419
454
  throw error;
420
455
  scanScopes = g.scopes;
@@ -422,13 +457,18 @@ export function watch(root, input) {
422
457
  }
423
458
  }
424
459
  // Retire stale inventory before the next pass; that crawl installs fresh anchors first.
425
- if ([...registered.keys()].some(name => !next.directories.has(name))) {
426
- refreshBackend = true;
460
+ const checkpoint = watchYield();
461
+ for (const name of registered.keys()) {
462
+ await checkpoint();
463
+ if (!next.directories.has(name)) {
464
+ refreshBackend = true;
465
+ break;
466
+ }
427
467
  }
428
468
  observedDirectories = backend?.directories === undefined ? next.directories.size
429
469
  : backend.directories + (g.scopes.some(scope => scope.kind === "tree") ? next.directories.size : 0);
430
470
  const initial = !snapshot;
431
- let observed = next.overflow ? undefined : changedEntries(snapshot, next, maxPendingPaths);
471
+ let observed = next.overflow ? undefined : await changedEntries(snapshot, next, maxPendingPaths);
432
472
  if (observed) {
433
473
  const changes = new Map(observed.map(change => [change.path, change]));
434
474
  for (const name of next.structural ?? [])
@@ -457,6 +497,8 @@ export function watch(root, input) {
457
497
  ? guardedHintChanges(g.scopes, snapshot, next, admittedHints, observed, maxPendingPaths)
458
498
  : observed;
459
499
  snapshot = next;
500
+ if (!reuse && scanScopes === g.scopes)
501
+ lastFullReconcile = performance.now();
460
502
  if (!initial && !details && hadBackendOverflow) {
461
503
  assertSynchronousCallbackResult(getFsSafeTestHooks()?.afterWatchBackendOverflow?.(context.rootReal, "reconciled"), "afterWatchBackendOverflow");
462
504
  }
@@ -464,6 +506,7 @@ export function watch(root, input) {
464
506
  if (initial || !details || details.length)
465
507
  dirty(g, initial ? "reconcile" : !details ? "overflow" : hadHints ? "event" : "reconcile", initial ? undefined : details);
466
508
  check(g);
509
+ hasBeenReady = true;
467
510
  state = "ready";
468
511
  notifyHealth();
469
512
  check(g);
@@ -503,6 +546,8 @@ export function watch(root, input) {
503
546
  catch (closeError) {
504
547
  retainRetirement(closeError);
505
548
  }
549
+ await closeIo?.();
550
+ closeIo = undefined;
506
551
  }
507
552
  if (current !== g) {
508
553
  await retireBackend();
@@ -564,6 +609,8 @@ export function watch(root, input) {
564
609
  catch (error) {
565
610
  retainRetirement(error);
566
611
  }
612
+ await closeIo?.();
613
+ closeIo = undefined;
567
614
  state = "closed";
568
615
  if (retirementFailure)
569
616
  throw combinedFailure();
@@ -1,6 +1,6 @@
1
1
  import type { NativeWindowsSecurityFacts } from "./native-binding.js";
2
2
  export declare const APP_CONTAINER_SID_RE: RegExp;
3
- export type DescriptorFacts = Pick<NativeWindowsSecurityFacts, "ownerSid" | "currentUserSid" | "currentAppContainerSid" | "daclPresent" | "isLocal" | "aceListComplete" | "unsupportedAceTypes" | "aces">;
3
+ export type DescriptorFacts = Pick<NativeWindowsSecurityFacts, "ownerSid" | "currentUserSid" | "currentAppContainerSid" | "daclPresent" | "isLocal" | "isVolumeRoot" | "aceListComplete" | "unsupportedAceTypes" | "aces">;
4
4
  export declare function unverified(message: string, cause?: unknown): never;
5
5
  /** Validate policy-free observations crossing the isolated batch transport. */
6
6
  export declare function parseWindowsOwnerAndDaclFacts(value: unknown): DescriptorFacts;
@@ -37,6 +37,7 @@ function validateDescriptor(value, allowUnknownFlags) {
37
37
  typeof value.currentUserSid !== "string" || !SID_RE.test(value.currentUserSid) ||
38
38
  (value.currentAppContainerSid != null && (typeof value.currentAppContainerSid !== "string" || !APP_CONTAINER_SID_RE.test(value.currentAppContainerSid))) ||
39
39
  typeof value.daclPresent !== "boolean" || typeof value.isLocal !== "boolean" ||
40
+ (value.isVolumeRoot !== undefined && typeof value.isVolumeRoot !== "boolean") ||
40
41
  typeof value.aceListComplete !== "boolean" || !Array.isArray(value.unsupportedAceTypes) ||
41
42
  !value.unsupportedAceTypes.every(uint32) || !Array.isArray(value.aces) ||
42
43
  !value.aces.every(ace => validateAce(ace, allowUnknownFlags))) {
@@ -117,8 +117,11 @@ is a validation failure and triggers one descriptor-close attempt. If cleanup al
117
117
  fails, the selected admission failure result is preserved. Successful opens transfer
118
118
  descriptor ownership to the caller. A custom `ioFs` supplies
119
119
  these observations; the built-in adapter uses fs-safe's native realpath wrapper.
120
- On Windows, the built-in adapter binds native root spelling before traversal,
121
- including supplied `rootRealPath`, and returns that spelling in its root receipt.
120
+ The built-in adapter binds native root spelling before traversal, including
121
+ supplied `rootRealPath`, and returns that spelling in its root receipt. If native
122
+ resolution changes the spelling (for example, case on macOS or short names on
123
+ Windows), the resolved directory must retain the captured identity. Differently
124
+ cased directories on case-sensitive filesystems remain distinct.
122
125
  This is an operation-local detection fence, not atomic confinement against a peer
123
126
  that can keep racing pathname bindings.
124
127
 
@@ -0,0 +1,392 @@
1
+ ---
2
+ title: Compiler qualification investigation
3
+ description: "Hypotheses, experiments, and acceptance gates for tsc-ts and scriptc in fs-safe."
4
+ ---
5
+
6
+ # Compiler qualification investigation
7
+
8
+ Status: investigation plan, 2026-10-17. This document does not qualify a compiler, runtime, or package change.
9
+
10
+ Investigate two separate capabilities:
11
+
12
+ 1. Use **tsc-ts** to check TypeScript and emit the existing Node.js package faster.
13
+ 2. Use **scriptc** to compile fs-safe application code into native programs while preserving its safety contracts.
14
+
15
+ The second capability changes the runtime. A faster compiler does not establish faster filesystem operations.
16
+ Keep both investigations open until measurements and contract proof support a decision.
17
+ Prepare small draft PRs. Obtain explicit maintainer approval before merging, releasing, or deploying anything.
18
+
19
+ ## Frozen baseline
20
+
21
+ | Input | Investigation identity |
22
+ | --- | --- |
23
+ | fs-safe | `0f396cd9cbd05519a9600d95daadcf827bff6b76`, package `0.26.7` |
24
+ | Package manager | `pnpm@12.10.1` |
25
+ | Official compiler | Lockfile resolves `typescript@7.0.2` |
26
+ | tsc-ts source | `7e21c1012710cc438fd30da0f22f30e276d4222b` |
27
+ | scriptc source | `66125d783b0e03af6e28843229c10b6436333c08` |
28
+ | Initial candidate | Published `tsc-ts@0.0.2` + `@tsc-ts/linux-x64-gnu@0.0.2`; release tag target `b0dbfb0ea2240b59d51de5de1aae3a35a9f3dc10`, distinct from inspected main above. |
29
+
30
+ Do not equate a release binary with a later source revision that declares the same package version.
31
+ Freeze source, lockfile, compiler libraries, runner, flags, and binary identities for each comparison.
32
+ Changes to those inputs require new evidence for the affected experiment.
33
+
34
+ The frozen [tsconfig](https://github.com/openclaw/fs-safe/blob/0f396cd9cbd05519a9600d95daadcf827bff6b76/tsconfig.json) includes 317 source TypeScript files. It excludes the test tree and JavaScript tooling.
35
+ Its contract includes strict NodeNext, ES2022 output, declarations, `noUncheckedIndexedAccess`, and `verbatimModuleSyntax`.
36
+
37
+ The [build script](https://github.com/openclaw/fs-safe/blob/0f396cd9cbd05519a9600d95daadcf827bff6b76/scripts/prepack-build.mjs) invokes the official compiler through Node.
38
+ It then prepares the Windows helper and Rust/WASM archive artifact.
39
+ Measure the compiler separately from the complete build.
40
+
41
+ The [package API checker](https://github.com/openclaw/fs-safe/blob/0f396cd9cbd05519a9600d95daadcf827bff6b76/scripts/public-api-surface.mjs) imports `typescript/unstable/ast` and `typescript/unstable/sync`.
42
+ Keep that dependency when evaluating a different compiler CLI.
43
+ Its export-name and error-code checks do not prove complete declaration-signature parity.
44
+
45
+ ## Hypotheses and experiments
46
+
47
+ | ID | Hypothesis | Experiment | Acceptance gate |
48
+ | --- | --- | --- | --- |
49
+ | H1 | tsc-ts preserves checking and emission. | Compare the official compiler and native tsc-ts on identical source, config, libraries, and consumer fixtures. | Equal exit status and diagnostics. Explain every emitted JS or declaration difference. |
50
+ | H2 | Compiler acceleration materially improves this package's workflow. | Measure compiler-only checking, emission, and complete builds separately. | Repeatable benefit on representative runners without hiding failed runs or memory regressions. |
51
+ | H3 | Candidate output preserves the published package contract. | Install each generated tarball and exercise all public subpaths, declarations, assets, and lifecycle behavior. | Consumer behavior and types remain compatible. No accidental fallback to official-compiler output. |
52
+ | H4 | scriptc supports the host mechanisms fs-safe requires. | Probe real native loading, exact identities, flags, disposal, Workers, and WASM before a whole-library build. | Preserve every required mechanism or identify a reviewed architecture change. Unsupported remains unsupported. |
53
+ | H5 | Native compilation helps actual filesystem workloads. | Measure lexical work, guarded I/O, archives, and watch workloads after semantic qualification. | Representative end-to-end gains with unchanged safety, cancellation, and resource ownership. |
54
+ | H6 | Differential findings reveal ordinary fs-safe or consumer bugs. | Reproduce each discrepancy with the official compiler and normal Node/Bun. | Establish the violated contract and repair its owner independently of compiler adoption. |
55
+ | H7 | A fork is necessary and maintainable. | Minimize an upstream blocker, identify its owner, and test the smallest patch. | Accepted requirement, reproducible defect, bounded maintenance cost, and upstream synchronization plan. |
56
+
57
+ ### Compiler parity protocol
58
+
59
+ 1. Record source and lockfile hashes, effective config, resolved input files, compiler versions, libraries, and binary hashes.
60
+ 2. Compare the locked official compiler with the candidate native executable.
61
+ 3. Use the matching TypeScript-Go oracle and Node-hosted tsc-ts to localize disagreements when needed.
62
+ 4. Compare diagnostics by category, code, relative file, span, and message.
63
+ 5. Include valid consumers and intentional type errors. A candidate must not silently omit invalid inputs.
64
+ 6. Compare emitted file inventories and bytes, including every `.js` and `.d.ts` file.
65
+ 7. Investigate differences before introducing normalization. Preserve error locations and meaningful output differences.
66
+ 8. Compile consumer fixtures against both declaration sets. Cover overloads, generics, unions, disposal, defaults, and public subpaths.
67
+
68
+ Reuse upstream corpus and package tests where they answer the same question.
69
+ The inspected [upstream benchmark](https://github.com/vercel-labs/tsc-ts/blob/7e21c1012710cc438fd30da0f22f30e276d4222b/packages/tsc-ts/scripts/bench.mjs#L77-L106) needs care.
70
+ Its whole-project timer ignores child status, and that comparison omits native tsc-ts.
71
+ Record this as an upstream follow-up. Do not use that timing table as fs-safe qualification.
72
+
73
+ ### First harness
74
+
75
+ Run `scripts/compiler-qualification.mjs` on an isolated validation host after
76
+ `pnpm install --frozen-lockfile`. Supply the candidate's native executable,
77
+ not its npm JavaScript launcher. Keep its installer provenance separately:
78
+ release, source commit, npm integrity, platform package, and download origin.
79
+
80
+ ```sh
81
+ node scripts/compiler-qualification.mjs \
82
+ --candidate /path/to/native/tsc-ts \
83
+ --output /path/to/new-evidence-directory
84
+ ```
85
+
86
+ The output directory must be new and its parent must exist. The harness resolves
87
+ the locked official `typescript@7.0.2` and its matching platform package, then
88
+ uses both native executables directly. It sets `TSC_TS_LIB_DIR` to the official
89
+ platform package's libraries and verifies the actual resolved input bytes.
90
+ The official package remains installed for the existing API checker.
91
+ The harness clears inherited `TSC_TS_*`, `GOGC`, `GOMEMLIMIT`, `GOMAXPROCS`, and
92
+ `GODEBUG` tuning before setting the library directory. It records cleared names.
93
+
94
+ The harness requires identical effective configs, source checks, one valid and
95
+ one deliberately invalid language fixture, and emitted JS/declaration inventories
96
+ and bytes before timing. It records CLI diagnostic **start-location parity**;
97
+ CLI text does not expose diagnostic end positions or lengths. Structured-span
98
+ and public declaration-consumer proof remain separate experiments.
99
+
100
+ `report.json` records hashes, versions, effective config, input/output inventories,
101
+ process status and streams, and every successful timing sample. A failed process
102
+ remains in the process log and makes the entire campaign fail. No failed campaign
103
+ qualifies performance. Initial and final hashes reject changed source, inputs,
104
+ compiler executables, libraries, manifests, and retained output.
105
+
106
+ Default timing is two warmups and ten measured runs per compiler for each of
107
+ checking and emission, interleaved in ABBA/BAAB blocks. Counts must be positive
108
+ even integers. `--single-threaded` passes the same flag to both compilers;
109
+ unsupported flags fail the campaign. `--timeout-ms` defaults to 120000 per process.
110
+ This first harness measures **wall time only**. CPU time and peak RSS remain
111
+ unmeasured. Filesystem cache state is uncontrolled. It does not qualify the
112
+ complete build, installed package, target runtime, or a compiler replacement.
113
+
114
+ Use `official/` and `candidate/` only as retained compiler-output artifacts.
115
+ Successful timing samples discard their duplicate emit directories after hashing.
116
+ The harness never writes `dist` or invokes a packaging command that rebuilds it.
117
+ Review the report for private diagnostics before publishing it.
118
+
119
+ ### Performance protocol
120
+
121
+ Use one isolated runner without competing builds. Record CPU allocation, operating system, filesystem, runtime, and resource limits.
122
+ Compare default concurrency and equal checker counts. Report single-threaded results separately.
123
+
124
+ Start with two warmups and ten measured runs per configuration, with interleaved ABBA/BAAB order.
125
+ Report median, spread, wall time, CPU time, and peak RSS. Exclude warmups from reported samples.
126
+ Classify nonzero exits, signals, crashes, incomplete output, and timeouts as failures rather than fast samples.
127
+ Use a reviewed finite deadline for each experiment and report censored runs.
128
+
129
+ Distinguish fresh-process runs from filesystem-cache-cold runs. Do not claim cold caches without controlling them.
130
+ Measure incremental no-op and one-file edits only as optional scenarios because the current build does not enable incremental compilation.
131
+ Measure repeated API/watch operation separately if adoption later includes a persistent compiler process.
132
+
133
+ Select a material benefit threshold after measuring the baseline and before evaluating candidate timings.
134
+ The initial official-only pilot took about 0.37 seconds per check or emit on a
135
+ Linux x64 c7a.8xlarge runner. For this investigation, require at least 20% median
136
+ compiler-time savings and 10% whole-build savings before proposing optional
137
+ selection. Report absolute savings and complete-check impact as well; meeting
138
+ those thresholds does not replace compatibility or maintenance review.
139
+ Prefer whole-build savings and runner cost over a microbenchmark ratio.
140
+ Do not adopt a candidate that saves negligible time while adding substantial platform or maintenance cost.
141
+
142
+ ### Package and runtime proof
143
+
144
+ Most runtime tests import source through Vitest. Those tests do not prove candidate-emitted JavaScript.
145
+ The experiment must preserve the identity of the generated package through every check.
146
+
147
+ Run the ordinary baseline through the existing repository commands:
148
+ Use a qualified validation host with frozen dependencies and the documented Rust/LLVM toolchain.
149
+
150
+ ```sh
151
+ pnpm check
152
+ pnpm native:build
153
+ pnpm package:smoke
154
+ git diff --check
155
+ ```
156
+
157
+ For candidate output, compare compiler artifacts before packaging.
158
+ Prepare required native/WASM/helper artifacts through their existing owners.
159
+ Then run `node scripts/check-pack.mjs` against the candidate's `dist` in its isolated validation checkout.
160
+ This checker packs with `--ignore-scripts`, imports public subpaths, and runs installed-package smoke checks.
161
+ It also compiles installed consumers for root overloads, JSON stores, lock callbacks,
162
+ resource disposal, and discriminated results, with strict checking and exact optional properties.
163
+ To check those same consumers with the candidate compiler as well:
164
+
165
+ ```sh
166
+ node scripts/check-pack.mjs --candidate-compiler /path/to/native/tsc-ts
167
+ ```
168
+
169
+ Both compilers use the official standard library and the same installed tarball.
170
+ An unused `@ts-expect-error` control must fail before negative consumer cases are trusted.
171
+ The fixtures are compile-only; the existing package smoke tests own runtime behavior.
172
+
173
+ Do not run `pnpm pack:check` or `pnpm check` over candidate output and claim that output survived.
174
+ Those commands rebuild with the official compiler in the frozen baseline.
175
+ Do not update `test/public-api.json` merely to accept compiler differences.
176
+
177
+ Reuse the existing security cohort plus these representative lifecycle tests:
178
+
179
+ - `test/root-read-final-fence.test.ts`
180
+ - `test/atomic-restore-error-precedence.test.ts`
181
+ - `test/atomic-restoration-falsy-settlement.test.ts`
182
+ - `test/descriptor-close-ownership.test.ts`
183
+ - `test/root-create-native-integration.test.ts`
184
+ - `test/root-mkdir-post-create.test.ts`
185
+ - `test/root-write-lifetime.test.ts`
186
+ - `test/temp-workspace-cleanup-capability.test.ts`
187
+ - `test/native-worker-fd-ownership.test.ts`
188
+ - `test/native-init-context.test.ts`
189
+ - `test/value-adapter-contracts.test.ts`
190
+ - `test/public-api-surface.test.ts`
191
+
192
+ For runtime changes, preserve the documented [platform matrix](contributing.md) and [native capabilities](native.md).
193
+ Stage qualification from Linux to macOS and Windows, then the remaining supported native targets.
194
+ Cover Node 22/24/26, native `off`/`auto`/`require`, supported Bun lanes, glibc/musl, and FreeBSD's narrower native surface.
195
+ Use the existing Bun native proofs. The full Bun suite is diagnostic, not an assumed passing compatibility gate.
196
+ Keep cross-build success separate from execution on the target operating system.
197
+
198
+ Exercise concurrent replacement with an external process where runtime scheduling could bypass an in-process race fixture.
199
+ Observe rejection, file contents, identity, descriptor ownership, cleanup, error precedence, cancellation, and event-loop delay.
200
+ Successful returns alone do not prove safety or cleanup.
201
+
202
+ ## scriptc feasibility gates
203
+
204
+ The frozen upstream explicitly excludes [N-API addons](https://github.com/vercel-labs/scriptc/blob/66125d783b0e03af6e28843229c10b6436333c08/docs/content/limitations/native-addons.mdx).
205
+ It also excludes [computed numeric flags](https://github.com/vercel-labs/scriptc/blob/66125d783b0e03af6e28843229c10b6436333c08/docs/content/limitations/builtin-apis/filesystem-flags.mdx)
206
+ and [BigInt filesystem stats](https://github.com/vercel-labs/scriptc/blob/66125d783b0e03af6e28843229c10b6436333c08/docs/content/limitations/builtin-apis/filehandle-and-descriptor-i-o.mdx).
207
+ Its filesystem operations complete synchronously before callbacks or promise continuations.
208
+ These documented limits are feasibility constraints, not newly demonstrated compiler defects.
209
+
210
+ | fs-safe owner | Required probe |
211
+ | --- | --- |
212
+ | `src/native.ts`, `src/native-binding.ts` | Lazy addon loading, async-context isolation, per-operation capabilities, and exactly-once descriptor close. |
213
+ | `src/file-identity.ts`, `src/exact-identity.ts` | BigInt identity, safe-integer rejection, Windows unknown identity, getter evaluation, and cleanup authority. |
214
+ | `src/root-read-admission.ts`, `src/root-write-verification.ts` | Fresh boundary observations, hardlink rejection, retained-parent authority, and post-operation identity fences. |
215
+ | `src/byte-budget.ts`, `src/errors.ts`, `src/async-lock.ts`, `src/suppressed-error.ts` | Small pure-TS probes for numeric boundaries, error identity, FIFO release, and cleanup-error precedence. |
216
+ | `src/archive-tar-wasm.ts`, `src/archive-zip-loader.ts` | WASM memory growth, chunk limits, physical EOF, cancellation, disposal, optional imports, and object identity. |
217
+ | `src/watch-io.ts`, `src/directory-receipt.ts` | Worker lifetime, event ordering, and WeakMap-held capability identity. |
218
+
219
+ Start with minimal unchanged-code probes, then test the actual public package.
220
+ An import-only success or native-off run does not qualify the native-required path.
221
+ Never replace BigInt identity with Number, retained descriptors with pathnames, or independent observations with cached values to make compilation succeed.
222
+
223
+ If a C ABI adapter is necessary, review its design before implementation.
224
+ Prefer reusing the existing Rust mechanisms while keeping policy in TypeScript.
225
+ Specify handle ownership, errors, cancellation, platform behavior, and ABI compatibility first.
226
+ Do not create a second filesystem policy engine.
227
+
228
+ ## Draft PR structure
229
+
230
+ Use dependency links only where one change actually requires another.
231
+
232
+ | PR | Scope | Prerequisite |
233
+ | --- | --- | --- |
234
+ | P1 | Compiler parity and measurement harness. Preserve official build defaults. | Frozen input and output identities, trustworthy failure accounting. |
235
+ | P2 | Candidate-package and declaration consumer proof missing from existing checks. | P1 artifact contract. |
236
+ | P3a, P3b, ... | One confirmed fs-safe or OpenClaw consumer defect per PR. | Minimal normal-runtime reproduction and owner agreement. |
237
+ | P4 | Optional compiler selection through the existing build owner. | H1-H3 pass and material measured benefit. Retain the official API dependency. |
238
+ | R1, R2, ... | Narrow runtime compatibility fixes or an accepted ABI design. | H4 findings, security-boundary review, and relevant platform proof. |
239
+ | U1, U2, ... | Reduced upstream regression tests and fixes. | Confirmed compiler/runtime owner and failing supported contract. |
240
+
241
+ P1 -> P2 -> P4 forms the compiler stack. Verified fixes join that dependency chain only when necessary.
242
+ Runtime work forms a separate reviewed stack. Do not couple ordinary Node fixes to compiler adoption.
243
+ Coordinate OpenClaw consumer changes with their existing owners and reuse shared benchmark evidence where inputs match.
244
+
245
+ ## Fork decision
246
+
247
+ Choose the owning project: tsc-ts for checker/emitter defects, scriptc for lowering/runtime defects, fs-safe for its own invariant violations.
248
+ A documented unsupported feature is an architecture discussion before it is a fork justification.
249
+
250
+ Before creating a fork:
251
+
252
+ 1. Preserve a minimal reproducer with expected and actual behavior.
253
+ 2. Check the pinned version and current upstream for an existing fix.
254
+ 3. Show that the blocker prevents an accepted fs-safe requirement.
255
+ 4. Estimate the patch surface, platform tests, release ownership, and upstream synchronization cost.
256
+ 5. Define an upstream contribution route and a fork retirement condition.
257
+
258
+ ## Completion evidence
259
+
260
+ This plan is the first deliverable. It does not complete compiler qualification or runtime adoption.
261
+ Every experiment must report `pass`, `fail`, `unsupported`, or `not run`, with its exact inputs and evidence.
262
+ Keep the full objective open while required tests, verified fixes, stack preparation, or architecture decisions remain unresolved.
263
+
264
+ The final handoff must identify measured benefits, semantic differences, package/platform coverage, remaining blockers, and every draft PR dependency.
265
+ Keep source validation, CI status, merge approval, release publication, and deployed behavior separate.
266
+ No landing occurs without explicit maintainer approval.
267
+
268
+ ## Initial Linux pilot
269
+
270
+ On Node 24.19.0 with pnpm 12.10.1, the published tsc-ts 0.0.2 Linux GNU binary
271
+ passed the first comparison: 508 identical resolved input hashes, 634 identical
272
+ emitted JavaScript/declaration files, and matching valid/invalid fixture results.
273
+ The official-versus-official control also passed. Each pilot used two warmups
274
+ and two measured samples per compiler per mode. These small pilots validate
275
+ harness execution; they do not establish a speedup or full package compatibility.
276
+
277
+ Native executable SHA-256 identities:
278
+
279
+ - Official TypeScript 7.0.2: `4f2de678286401759b3fb4475bafe35b8f32b4b3a07d92642bbf37eadc9b34a4`
280
+ - tsc-ts 0.0.2 Linux GNU: `fb24c9cc43533dc004b9dfc9547d7066cea348b807c3239f8e31c7ba062017b6`
281
+
282
+ The installed package's launcher selects `lib/tsc-ts` (`tsc-ts.exe` on Windows).
283
+ Record the npm package integrity from the experiment's installation lockfile;
284
+ do not infer binary provenance from its `Version 7.0.2` CLI output alone.
285
+
286
+ ## Linux compiler and package results
287
+
288
+ The 2026-10-17 campaign used the same frozen source and executable identities
289
+ above, Linux x64 on an AWS c7a.8xlarge (32 logical CPUs), Node 24.19.0, and
290
+ pnpm 12.10.1. Each comparison used two warmups and ten measured samples per
291
+ compiler, in balanced ABBA/BAAB order. OS caches were uncontrolled.
292
+
293
+ | Invocation | Official median | tsc-ts median | Speedup |
294
+ | --- | --- | --- | --- |
295
+ | Default check | 387 ms | 114 ms | 3.40x |
296
+ | Default emit | 400 ms | 124 ms | 3.24x |
297
+ | Single-threaded check | 973 ms | 704 ms | 1.38x |
298
+ | Single-threaded emit | 1,041 ms | 863 ms | 1.21x |
299
+ | Complete warm build | 1,324 ms | 1,031 ms | 1.28x |
300
+
301
+ Both compiler configurations preserved all 508 resolved inputs and 634 emitted
302
+ JavaScript/declaration files. The complete build used the existing prepack
303
+ steps with only the compiler invocation substituted in a temporary runner.
304
+ Dependencies and Rust/.NET/WASM toolchains were warm; `dist` was recreated on
305
+ each invocation. All 636 complete-build artifacts matched across every run.
306
+ The median build saving was 22.1%, approximately 293 ms.
307
+
308
+ GNU time measured median build user CPU of 2.890/1.955 seconds, system CPU of
309
+ 0.715/0.475 seconds, and maximum RSS of 310,372/261,192 KiB (official/candidate).
310
+ Maximum RSS is a process high-water mark, not summed concurrent memory.
311
+ Compiler-only CPU and RSS were not measured.
312
+
313
+ Candidate output also passed the installed-package checker without a rebuild:
314
+ public imports and API manifest, guest Python/rename protocol, directory receipt
315
+ runtime behavior, Root.walk declarations, and the three consumer fixtures under
316
+ both compilers. The complete `dist` inventory was unchanged after packing.
317
+
318
+ This clears the provisional Linux performance gate. It does not qualify cold
319
+ builds, macOS/Windows compiler binaries, other runtimes, or every native mode.
320
+ Identical emitted JavaScript provides no evidence of faster filesystem I/O.
321
+ Keep compiler adoption separate from these qualification tools.
322
+
323
+ ## Platform and clean-output campaign
324
+
325
+ `compiler-qualification.yml` runs the published tsc-ts 0.0.2 native package on
326
+ Linux, macOS, and Windows with Node 24.19.0. It records npm lockfile integrity,
327
+ native executable hashes, the exact checkout commit, and toolchain versions.
328
+ Windows uses the existing x64 CI runner; upstream 0.0.2 has no Windows ARM64
329
+ package. The npm launcher gets a separate smoke check using its default libraries.
330
+ Parity and package consumers continue to use the official TypeScript libraries.
331
+
332
+ The campaign runs the compiler parity harness in default and single-threaded
333
+ modes, with two warmups and ten measured samples per arm. Complete builds use
334
+ the existing prepack owner with an explicit native compiler for both arms.
335
+ Ordinary builds retain the official compiler; this option is experimental
336
+ qualification tooling, not an adoption decision. Strict toolchain mode requires
337
+ the .NET helper build and cannot reuse the staged executable as a fallback.
338
+
339
+ Warm complete builds retain Rust/.NET outputs and recreate `dist`. Clean-output
340
+ builds use a fresh Cargo target and remove only the helper's `bin` and `obj`
341
+ directories before each invocation. Dependencies and their download stores stay
342
+ installed; OS and dependency caches are uncontrolled. These are **clean build
343
+ outputs**, not cold-machine or cold-page-cache measurements. Each arm has two
344
+ warmups, ten measured warm builds, and six measured clean-output builds, in
345
+ balanced ABBA/BAAB order. Timings cover the complete build, excluding hashing,
346
+ output cleanup and installed-package checks. They measure wall time only.
347
+
348
+ Every build must match all JavaScript, declaration, helper, and WASM bytes on
349
+ that runner. Cross-OS binary equality is not required. Source, build scripts,
350
+ lockfiles, generated helper input, compiler binaries, and libraries are checked
351
+ for mutation. Both arms in each cache mode get installed-package proof without
352
+ a rebuild, followed by another complete `dist` hash comparison.
353
+
354
+ The runner accepts only its disposable Actions checkout. The isolated job's
355
+ 45-minute deadline owns cancellation of the full Cargo/.NET process tree;
356
+ it does not use a parent-only build timeout and then continue measuring.
357
+ It uploads at most three redacted JSON reports per platform, each capped at
358
+ 4 MiB, for 14 days. Generated binaries and compiler installations stay temporary.
359
+ A failed campaign cannot establish a performance claim, even if it has samples.
360
+
361
+ OpenClaw is a separate qualification target. Its declaration build uses
362
+ `typescript/unstable` API transport and `program.emitToString`; a faster CLI
363
+ typecheck does not establish compatibility with that build owner. Reuse the
364
+ existing compiler lane's exact-source evidence before proposing an API/emission
365
+ adapter or a paired `build:strict-smoke` comparison.
366
+
367
+ ### Separate scriptc runtime probe
368
+
369
+ Published scriptc 0.3.5 was installed on the same Linux host. Its native
370
+ control program built and ran after the host's missing Clang was installed.
371
+ The executable SHA-256 was
372
+ `b7087211a304c049de53fcaac40564e9a6e9adac6b4055c895ab340e82d47a84`.
373
+ Its [release tag](https://github.com/vercel-labs/scriptc/tree/973362d0d2be9b948b3e0d59d77ff3705278f24c)
374
+ is a source reference, not an attestation of the npm binary.
375
+
376
+ All three filesystem probes ran successfully in Node. With scriptc coverage
377
+ configured to fail on blockers:
378
+
379
+ - `statSync(path, { bigint: true })` failed preflight with `SC0001`: the
380
+ supported declaration accepts only one argument.
381
+ - Passing computed numeric open flags failed lowering with `SC2020`.
382
+ - An installed-package consumer using `root(path).readBytes(...)` failed
383
+ preflight with missing `BigIntStats` and `NodeJS.Platform` declarations
384
+ (`SC0001`), plus the package's computed/escaping `createRequire` use (`SC2013`).
385
+
386
+ These results agree with the release's documented
387
+ [descriptor/identity limits](https://github.com/vercel-labs/scriptc/blob/973362d0d2be9b948b3e0d59d77ff3705278f24c/docs/content/limitations/builtin-apis/filehandle-and-descriptor-i-o.mdx)
388
+ and [native-addon boundary](https://github.com/vercel-labs/scriptc/blob/973362d0d2be9b948b3e0d59d77ff3705278f24c/docs/content/limitations/native-addons.mdx).
389
+ They block runtime adoption; they do not establish a newly broken supported
390
+ contract or justify a fork. No fs-safe safety policy was weakened for these probes.
391
+ Further native-port design requires an exact-identity and addon/FFI solution
392
+ before broader runtime tests can become meaningful.