@animus-ui/extract 0.1.25 → 0.1.26

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 (83) hide show
  1. package/crates/extract-v2/animus-extract-v2.darwin-arm64.node +0 -0
  2. package/crates/extract-v2/animus-extract-v2.linux-arm64-gnu.node +0 -0
  3. package/crates/extract-v2/animus-extract-v2.linux-x64-gnu.node +0 -0
  4. package/crates/extract-v2/index.d.ts +101 -6
  5. package/dist/index.cjs +48 -5
  6. package/dist/index.mjs +2 -2
  7. package/dist/pipeline/alias-agreement.d.ts +107 -0
  8. package/dist/pipeline/alias-agreement.d.ts.map +1 -0
  9. package/dist/pipeline/analyze-project-args.d.ts +7 -2
  10. package/dist/pipeline/analyze-project-args.d.ts.map +1 -1
  11. package/dist/pipeline/asset-copies.d.ts +20 -0
  12. package/dist/pipeline/asset-copies.d.ts.map +1 -0
  13. package/dist/pipeline/built-in-asset-types.d.ts +9 -0
  14. package/dist/pipeline/built-in-asset-types.d.ts.map +1 -0
  15. package/dist/pipeline/core-options.d.ts +31 -7
  16. package/dist/pipeline/core-options.d.ts.map +1 -1
  17. package/dist/pipeline/discover-packages.d.ts +98 -12
  18. package/dist/pipeline/discover-packages.d.ts.map +1 -1
  19. package/dist/pipeline/engine-adapter.d.ts +41 -3
  20. package/dist/pipeline/engine-adapter.d.ts.map +1 -1
  21. package/dist/pipeline/engine-retirement.d.ts +7 -1
  22. package/dist/pipeline/engine-retirement.d.ts.map +1 -1
  23. package/dist/pipeline/extension-policy.d.ts +95 -0
  24. package/dist/pipeline/extension-policy.d.ts.map +1 -0
  25. package/dist/pipeline/host-assets.d.ts +32 -0
  26. package/dist/pipeline/host-assets.d.ts.map +1 -0
  27. package/dist/pipeline/host-expansions.d.ts +92 -0
  28. package/dist/pipeline/host-expansions.d.ts.map +1 -0
  29. package/dist/pipeline/host-extensions.d.ts +64 -0
  30. package/dist/pipeline/host-extensions.d.ts.map +1 -0
  31. package/dist/pipeline/host-package-resolver.d.ts +40 -0
  32. package/dist/pipeline/host-package-resolver.d.ts.map +1 -0
  33. package/dist/pipeline/index.d.ts +32 -8
  34. package/dist/pipeline/index.d.ts.map +1 -1
  35. package/dist/pipeline/kit-publication.d.ts.map +1 -1
  36. package/dist/pipeline/manifest-diagnostics.d.ts +25 -4
  37. package/dist/pipeline/manifest-diagnostics.d.ts.map +1 -1
  38. package/dist/pipeline/manifest-schema.d.ts +16 -0
  39. package/dist/pipeline/manifest-schema.d.ts.map +1 -1
  40. package/dist/pipeline/output-targets.d.ts +52 -0
  41. package/dist/pipeline/output-targets.d.ts.map +1 -0
  42. package/dist/pipeline/probe-evidence.d.ts +58 -0
  43. package/dist/pipeline/probe-evidence.d.ts.map +1 -0
  44. package/dist/pipeline/run-analysis.d.ts +26 -0
  45. package/dist/pipeline/run-analysis.d.ts.map +1 -1
  46. package/dist/pipeline/runtime-import.d.ts +10 -0
  47. package/dist/pipeline/runtime-import.d.ts.map +1 -0
  48. package/dist/pipeline/served-files.d.ts +16 -0
  49. package/dist/pipeline/served-files.d.ts.map +1 -0
  50. package/dist/pipeline/source-corpus.d.ts +4 -0
  51. package/dist/pipeline/source-corpus.d.ts.map +1 -1
  52. package/dist/pipeline/source-ingestion.d.ts +37 -5
  53. package/dist/pipeline/source-ingestion.d.ts.map +1 -1
  54. package/dist/pipeline/system-config.d.ts +18 -0
  55. package/dist/pipeline/system-config.d.ts.map +1 -1
  56. package/dist/pipeline/system-instances.d.ts +27 -0
  57. package/dist/pipeline/system-instances.d.ts.map +1 -0
  58. package/dist/pipeline/tsconfig-paths.d.ts +4 -0
  59. package/dist/pipeline/tsconfig-paths.d.ts.map +1 -1
  60. package/dist/pipeline/vite-expansions.d.ts +40 -0
  61. package/dist/pipeline/vite-expansions.d.ts.map +1 -0
  62. package/dist/pipeline/webpack-expansions.d.ts +55 -0
  63. package/dist/pipeline/webpack-expansions.d.ts.map +1 -0
  64. package/dist/pipeline/workspace-packages.d.ts +13 -0
  65. package/dist/pipeline/workspace-packages.d.ts.map +1 -0
  66. package/dist/{pipeline-YGWpNr_Z.mjs → pipeline-BzMQ_Z0M.mjs} +2285 -773
  67. package/dist/{pipeline-DOgKR4VZ.cjs → pipeline-DyUf-eKZ.cjs} +2556 -768
  68. package/dist/session/coalescing-window.d.ts +34 -0
  69. package/dist/session/coalescing-window.d.ts.map +1 -0
  70. package/dist/session/extraction-session.d.ts +297 -35
  71. package/dist/session/extraction-session.d.ts.map +1 -1
  72. package/dist/session/index.d.ts +7 -3
  73. package/dist/session/index.d.ts.map +1 -1
  74. package/dist/session/published-set.d.ts +6 -1
  75. package/dist/session/published-set.d.ts.map +1 -1
  76. package/dist/session/resolve-packages.d.ts +9 -5
  77. package/dist/session/resolve-packages.d.ts.map +1 -1
  78. package/dist/session/singleton.d.ts +218 -53
  79. package/dist/session/singleton.d.ts.map +1 -1
  80. package/dist/session/turbopack-orchestrator.d.ts.map +1 -1
  81. package/dist/session.cjs +1462 -510
  82. package/dist/session.mjs +1430 -489
  83. package/package.json +4 -4
package/dist/session.mjs CHANGED
@@ -1,7 +1,106 @@
1
- import { $ as discoverFiles, At as surfaceManifestDiagnostics, B as unresolvableIncludesMessage, Bt as substituteSheetAssets, C as resolveAssetFile, D as engineModuleParser, Dt as isDeletedSource, E as collectExternalPackageSources, Gt as assembleStylesheet, H as createSourceIdentity, Ht as contentHash, It as findSheetAssetSpecifiers, Jt as __require, Lt as generatedModuleCode, M as firstOwners, Mt as unreadableSourceDiagnostic, N as importedKitDiagnostics, O as excludeCollectedPackages, P as isExcludedPackageRelativePath, Rt as reportSurvivingAssetPlaceholders, S as enforceExternalTokenContracts, T as createSourceCorpus, U as isPathWithinRoot, V as walkPackageSources, Vt as parseInternalWire, W as sharesVolumeRoot, X as isJsonString, Y as isJsonBlock, _ as runProjectAnalysis, b as resolveLightningTargets, ct as resolveMode, f as serializeStaticCss, ft as createV2EngineApi, g as clearEngineCache, gt as projectExternalFileOwners, i as snapshotFilePlans, j as findPackageRoot, k as extractSystemFilePackages, lt as resolveVerbosity, r as hashReplacementPlans, s as buildSystemPropsModule, st as createExcludeMatcher, t as toWatchKeys, u as loadSystemConfig, vt as DEFAULT_EXTENSIONS, y as postProcessCss, z as staleDistIncludesMessage } from "./pipeline-YGWpNr_Z.mjs";
1
+ import { $ as excludeCollectedPackages, $t as createSourceIdentity, A as postProcessCss, Bn as __require, C as serializeStaticCss, Ct as tsconfigChainPaths, D as clearEngineCache, Dn as findSheetAssetSpecifiers, Et as extensionPolicyJson, Ft as pruneStaleAssets, G as discoveryResolver, I as createSourceCorpus, It as writeAssetCopy, J as packageInstanceFinding, Jt as outputSourceGuard, K as AliasAgreementCache, Kt as createExcludeMatcher, Ln as assembleStylesheet, M as buildPathAliasesJson, Mt as discoverFiles, N as enforceExternalTokenContracts, Nn as contentHash, Nt as DEFAULT_RUNTIME_IMPORT, O as runProjectAnalysis, On as generatedModuleCode, P as resolveAssetFile, Pn as parseInternalWire, Pt as assertRuntimeImportSuppliesTerminals, Qt as unmatchedExcludeLines, Sn as surfaceManifestDiagnostics, St as readTsconfigAliasPairs, T as stableStringify, X as bareSpecifierPackageName, Xt as resolveMode, Y as packageReadRecord, Yt as outputSourceMessage, Z as collectExternalPackageSources, Zt as resolveVerbosity, _ as buildSystemPropsModule, _t as declaredWorkspacePackages, at as isExcludedPackageRelativePath, b as loadSystemConfig, cn as adaptedOriginals, d as toWatchKeys, dn as isEngineTransformExtension, en as isPathWithinRoot, et as extractSystemFilePackages, f as diffFilePlans, gn as SYSTEM_INSTANCE_MISMATCH, gt as walkPackageSources, ht as unresolvableIncludesMessage, it as importedKitDiagnostics, j as resolveLightningTargets, jn as substituteSheetAssets, kn as reportSurvivingAssetPlaceholders, ln as DEFAULT_EXTENSIONS, lt as nativeKitSourceRoot, m as snapshotFilePlans, nn as createV2EngineApi, nt as findPackageRoot, on as projectExternalFileOwners, ot as isPackageSpecifier, p as hashReplacementPlans, pt as staleDistIncludesMessage, q as compareAliases, rt as firstOwners, s as builtInAssetClassification, tn as sharesVolumeRoot, ut as nativePackageLocator, wn as unreadableSourceDiagnostic, wt as BUILT_IN_EXTENSION_POLICY, x as systemLoadReads, xn as severityFor, yn as isDeletedSource } from "./pipeline-BzMQ_Z0M.mjs";
2
2
  import { randomUUID } from "crypto";
3
3
  import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, watch, writeFileSync } from "fs";
4
- import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "path";
4
+ import { basename, dirname, extname, isAbsolute, join, posix, relative, resolve, sep } from "path";
5
+ //#region session/coalescing-window.ts
6
+ /**
7
+ * The one coalescing window (SA19). It opens on the first pending event and
8
+ * fires a fixed interval later: a later event joins the open window and never
9
+ * pushes it back, so a steady stream of edits cannot starve the work it
10
+ * coalesces. Builds, startup and explicit flushes bypass it through `flush`.
11
+ * Hosts add no timers of their own.
12
+ */
13
+ const COALESCING_WINDOW_MS = 75;
14
+ const SYSTEM_CLOCK = {
15
+ schedule: (fn, ms) => {
16
+ const timer = setTimeout(fn, ms);
17
+ timer.unref?.();
18
+ return timer;
19
+ },
20
+ cancel: (timer) => clearTimeout(timer)
21
+ };
22
+ var CoalescingWindow = class {
23
+ fire;
24
+ windowMs;
25
+ clock;
26
+ timer = null;
27
+ constructor(fire, windowMs = 75, clock = SYSTEM_CLOCK) {
28
+ this.fire = fire;
29
+ this.windowMs = windowMs;
30
+ this.clock = clock;
31
+ }
32
+ /** A pending event: opens the window, or joins the open one. */
33
+ note() {
34
+ if (this.timer !== null) return;
35
+ this.timer = this.clock.schedule(() => {
36
+ this.timer = null;
37
+ return this.fire();
38
+ }, this.windowMs);
39
+ }
40
+ /** Fires now, closing any open window: the bypass. */
41
+ flush() {
42
+ this.cancel();
43
+ return this.fire();
44
+ }
45
+ /** Closes an open window without firing it. */
46
+ cancel() {
47
+ if (this.timer === null) return;
48
+ this.clock.cancel(this.timer);
49
+ this.timer = null;
50
+ }
51
+ };
52
+ //#endregion
53
+ //#region pipeline/system-instances.ts
54
+ /**
55
+ * The system and the analysis read one instance of each kit (spec §2.2
56
+ * A4). Discovery asks the host which instance a kit specifier names; the
57
+ * system loader resolves the system file's imports itself. Until the loader
58
+ * takes the host's instance, a kit whose analysed instance differs from the
59
+ * one the system evaluated is refused: its components would be extracted
60
+ * against another instance's vocabulary.
61
+ */
62
+ function realPath(path) {
63
+ try {
64
+ return realpathSync(path);
65
+ } catch {
66
+ return path;
67
+ }
68
+ }
69
+ /**
70
+ * One error for each system-file kit specifier whose analysed package
71
+ * instance is not the one the system load evaluated: the file the loader's
72
+ * own resolution names must be among the files the load read, and lie in
73
+ * another package root than the instance discovery read for its package.
74
+ */
75
+ function systemInstanceMismatches(input) {
76
+ const evaluated = new Set(input.evaluated.map(realPath));
77
+ const shown = (path) => relative(input.rootDir, path).split(sep).join("/");
78
+ const diagnostics = [];
79
+ for (const specifier of input.specifiers) {
80
+ if (!isPackageSpecifier(specifier)) continue;
81
+ const analysed = input.instances[bareSpecifierPackageName(specifier)];
82
+ if (analysed === void 0) continue;
83
+ let file;
84
+ try {
85
+ file = input.loaderResolve(specifier);
86
+ } catch {
87
+ continue;
88
+ }
89
+ if (file === null || !evaluated.has(realPath(file))) continue;
90
+ const system = realPath(findPackageRoot(realPath(file)));
91
+ if (system === analysed) continue;
92
+ diagnostics.push({
93
+ file: specifier,
94
+ component: "kit",
95
+ kind: "error",
96
+ message: `the system evaluates '${specifier}' from ${shown(system)}, but the host bundler resolves it to ${shown(analysed)}, which extraction analyses, so its components would be extracted against another instance's vocabulary — make the bundler and the system resolve '${specifier}' to one instance`,
97
+ code: SYSTEM_INSTANCE_MISMATCH,
98
+ severity: severityFor(SYSTEM_INSTANCE_MISMATCH)
99
+ });
100
+ }
101
+ return diagnostics;
102
+ }
103
+ //#endregion
5
104
  //#region session/session-paths.ts
6
105
  /**
7
106
  * Session artifact names, path derivations, and artifact shapes. Imports no
@@ -168,9 +267,11 @@ function readCliLockRecord(dir) {
168
267
  if (!isPublishedSetRecord(candidate) || !isPublishedSetNumber(candidate.pid)) return { kind: "indeterminate" };
169
268
  const startedAt = candidate.startedAt;
170
269
  const heartbeatAt = candidate.heartbeatAt;
270
+ const build = candidate.build;
171
271
  const record = { pid: candidate.pid };
172
272
  if (isPublishedSetString(startedAt)) record.startedAt = startedAt;
173
273
  if (isPublishedSetString(heartbeatAt)) record.heartbeatAt = heartbeatAt;
274
+ if (isPublishedSetString(build)) record.build = build;
174
275
  return {
175
276
  kind: "held",
176
277
  record
@@ -201,12 +302,14 @@ function checkLockLiveness(record) {
201
302
  reason: `holder pid ${record.pid} is running but its lock heartbeat (${record.heartbeatAt}) is ${Math.round(ageMs / 1e3)}s old, past the ${CLI_LOCK_STALE_AFTER_MS / 1e3}s staleness window — the pid names a different process now`
202
303
  };
203
304
  }
204
- function lockRecordBytes(startedAt) {
205
- return JSON.stringify({
305
+ function lockRecordBytes(startedAt, build) {
306
+ const record = {
206
307
  pid: process.pid,
207
308
  startedAt,
208
309
  heartbeatAt: (/* @__PURE__ */ new Date()).toISOString()
209
- });
310
+ };
311
+ if (build !== void 0) record.build = build;
312
+ return JSON.stringify(record);
210
313
  }
211
314
  const HEARTBEAT_TEMP_PREFIX = ".lock-heartbeat-";
212
315
  function holdDirectoryClaim(dir, options = {}) {
@@ -224,9 +327,9 @@ function holdDirectoryClaim(dir, options = {}) {
224
327
  const stamp = () => {
225
328
  try {
226
329
  if (rename) {
227
- writeFileSync(tempPath, lockRecordBytes(startedAt));
330
+ writeFileSync(tempPath, lockRecordBytes(startedAt, options.build));
228
331
  renameSync(tempPath, path);
229
- } else writeFileSync(path, lockRecordBytes(startedAt));
332
+ } else writeFileSync(path, lockRecordBytes(startedAt, options.build));
230
333
  } catch (error) {
231
334
  stop();
232
335
  if (isMissingFile(error)) return;
@@ -256,320 +359,424 @@ function collectSessionAssets(sessionDir) {
256
359
  }));
257
360
  }
258
361
  //#endregion
259
- //#region session/resolve-packages.ts
260
- /** An exports target under the `import` condition, then `default`, as the
261
- * system loader reads it; an array offers its targets in order. */
262
- function importTarget(value) {
263
- if (isJsonString(value)) return value;
264
- if (Array.isArray(value)) {
265
- for (const entry of value) {
266
- const target = importTarget(entry);
267
- if (target !== null) return target;
268
- }
269
- return null;
270
- }
271
- if (!isJsonBlock(value)) return null;
272
- return importTarget(value.import) ?? importTarget(value.default);
273
- }
274
- /** The exports entry for `subpath`: an exact key answers alone; otherwise
275
- * the pattern with the longest prefix, then the longest suffix, wins. */
276
- function exportsTarget(exports, subpath) {
277
- const map = isJsonBlock(exports) && Object.keys(exports).some((k) => k.startsWith(".")) ? exports : { ".": exports };
278
- if (subpath in map) return importTarget(map[subpath]);
279
- let best = null;
280
- for (const [pattern, value] of Object.entries(map)) {
281
- const star = pattern.indexOf("*");
282
- if (star === -1 || pattern.indexOf("*", star + 1) !== -1) continue;
283
- const prefix = pattern.slice(0, star);
284
- const suffix = pattern.slice(star + 1);
285
- if (!subpath.startsWith(prefix) || !subpath.endsWith(suffix) || subpath.length < prefix.length + suffix.length) continue;
286
- if (!best || prefix.length > best.prefix.length || prefix.length === best.prefix.length && suffix.length > best.suffix.length) best = {
287
- prefix,
288
- suffix,
289
- value
290
- };
291
- }
292
- if (!best) return null;
293
- const matched = subpath.slice(best.prefix.length, subpath.length - best.suffix.length);
294
- return importTarget(best.value)?.replaceAll("*", matched) ?? null;
295
- }
296
- /** The entry an ES import of `name` resolves to through the package's
297
- * `exports`, under the `import` condition, then `default`: what the Vite
298
- * host and the system loader resolve. Null without an `exports` field, or
299
- * when no target exists. */
300
- function resolveImportEntry(rootDir, name) {
301
- const packageName = name.split("/").slice(0, name.startsWith("@") ? 2 : 1).join("/");
302
- const subpath = `.${name.slice(packageName.length)}`;
303
- for (let dir = rootDir;; dir = dirname(dir)) {
304
- const manifest = join(dir, "node_modules", packageName, "package.json");
305
- if (existsSync(manifest)) {
306
- let exports;
307
- try {
308
- const parsed = parseInternalWire(readFileSync(manifest, "utf-8"), `${manifest} (a package manifest)`);
309
- exports = isJsonBlock(parsed) ? parsed.exports : void 0;
310
- } catch {
311
- return null;
312
- }
313
- if (exports === void 0 || exports === null) return null;
314
- const target = exportsTarget(exports, subpath);
315
- if (target === null) return null;
316
- const entryPath = resolve(dirname(manifest), target);
317
- return existsSync(entryPath) ? realpathSync(entryPath) : null;
318
- }
319
- if (dir === dirname(dir)) return null;
320
- }
321
- }
322
- function resolvePackagesByName(rootDir, names) {
323
- if (names.length === 0) return {};
324
- const nameSet = new Set(names);
325
- const resolved = /* @__PURE__ */ new Set();
326
- const packageMap = {};
327
- try {
328
- const rootPkg = JSON.parse(readFileSync(join(rootDir, "package.json"), "utf-8"));
329
- const workspaces = Array.isArray(rootPkg.workspaces) ? rootPkg.workspaces : rootPkg.workspaces?.packages ?? [];
330
- for (const ws of workspaces) {
331
- const wsDir = resolve(rootDir, ws);
332
- if (!existsSync(wsDir)) continue;
333
- try {
334
- const pkg = JSON.parse(readFileSync(join(wsDir, "package.json"), "utf-8"));
335
- const name = pkg.name || "";
336
- if (nameSet.has(name)) {
337
- const main = pkg.main || pkg.module || "index.ts";
338
- const entryPath = resolve(wsDir, main);
339
- if (existsSync(entryPath)) {
340
- packageMap[name] = relative(rootDir, entryPath);
341
- resolved.add(name);
342
- }
343
- }
344
- } catch {}
345
- }
346
- } catch {}
347
- for (const name of nameSet) {
348
- if (resolved.has(name)) continue;
349
- const importEntry = resolveImportEntry(rootDir, name);
350
- if (importEntry !== null) {
351
- packageMap[name] = relative(rootDir, importEntry);
352
- continue;
353
- }
354
- try {
355
- const entryPath = __require.resolve(name, { paths: [rootDir] });
356
- packageMap[name] = relative(rootDir, entryPath);
357
- } catch {}
358
- }
359
- return packageMap;
360
- }
361
- //#endregion
362
362
  //#region session/singleton.ts
363
363
  /**
364
- * globalThis-backed so the ESM plugin copy and the CJS webpack-loader copy
365
- * share one store; module scope alone gives each copy its own instance.
364
+ * A build's shared state lives in one registry on `globalThis`, keyed by its
365
+ * `SessionKey`, so the ESM plugin copy and the CJS webpack-loader copy of
366
+ * this module find the same entry, and two builds of one root never share
367
+ * one. A reader of a build no participant has joined gets the empty value;
368
+ * a writer must have joined it.
366
369
  */
367
- const MANIFEST_KEY = "__animus_manifest_json__";
368
- const ANALYSIS_STARTED_KEY = "__animus_analysis_started__";
369
- const SHARED_CSS_KEY = "__animus_shared_css__";
370
- const SHARED_SYSTEM_PROPS_KEY = "__animus_shared_system_props__";
371
- const SHARED_EXTERNAL_DIRS_KEY = "__animus_external_pkg_dirs__";
372
- const SHARED_EXTERNAL_ENTRIES_KEY = "__animus_external_source_entries__";
373
- const SHARED_EXTERNAL_SIDE_EFFECTS_KEY = "__animus_external_side_effects__";
374
- const ANALYZED_HASHES_KEY = "__animus_analyzed_hashes__";
375
- const REPLACEMENT_EPOCH_KEY = "__animus_replacement_epoch__";
376
- const WATCH_TRANSACTION_KEY = "__animus_watch_transaction__";
377
- const PROCESS_SESSION_ID_KEY = "__animus_process_session_id__";
378
- const SESSION_ARTIFACT_DIR_KEY = "__animus_session_artifact_dir__";
379
- const OWNING_WATCH_SESSION_KEY = "__animus_owning_watch_session__";
380
- const EXCLUSIVE_SESSION_OWNER_KEY = "__animus_exclusive_session_owner__";
381
- const ENGINE_KEY = "__animus_engine__";
382
- const V2_ENGINE_KEY = "__animus_v2_engine__";
383
- const V2_SENT_SOURCES_KEY = "__animus_v2_sent_sources__";
384
- const V2_DRIFT_WARNED_KEY = "__animus_v2_drift_warned__";
385
- const ENGINE_API_OVERRIDE_KEY = "__animus_engine_api_override__";
386
- const singletonStore = globalThis;
387
- const isSingletonString = (value) => {
388
- if (Object(value) === value) return false;
389
- try {
390
- String.prototype.valueOf.call(value);
391
- return true;
392
- } catch {
393
- return false;
394
- }
395
- };
396
- function getManifestJson() {
397
- return singletonStore[MANIFEST_KEY];
370
+ function joined(key) {
371
+ const state = findSession(key);
372
+ if (state === void 0) throw new Error(`[animus] no participant has joined build ${key.build} of ${key.root}`);
373
+ return state;
374
+ }
375
+ function getManifestJson(key) {
376
+ return findSession(key)?.manifestJson ?? null;
398
377
  }
399
- function setManifestJson(json) {
400
- singletonStore[MANIFEST_KEY] = json;
378
+ function setManifestJson(key, json) {
379
+ joined(key).manifestJson = json;
401
380
  }
402
381
  /**
403
382
  * A latch over the most recently STARTED analysis, never cleared on settle:
404
383
  * non-null means one was scheduled. In-flight lives in getWatchTransaction.
405
384
  */
406
- function getAnalysisStartedPromise() {
407
- return singletonStore[ANALYSIS_STARTED_KEY];
385
+ function getAnalysisStartedPromise(key) {
386
+ return findSession(key)?.analysisStarted ?? null;
408
387
  }
409
- function setAnalysisStartedPromise(promise) {
410
- singletonStore[ANALYSIS_STARTED_KEY] = promise;
388
+ function setAnalysisStartedPromise(key, promise) {
389
+ joined(key).analysisStarted = promise;
411
390
  }
412
- function resetAnalysisStartedPromise() {
413
- singletonStore[ANALYSIS_STARTED_KEY] = null;
391
+ function resetAnalysisStartedPromise(key) {
392
+ const state = findSession(key);
393
+ if (state !== void 0) state.analysisStarted = null;
414
394
  }
415
- function getSharedCss() {
416
- return singletonStore[SHARED_CSS_KEY] || "";
395
+ /** True for the build's first caller only: the one that reports its
396
+ * unmatched user excludes. */
397
+ function claimUnmatchedExcludeReport(key) {
398
+ const state = joined(key);
399
+ if (state.unmatchedExcludesReported) return false;
400
+ state.unmatchedExcludesReported = true;
401
+ return true;
417
402
  }
418
- function setSharedCss(css) {
419
- singletonStore[SHARED_CSS_KEY] = css;
403
+ function getSharedCss(key) {
404
+ return findSession(key)?.sharedCss ?? "";
420
405
  }
421
- function getSharedSystemProps() {
422
- return singletonStore[SHARED_SYSTEM_PROPS_KEY] || "";
406
+ function setSharedCss(key, css) {
407
+ joined(key).sharedCss = css;
423
408
  }
424
- function setSharedSystemProps(content) {
425
- singletonStore[SHARED_SYSTEM_PROPS_KEY] = content;
409
+ function getSharedSystemProps(key) {
410
+ return findSession(key)?.sharedSystemProps ?? "";
426
411
  }
427
- function getSharedExternalDirs() {
428
- return singletonStore[SHARED_EXTERNAL_DIRS_KEY] || [];
412
+ function setSharedSystemProps(key, content) {
413
+ joined(key).sharedSystemProps = content;
429
414
  }
430
- function setSharedExternalDirs(dirs) {
431
- singletonStore[SHARED_EXTERNAL_DIRS_KEY] = dirs;
415
+ function getSharedExternalDirs(key) {
416
+ return findSession(key)?.externalDirs ?? [];
432
417
  }
433
- function getSharedExternalEntries() {
434
- return singletonStore[SHARED_EXTERNAL_ENTRIES_KEY] || /* @__PURE__ */ new Map();
418
+ function setSharedExternalDirs(key, dirs) {
419
+ joined(key).externalDirs = dirs;
435
420
  }
436
- function setSharedExternalEntries(entries) {
437
- singletonStore[SHARED_EXTERNAL_ENTRIES_KEY] = entries;
421
+ function getSharedExternalEntries(key) {
422
+ return findSession(key)?.externalEntries ?? /* @__PURE__ */ new Map();
423
+ }
424
+ function setSharedExternalEntries(key, entries) {
425
+ joined(key).externalEntries = entries;
438
426
  }
439
427
  /** Specifier → its source entry's side effects, published with the shared
440
428
  * entries; absent leaves the bundler's own classification. */
441
- function getSharedExternalSideEffects() {
442
- return singletonStore[SHARED_EXTERNAL_SIDE_EFFECTS_KEY] || /* @__PURE__ */ new Map();
429
+ function getSharedExternalSideEffects(key) {
430
+ return findSession(key)?.externalSideEffects ?? /* @__PURE__ */ new Map();
443
431
  }
444
- function setSharedExternalSideEffects(sideEffects) {
445
- singletonStore[SHARED_EXTERNAL_SIDE_EFFECTS_KEY] = sideEffects;
432
+ function setSharedExternalSideEffects(key, sideEffects) {
433
+ joined(key).externalSideEffects = sideEffects;
446
434
  }
447
- function globalSlot(key) {
448
- return {
449
- get: () => singletonStore[key] ?? null,
450
- set: (value) => {
451
- singletonStore[key] = value;
452
- }
453
- };
454
- }
455
- const analyzedHashesSlot = globalSlot(ANALYZED_HASHES_KEY);
456
- const replacementEpochSlot = globalSlot(REPLACEMENT_EPOCH_KEY);
457
- const watchTransactionSlot = globalSlot(WATCH_TRANSACTION_KEY);
458
- const sessionArtifactDirSlot = globalSlot(SESSION_ARTIFACT_DIR_KEY);
459
- const owningWatchSessionSlot = globalSlot(OWNING_WATCH_SESSION_KEY);
460
435
  /**
461
- * The only session that can run watch analysis. A non-owning session forwards
462
- * its batch here; dropping it loses files only that watcher sees.
436
+ * The only session of a build that can run watch analysis. A non-owning
437
+ * session's observations queue for it; dropping them loses files only that
438
+ * watcher sees.
463
439
  */
464
- function getOwningWatchSession() {
465
- return owningWatchSessionSlot.get();
440
+ function getOwningWatchSession(key) {
441
+ return findSession(key)?.authority ?? null;
466
442
  }
467
- function setOwningWatchSession(session) {
468
- owningWatchSessionSlot.set(session);
443
+ function setOwningWatchSession(key, session) {
444
+ joined(key).authority = session;
469
445
  }
470
446
  /**
471
447
  * Per-file content hashes of the last published analysis (relPath → hash of
472
448
  * the exact bytes analyzed), including files that produced zero entries.
473
449
  */
474
- function getAnalyzedHashes() {
475
- return analyzedHashesSlot.get();
450
+ function getAnalyzedHashes(key) {
451
+ return findSession(key)?.analyzedHashes ?? null;
476
452
  }
477
- function setAnalyzedHashes(hashes) {
478
- analyzedHashesSlot.set(hashes);
453
+ function setAnalyzedHashes(key, hashes) {
454
+ joined(key).analyzedHashes = hashes;
479
455
  }
480
- /**
481
- * Session identity is per process: the first caller claims a fresh id and
482
- * every later one adopts it, so all compilers share one artifact tree.
483
- */
484
- function claimProcessSessionId() {
485
- const existing = singletonStore[PROCESS_SESSION_ID_KEY];
486
- if (isSingletonString(existing) && existing.length > 0) return existing;
487
- const fresh = randomUUID();
488
- singletonStore[PROCESS_SESSION_ID_KEY] = fresh;
489
- return fresh;
456
+ /** What the build's last published analysis read of the discovered
457
+ * packages (spec §2.2 A4); null before one publishes. */
458
+ function getSharedPackageReadRecord(key) {
459
+ return findSession(key)?.packageReadRecord ?? null;
460
+ }
461
+ function setSharedPackageReadRecord(key, record) {
462
+ joined(key).packageReadRecord = record;
490
463
  }
491
464
  /**
492
- * Two CONCURRENT drive loops in one process would share one session tree and
493
- * clobber each other; sequential claim/release cycles are legal.
465
+ * One participant publishes a build's analysis, holding this claim until it
466
+ * closes: a second publisher would clobber its artifacts. Two builds never
467
+ * exclude each other; sequential claim and release cycles are legal.
494
468
  */
495
- function claimExclusiveSessionOwner(label) {
496
- const active = singletonStore[EXCLUSIVE_SESSION_OWNER_KEY];
497
- if (isSingletonString(active)) throw new Error(`[animus] a second Animus host ("${label}") started while "${active}" is still active in this process. The extraction session is process-global (one session tree, one manifest), so concurrent hosts would clobber each other's analysis. Run one Animus-enabled config per process, or make the builds sequential.`);
498
- singletonStore[EXCLUSIVE_SESSION_OWNER_KEY] = label;
469
+ function claimExclusiveSessionOwner(key, label) {
470
+ const state = joined(key);
471
+ const active = state.activeAttempt;
472
+ if (active !== null) throw new Error(`[animus] "${label}" started an analysis of build ${key.build} of ${key.root}, which "${active}" publishes. A build's participants share one analysis; run it once and let the others read its results.`);
473
+ state.activeAttempt = label;
499
474
  return () => {
500
- if (singletonStore[EXCLUSIVE_SESSION_OWNER_KEY] === label) delete singletonStore[EXCLUSIVE_SESSION_OWNER_KEY];
475
+ if (state.activeAttempt === label) state.activeAttempt = null;
501
476
  };
502
477
  }
503
478
  /**
504
- * Absolute artifact directory of the OWNING session's last publication. The
479
+ * Absolute artifact directory of the build's last publication. The
505
480
  * Turbopack loader runs out of process and receives it via options instead.
506
481
  */
507
- function getSessionArtifactDir() {
508
- return sessionArtifactDirSlot.get();
482
+ function getSessionArtifactDir(key) {
483
+ return findSession(key)?.artifactDir ?? null;
509
484
  }
510
- function setSessionArtifactDir(dir) {
511
- sessionArtifactDirSlot.set(dir);
485
+ function setSessionArtifactDir(key, dir) {
486
+ joined(key).artifactDir = dir;
512
487
  }
513
488
  /**
514
489
  * Replacement epoch of the last published analysis, written AFTER the
515
490
  * manifest so an epoch reader always sees at least that generation's manifest.
516
491
  */
517
- function getReplacementEpoch() {
518
- return replacementEpochSlot.get();
492
+ function getReplacementEpoch(key) {
493
+ return findSession(key)?.replacementEpoch ?? null;
519
494
  }
520
- function setReplacementEpoch(epoch) {
521
- replacementEpochSlot.set(epoch);
495
+ function setReplacementEpoch(key, epoch) {
496
+ joined(key).replacementEpoch = epoch;
522
497
  }
523
498
  /**
524
- * The one in-flight watch-analysis transaction: a compiler that did not start
525
- * it joins this promise rather than read a pre-transaction generation.
499
+ * The build's one in-flight watch-analysis transaction: a participant that
500
+ * did not start it joins this promise rather than read a pre-transaction
501
+ * generation.
526
502
  */
527
- function getWatchTransaction() {
528
- return watchTransactionSlot.get();
503
+ function getWatchTransaction(key) {
504
+ return findSession(key)?.watchTransaction ?? null;
529
505
  }
530
- function setWatchTransaction(transaction) {
531
- watchTransactionSlot.set(transaction);
506
+ function observationQueue(key) {
507
+ return joined(key).observations;
532
508
  }
533
- /** Engine selection travels through the singleton so non-owning compilers
534
- * and the webpack loader honor the owner's choice. */
535
- function setSharedEngine(engine) {
536
- singletonStore[ENGINE_KEY] = engine;
509
+ function getSystemAuthority(key) {
510
+ return findSession(key)?.systemAuthority ?? null;
537
511
  }
538
- function getSharedEngine() {
539
- return singletonStore[ENGINE_KEY] || "v2";
512
+ function setSystemAuthority(key, authority) {
513
+ joined(key).systemAuthority = authority;
514
+ }
515
+ function setWatchTransaction(key, transaction) {
516
+ joined(key).watchTransaction = transaction;
540
517
  }
541
518
  /** Single choke-point for every native extraction call; the NAPI module's
542
519
  * own `.d.ts` is the authoritative surface for what it returns. */
543
520
  function requireEngine() {
544
521
  return __require("@animus-ui/extract");
545
522
  }
523
+ const ENGINE_API_OVERRIDE_KEY = "__animus_engine_api_override__";
524
+ const overrideGlobal = globalThis;
525
+ /** The native engine module could not be loaded: an environment failure,
526
+ * never answered by another engine. */
527
+ var EnvironmentFailure = class extends Error {
528
+ code = "EnvironmentFailure";
529
+ constructor(cause) {
530
+ super(`[animus] EnvironmentFailure: the native extraction engine could not be loaded: ${String(cause)}`, { cause });
531
+ this.name = "EnvironmentFailure";
532
+ }
533
+ };
534
+ let loadEngineFactory = requireEngine;
535
+ /** The native factory, loaded once for every build in the process. A load
536
+ * failure is an EnvironmentFailure, and is not cached: the next use tries
537
+ * the load again. */
538
+ function engineFactory() {
539
+ const registry = sessionRegistry();
540
+ if (registry.factory === null) try {
541
+ registry.factory = loadEngineFactory();
542
+ } catch (error) {
543
+ throw new EnvironmentFailure(error instanceof Error ? error : String(error));
544
+ }
545
+ return registry.factory;
546
+ }
546
547
  /**
547
- * The one engine API for this process: the instance, its analyze-time
548
- * sources, and the drift flag live on globalThis so both copies share them.
548
+ * One engine-handle store (SA18): the engine API over `key`'s own engine
549
+ * state, made once per key in the registry every module copy joins. The
550
+ * native factory is shared; each key's engine instance, analyze-time
551
+ * sources and drift flag are its own, and its build's retirement releases
552
+ * them. A key need not be a joined build's: a Vite context or a Turbopack
553
+ * worker keys its own.
549
554
  */
550
- const v2EngineApi = createV2EngineApi({
551
- label: "animus",
552
- isV2: () => getSharedEngine() === "v2",
553
- loadNativeEngine: requireEngine,
554
- passThroughUnknownPaths: true,
555
- store: {
556
- getEngine: () => singletonStore[V2_ENGINE_KEY],
557
- setEngine: (engine) => {
558
- singletonStore[V2_ENGINE_KEY] = engine;
559
- },
560
- getSentSources: () => singletonStore[V2_SENT_SOURCES_KEY] ?? null,
561
- setSentSources: (sources) => {
562
- singletonStore[V2_SENT_SOURCES_KEY] = sources;
563
- },
564
- getDriftWarned: () => Boolean(singletonStore[V2_DRIFT_WARNED_KEY]),
565
- setDriftWarned: (value) => {
566
- singletonStore[V2_DRIFT_WARNED_KEY] = value;
555
+ function engineHandle(key, options) {
556
+ const engines = sessionRegistry().engines;
557
+ const id = sessionKeyId(key);
558
+ let api = engines.get(id);
559
+ if (api === void 0) {
560
+ let engine = null;
561
+ let sentSources = null;
562
+ let driftWarned = false;
563
+ api = createV2EngineApi({
564
+ ...options,
565
+ isV2: () => true,
566
+ loadNativeEngine: engineFactory,
567
+ store: {
568
+ getEngine: () => engine,
569
+ setEngine: (next) => {
570
+ engine = next;
571
+ },
572
+ getSentSources: () => sentSources,
573
+ setSentSources: (sources) => {
574
+ sentSources = sources;
575
+ },
576
+ getDriftWarned: () => driftWarned,
577
+ setDriftWarned: (value) => {
578
+ driftWarned = value;
579
+ }
580
+ }
581
+ });
582
+ engines.set(id, api);
583
+ }
584
+ return api;
585
+ }
586
+ /** Releases a standalone handle, one no joined build retires: its host
587
+ * calls this when it closes. */
588
+ function releaseEngineHandle(key) {
589
+ sessionRegistry().engines.delete(sessionKeyId(key));
590
+ }
591
+ /**
592
+ * The build's engine API, from the one engine-handle store.
593
+ */
594
+ function engineApi(key) {
595
+ const override = overrideGlobal[ENGINE_API_OVERRIDE_KEY];
596
+ if (override) return override();
597
+ joined(key);
598
+ return engineHandle(key, {
599
+ label: "animus",
600
+ passThroughUnknownPaths: true
601
+ })();
602
+ }
603
+ /** A fresh build id, created once by the owning host wrapper. */
604
+ function createBuildId() {
605
+ return randomUUID();
606
+ }
607
+ /** The key of build `build` of `root`, by the root's canonical path. */
608
+ function sessionKey(build, root) {
609
+ const absolute = resolve(root);
610
+ let canonical = absolute;
611
+ try {
612
+ canonical = realpathSync(absolute);
613
+ } catch {}
614
+ return {
615
+ build,
616
+ root: canonical,
617
+ protocol: 1
618
+ };
619
+ }
620
+ function sessionKeyId(key) {
621
+ return JSON.stringify([
622
+ key.protocol,
623
+ key.build,
624
+ key.root
625
+ ]);
626
+ }
627
+ /** A participant joined a session with semantic options that differ from
628
+ * those the session was created with. */
629
+ var SessionConfigurationConflict = class extends Error {
630
+ key;
631
+ established;
632
+ joining;
633
+ differing;
634
+ code = "SessionConfigurationConflict";
635
+ constructor(key, established, joining, differing, requirement = "Every participant of one build must use the same Animus options.") {
636
+ super(`[animus] SessionConfigurationConflict: "${joining}" joined build ${key.build} of ${key.root} with ${differing.join(", ")} differing from "${established}". ${requirement}`);
637
+ this.key = key;
638
+ this.established = established;
639
+ this.joining = joining;
640
+ this.differing = differing;
641
+ this.name = "SessionConfigurationConflict";
642
+ }
643
+ };
644
+ function digestOf(field) {
645
+ return {
646
+ field,
647
+ text: (options) => {
648
+ const value = options[field];
649
+ return value === void 0 ? void 0 : stableStringify(value);
567
650
  }
651
+ };
652
+ }
653
+ /**
654
+ * Every option, classified: one a build's analysis depends on, which every
655
+ * participant must agree on, or one deliberately excluded. `verbose` changes
656
+ * no output, and the root is the key's. The type checker rejects an option
657
+ * added to `AnimusCoreOptions` until it is classified here.
658
+ */
659
+ const OPTION_CLASSES = {
660
+ system: digestOf("system"),
661
+ exclude: digestOf("exclude"),
662
+ extensions: digestOf("extensions"),
663
+ strict: digestOf("strict"),
664
+ diagnostics: digestOf("diagnostics"),
665
+ verbose: "excluded",
666
+ prefix: digestOf("prefix"),
667
+ prefixContextualVars: digestOf("prefixContextualVars"),
668
+ targets: digestOf("targets"),
669
+ minify: digestOf("minify"),
670
+ staticCss: digestOf("staticCss"),
671
+ layers: digestOf("layers"),
672
+ engine: digestOf("engine"),
673
+ mode: digestOf("mode"),
674
+ root: "excluded"
675
+ };
676
+ /** The options a build's analysis depends on, each as stable text; each is
677
+ * data by its type, so equal text is an equal value. A host's own options
678
+ * are not the build's. */
679
+ function semanticOptions(options) {
680
+ const present = [];
681
+ for (const [name, digest] of Object.entries(OPTION_CLASSES)) {
682
+ if (digest === "excluded") continue;
683
+ const text = digest.text(options);
684
+ if (text !== void 0) present.push([name, text]);
685
+ }
686
+ return new Map(present);
687
+ }
688
+ const SESSION_REGISTRY = Symbol.for("animus.session-registry.v1");
689
+ const registryGlobal = globalThis;
690
+ function sessionRegistry() {
691
+ return registryGlobal[SESSION_REGISTRY] ??= {
692
+ sessions: /* @__PURE__ */ new Map(),
693
+ engines: /* @__PURE__ */ new Map(),
694
+ factory: null
695
+ };
696
+ }
697
+ /**
698
+ * Joins `participant` to the session of `key`, creating it with `options` on
699
+ * the first join. A later participant must bring the same semantic options.
700
+ */
701
+ function joinSession(key, participant, options) {
702
+ const sessions = sessionRegistry().sessions;
703
+ const id = sessionKeyId(key);
704
+ const joining = semanticOptions(options);
705
+ const existing = sessions.get(id);
706
+ if (existing === void 0) {
707
+ const state = {
708
+ key,
709
+ established: participant,
710
+ options: joining,
711
+ participants: /* @__PURE__ */ new Set([participant]),
712
+ activeAttempt: null,
713
+ authority: null,
714
+ analysisStarted: null,
715
+ watchTransaction: null,
716
+ observations: {
717
+ revision: 0,
718
+ rescan: false,
719
+ untaken: /* @__PURE__ */ new Map(),
720
+ suppliedSources: /* @__PURE__ */ new Map(),
721
+ revisions: /* @__PURE__ */ new Map(),
722
+ active: null
723
+ },
724
+ systemAuthority: null,
725
+ claimRecord: null,
726
+ manifestJson: null,
727
+ sharedCss: "",
728
+ sharedSystemProps: "",
729
+ externalDirs: [],
730
+ externalEntries: /* @__PURE__ */ new Map(),
731
+ externalSideEffects: /* @__PURE__ */ new Map(),
732
+ analyzedHashes: null,
733
+ packageReadRecord: null,
734
+ replacementEpoch: null,
735
+ artifactDir: null,
736
+ unmatchedExcludesReported: false
737
+ };
738
+ sessions.set(id, state);
739
+ return state;
740
+ }
741
+ const differing = [.../* @__PURE__ */ new Set([...existing.options.keys(), ...joining.keys()])].filter((name) => existing.options.get(name) !== joining.get(name));
742
+ if (differing.length > 0) throw new SessionConfigurationConflict(key, existing.established, participant, differing.sort());
743
+ existing.participants.add(participant);
744
+ return existing;
745
+ }
746
+ /** The session of `key`, if a participant has joined it. */
747
+ function findSession(key) {
748
+ return sessionRegistry().sessions.get(sessionKeyId(key));
749
+ }
750
+ /** The build's on-disk claim record, which stops another process pruning
751
+ * its tree: held once by the build, whichever participant publishes, and
752
+ * released only when the build retires. */
753
+ function holdBuildClaimRecord(key, hold) {
754
+ const state = joined(key);
755
+ state.claimRecord ??= hold();
756
+ }
757
+ /** `participant` leaves the build of `key`. The build retires with its
758
+ * last participant, once no attempt is in flight: it leaves the registry
759
+ * and releases its claim record, so its tree is pruned once stale. */
760
+ function leaveSession(key, participant) {
761
+ const state = findSession(key);
762
+ if (state === void 0) return;
763
+ state.participants.delete(participant);
764
+ retireIfUnused(sessionKeyId(key), state);
765
+ }
766
+ function retireIfUnused(id, state) {
767
+ if (state.participants.size > 0) return;
768
+ const inflight = state.watchTransaction;
769
+ if (inflight !== null) {
770
+ const retry = () => retireIfUnused(id, state);
771
+ inflight.then(retry, retry);
772
+ return;
568
773
  }
569
- });
570
- function engineApi() {
571
- const override = singletonStore[ENGINE_API_OVERRIDE_KEY];
572
- return override ? override() : v2EngineApi();
774
+ const sessions = sessionRegistry().sessions;
775
+ if (sessions.get(id) !== state) return;
776
+ sessions.delete(id);
777
+ sessionRegistry().engines.delete(id);
778
+ state.claimRecord?.();
779
+ state.claimRecord = null;
573
780
  }
574
781
  //#endregion
575
782
  //#region session/timing.ts
@@ -605,6 +812,23 @@ function logBuildTimings(bt, rustTiming, log, verbose) {
605
812
  }
606
813
  //#endregion
607
814
  //#region session/extraction-session.ts
815
+ /** An observation's state when its file is gone, or cannot be read; any
816
+ * other state is the content hash it was observed with. */
817
+ const REMOVED_OBSERVATION = "removed";
818
+ const UNREADABLE_OBSERVATION = "unreadable";
819
+ /** A loaded system's identity: its evaluated modules and a fingerprint of
820
+ * the whole load. */
821
+ function systemLoad(system) {
822
+ return {
823
+ identity: contentHash(JSON.stringify(system)),
824
+ dependencies: system.dependencies ?? []
825
+ };
826
+ }
827
+ const CONSOLE_LOGGER = {
828
+ info: (line) => console.error(line),
829
+ warn: (line) => console.warn(line),
830
+ error: (line) => console.error(line)
831
+ };
608
832
  const SESSION_DIR_MAX_AGE_MS = 864e5;
609
833
  const STATUS_WATCHDOG_MS = 2e3;
610
834
  const LEGACY_FLAT_ARTIFACTS = [
@@ -623,19 +847,96 @@ function isDiskString(value) {
623
847
  }
624
848
  /** Rejection of an attempt whose corpus holds an aborted parse. */
625
849
  var AbortedParseRejection = class extends Error {};
850
+ /** A reload refused because its full read would replace host-supplied
851
+ * bytes with the disk's: a system reload a parse hold deferred, or a full
852
+ * read that reaches a host-supplied file only under another spelling (a
853
+ * case-insensitive volume). Like a hold, it keeps the prior publication
854
+ * and the observed sources. */
855
+ var DeferredReloadRefusal = class extends Error {};
856
+ /** The file's canonical path, or null when it cannot be resolved. */
857
+ function canonicalPath(path) {
858
+ try {
859
+ return realpathSync.native(path);
860
+ } catch {
861
+ return null;
862
+ }
863
+ }
864
+ /** The host sources a full read reaches only under another spelling: a key
865
+ * the read did not reach exactly, whose file is one the read reached by
866
+ * another path (a case-insensitive volume, a symlinked alias). A key with
867
+ * no such file is not read either way, and is left alone. */
868
+ function misspelledHostSources(hostSources, readPaths) {
869
+ const misses = [...hostSources.keys()].filter((path) => !readPaths.has(path));
870
+ if (misses.length === 0) return [];
871
+ const readByCanonical = /* @__PURE__ */ new Map();
872
+ for (const readPath of readPaths) {
873
+ const canonical = canonicalPath(readPath);
874
+ if (canonical === null) continue;
875
+ readByCanonical.set(canonical, [...readByCanonical.get(canonical) ?? [], readPath]);
876
+ }
877
+ return misses.flatMap((path) => {
878
+ const canonical = canonicalPath(path);
879
+ const read = canonical === null ? void 0 : readByCanonical.get(canonical);
880
+ return read === void 0 ? [] : [{
881
+ path,
882
+ read
883
+ }];
884
+ });
885
+ }
626
886
  function isDiskNumber(value) {
627
887
  return Object(value) !== value && Object.prototype.toString.call(value) === "[object Number]";
628
888
  }
889
+ /** A file's content hash, or '' when it cannot be read. */
890
+ function fileHashOrEmpty(path) {
891
+ try {
892
+ return contentHash(readFileSync(path, "utf-8"));
893
+ } catch {
894
+ return "";
895
+ }
896
+ }
897
+ /** The first file whose content no longer has its recorded hash: an
898
+ * unreadable or deleted file reads as '', which mismatches a real one. */
899
+ function firstChangedFile(hashes) {
900
+ for (const [path, hash] of hashes) if (fileHashOrEmpty(path) !== hash) return path;
901
+ }
629
902
  /** Bundler-agnostic extraction pipeline: system load, source ingestion,
630
903
  * analysis, emission, and watch-cycle diffing. Carries no bundler types. */
631
904
  var ExtractionSession = class {
632
905
  host;
633
906
  /** Set by the adapter before any pipeline run. */
634
907
  rootDir = null;
908
+ /** The module extracted terminals import their factories from; an
909
+ * override must supply every terminal the analysis extracts. */
910
+ runtimeImport = DEFAULT_RUNTIME_IMPORT;
911
+ /** A host that resolves imports itself (Vite's `this.resolve`): package
912
+ * discovery asks it first, the loader only for a declared workspace it
913
+ * cannot resolve. Null resolves through the loader alone. */
914
+ hostResolve = null;
915
+ /** Each file's alias comparison, kept by content hash across analyses. */
916
+ aliasCache = new AliasAgreementCache();
635
917
  /** Serialized path aliases, harvested by the adapter from bundler config. */
636
918
  pathAliasesJson = null;
919
+ /** The host's aliases are the project's tsconfig `paths`: each full pass
920
+ * re-reads them, so a tsconfig edit takes effect. */
921
+ aliasesFromTsconfig = false;
922
+ /** The root package.json and the tsconfig chain, absent files included:
923
+ * a change to any is a configuration change, which runs the full pass. */
924
+ configInputPaths = [];
925
+ configInputKeys = /* @__PURE__ */ new Set();
926
+ /** Each configuration input's content hash at the last full pass, ''
927
+ * for an absent one: the probe's basis when the host reports nothing. */
928
+ configInputHashes = /* @__PURE__ */ new Map();
637
929
  /** The host's export conditions, in its order, for the system loader. */
638
930
  conditions = [];
931
+ /** The host's extension order, as its adapter captured it; the built-in
932
+ * policy where no host resolves. */
933
+ extensionPolicy = BUILT_IN_EXTENSION_POLICY;
934
+ /** Which files the host serves as assets (spec §2.2 A8); the built-in
935
+ * classification where no host's can be read. */
936
+ assetClassification = builtInAssetClassification("noHost");
937
+ /** How the host's bundler expands computed loads (spec §6.3); without
938
+ * one, every computed load keeps its widest reading. */
939
+ loadExpander;
639
940
  assetCopyCache = /* @__PURE__ */ new Map();
640
941
  /** Physical asset files registered with the host watcher. */
641
942
  assetDependencyPaths = /* @__PURE__ */ new Set();
@@ -643,9 +944,23 @@ var ExtractionSession = class {
643
944
  /** System-props module id override. Turbopack rejects absolute-path
644
945
  * imports, so its driver sets a virtual id; null injects the abs path. */
645
946
  systemPropsModuleId = null;
947
+ /** The stylesheet module generated code imports; a host that serves the
948
+ * stylesheet itself names its own. */
949
+ cssModuleId = ANIMUS_CSS_MODULE_ID;
646
950
  /** Whether the analysis-inputs hydration corpus is written to disk. Only
647
951
  * isolated loader workers replay it; an in-process loader reads memory. */
648
952
  persistAnalysisInputs = false;
953
+ /** The host substitutes asset() URLs itself (Vite: dev copies, build
954
+ * emitFile markers): the published sheets keep their placeholders, and
955
+ * the session makes no copy and tracks no asset file. */
956
+ hostSubstitutesAssets = false;
957
+ /** Whether the session keeps its artifacts on disk: status, commit,
958
+ * manifest, styles, the epoch witness, asset copies and its directory
959
+ * claim, with the sibling reconciliation they need. A host that serves
960
+ * every result from memory (Vite) turns it off; the shared state still
961
+ * publishes. Such a host also substitutes assets (above): asset copies
962
+ * are artifacts. */
963
+ persistArtifacts = true;
649
964
  /** Whether this session serves development (a watch), where an error-level
650
965
  * diagnostic is reported and the session keeps running. Only a build
651
966
  * fails on one. */
@@ -693,29 +1008,69 @@ var ExtractionSession = class {
693
1008
  /** Fires exactly once per settled `handleWatchUpdate` — the failure cause,
694
1009
  * or null when clean. Observation never alters delivery. */
695
1010
  onCycleSettled = null;
1011
+ /** Fires once per published generation, and per held attempt. */
1012
+ onOutcome = null;
1013
+ /** The last published generation; null before the first. */
1014
+ publication = null;
1015
+ /** The original sources (raw, before adaptation) the session holds, by
1016
+ * root-relative path: the published generation's, plus any that an
1017
+ * attempt in flight or held by an aborted parse has read. */
1018
+ get heldSources() {
1019
+ return this.fileCache;
1020
+ }
1021
+ /** The generation an emit produced, current once its ingestion
1022
+ * publishes. */
1023
+ stagedPublication = null;
696
1024
  options;
697
1025
  staticCssJson;
698
1026
  system = null;
699
- /** Configured external files that could not be read. Incremental passes
700
- * re-collect nothing, so they replay these instead of passing strict. */
1027
+ /** Configured files that could not be read. Incremental passes replay
1028
+ * these instead of passing strict, and add a watched change they cannot
1029
+ * read. */
701
1030
  ingestionFailureDiagnostics = [];
702
1031
  /** Whether the corpus being analyzed left a file out, whose renders the
703
1032
  * analysis cannot see. */
704
1033
  skippedSources = [];
1034
+ /** Resolver calls ingestion ended under another spelling. */
1035
+ ingestionOpenings = [];
1036
+ /** The adapted originals of the ingestion being analysed: it publishes
1037
+ * only after its analysis does. */
1038
+ ingestionAdapted = /* @__PURE__ */ new Map();
705
1039
  /** Full package-resolution map from the last full pipeline — replayed by
706
1040
  * incremental passes (sourceEntries alone omits dist-resolved packages). */
707
1041
  lastPackageMap = {};
708
1042
  fileCache = /* @__PURE__ */ new Map();
709
- corpus = createSourceCorpus({
710
- engineApi: () => engineApi(),
711
- prefix: "[animus-next]",
712
- strict: () => !!this.options.strict,
713
- warn: (message) => this.warn(message),
714
- reportErrors: () => this.reportErrors()
715
- });
1043
+ /** Held sources whose bytes the host supplied (WatchChanges.sources),
1044
+ * by root-relative path; a read from disk drops the path. */
1045
+ hostSuppliedKeys = /* @__PURE__ */ new Set();
1046
+ corpus = createSourceCorpus(this.corpusHost());
1047
+ /** The corpus reads the driver label when it reports, so a host that
1048
+ * sets `driverLabel` after construction is named in its messages. */
1049
+ corpusHost() {
1050
+ const label = () => this.driverLabel;
1051
+ return {
1052
+ engineApi: () => this.engine(),
1053
+ get prefix() {
1054
+ return `[${label()}]`;
1055
+ },
1056
+ strict: () => !!this.options.strict,
1057
+ warn: (message) => this.warn(message),
1058
+ reportErrors: () => this.reportErrors(),
1059
+ extensions: () => this.extensionPolicy,
1060
+ rootDir: () => this.rootDir
1061
+ };
1062
+ }
716
1063
  systemDependencyKeys = /* @__PURE__ */ new Set();
1064
+ /** Whether `path` is the system module, one of its dependencies, or a
1065
+ * path its load found absent: a change to it reloads the system. */
1066
+ isSystemDependency(path) {
1067
+ return toWatchKeys(path).some((key) => this.systemDependencyKeys.has(key));
1068
+ }
717
1069
  /** Loader-reported dependency paths for bundler watch registration. */
718
1070
  systemDependencyPaths = [];
1071
+ /** Paths the system load looked for and found absent: a host registers
1072
+ * them for creation, which reloads the system. */
1073
+ systemMissingPaths = [];
719
1074
  systemDependencyHashes = /* @__PURE__ */ new Map();
720
1075
  lastSystemPropsHash = null;
721
1076
  artifactRecords = {
@@ -729,9 +1084,20 @@ var ExtractionSession = class {
729
1084
  /** Release handle for the in-process owner slot AND the directory's
730
1085
  * on-disk owner record, taken and released as one; null when unheld. */
731
1086
  releasePublicationClaim = null;
732
- /** Session identity, claimed once per PROCESS and adopted by every later
733
- * session instance so all compilers share one artifact tree. */
734
- sessionId = claimProcessSessionId();
1087
+ /** The build this session takes part in: every participant of one build
1088
+ * shares its artifact tree and state; another build of the root never
1089
+ * does. */
1090
+ sessionId;
1091
+ /** The log tier, from the option and `ANIMUS_DEBUG` at construction. */
1092
+ verbosity;
1093
+ /** The emission mode at construction: explicit, else `NODE_ENV`'s. It
1094
+ * defaults minify. */
1095
+ startupMode;
1096
+ joinedKey = null;
1097
+ /** This instance as a participant of its build, once joined. */
1098
+ participant = null;
1099
+ /** Closed: it left its build and starts no attempt until it reattaches. */
1100
+ detached = false;
735
1101
  /** Epoch of the last published analysis; null seeds from disk so a
736
1102
  * same-session restart with unchanged plans rewrites no bytes. */
737
1103
  lastEpochValue = null;
@@ -746,10 +1112,23 @@ var ExtractionSession = class {
746
1112
  * the cache keeps every observed source so the repair publishes them in
747
1113
  * one generation; a successful publication clears it. */
748
1114
  heldRejection = null;
749
- /** A system reload an aborted parse rejected. Its dependency hashes already
750
- * advanced, so the first batch that passes the parse check publishes
751
- * through the full pipeline, which loads the latest system. */
1115
+ /** Reload debt: a full pass that failed with a committed system to
1116
+ * hold, in development or under an aborted parse, owed until one
1117
+ * succeeds. runFullPipeline alone sets and clears it. While it stands, every batch retries the full reload and
1118
+ * nothing publishes against the stale system. Under an aborted parse's
1119
+ * hold, the first batch that passes the parse check pays it instead. */
752
1120
  systemReloadOwed = false;
1121
+ /** The system dependencies the last committed full pass recorded: a
1122
+ * failed reload watches these and its own, never an earlier failure's. */
1123
+ lastGoodSystemDependencies = {
1124
+ keys: /* @__PURE__ */ new Set(),
1125
+ paths: [],
1126
+ missing: [],
1127
+ hashes: /* @__PURE__ */ new Map()
1128
+ };
1129
+ /** Whether the current full pass's system load recorded its
1130
+ * dependencies. */
1131
+ attemptRecordedSystemDependencies = false;
753
1132
  /** Monotonic readiness witness: flips true on the first complete
754
1133
  * publication and never regresses; every later status write carries it. */
755
1134
  firstEmissionComplete = false;
@@ -760,27 +1139,53 @@ var ExtractionSession = class {
760
1139
  constructor(options, host = {}) {
761
1140
  this.host = host;
762
1141
  this.options = options;
1142
+ this.sessionId = host.build ?? createBuildId();
763
1143
  this.staticCssJson = serializeStaticCss(options.staticCss);
1144
+ this.verbosity = resolveVerbosity(options.verbose, process.env.ANIMUS_DEBUG);
1145
+ this.startupMode = resolveMode(options.mode, () => process.env.NODE_ENV === "production" ? "production" : "development").mode;
764
1146
  }
765
1147
  /** Forgets that `relPath` could not be read once an incremental pass reads
766
- * or deletes it, so its cost stops holding pruning off. */
1148
+ * or deletes it, so its cost stops holding pruning off. Whether one was
1149
+ * recorded. */
767
1150
  clearIngestionFailure(relPath) {
1151
+ const before = this.ingestionFailureDiagnostics.length;
768
1152
  this.ingestionFailureDiagnostics = this.ingestionFailureDiagnostics.filter((diagnostic) => diagnostic.file !== relPath);
1153
+ return this.ingestionFailureDiagnostics.length !== before;
769
1154
  }
770
1155
  get sessionDir() {
771
1156
  return sessionArtifactDir(this.rootDir, this.sessionId);
772
1157
  }
1158
+ /** This session's build of its root, joined on first use: a participant
1159
+ * whose options differ from the build's fails with
1160
+ * `SessionConfigurationConflict`. */
1161
+ get key() {
1162
+ if (this.joinedKey === null) {
1163
+ const key = sessionKey(this.sessionId, this.rootDir);
1164
+ const participant = `${this.driverLabel}#${randomUUID()}`;
1165
+ joinSession(key, participant, this.options);
1166
+ this.participant = participant;
1167
+ this.joinedKey = key;
1168
+ }
1169
+ return this.joinedKey;
1170
+ }
773
1171
  get verbose() {
774
- return resolveVerbosity(this.options.verbose, process.env.ANIMUS_DEBUG) !== "quiet";
1172
+ return this.verbosity !== "quiet";
1173
+ }
1174
+ /** The engine of this session's build. */
1175
+ engine() {
1176
+ return this.host.engine?.() ?? engineApi(this.key);
1177
+ }
1178
+ get logger() {
1179
+ return this.host.logger ?? CONSOLE_LOGGER;
775
1180
  }
776
1181
  log(msg) {
777
- if (this.verbose) console.error(`[animus] ${msg}`);
1182
+ if (this.verbose) this.logger.info(`[animus] ${msg}`);
778
1183
  }
779
1184
  warn(msg) {
780
- console.warn(`[animus] ${msg}`);
1185
+ this.logger.warn(`[animus] ${msg}`);
781
1186
  }
782
1187
  reportErrors() {
783
- return this.development ? (message) => console.error(message) : void 0;
1188
+ return this.development ? (message) => this.logger.error(message) : void 0;
784
1189
  }
785
1190
  /** A build-strictness check that failed: a build throws `message`, and
786
1191
  * development reports it at error level and takes the non-strict path. */
@@ -789,6 +1194,11 @@ var ExtractionSession = class {
789
1194
  if (!report) throw new Error(message);
790
1195
  report(message);
791
1196
  }
1197
+ /** A finding `strict` turns into a strict failure; otherwise a warning. */
1198
+ strictOrWarn(message, strictMessage = message) {
1199
+ if (this.options.strict) this.strictFailure(strictMessage);
1200
+ else this.warn(message);
1201
+ }
792
1202
  now() {
793
1203
  return this.verbose ? performance.now() : 0;
794
1204
  }
@@ -803,6 +1213,11 @@ var ExtractionSession = class {
803
1213
  lastExternalOutcomes = [];
804
1214
  /** The analysed kits' descriptors, from the last discovery. */
805
1215
  kitDescriptors = [];
1216
+ /** Package name → the real root of the instance discovery read. */
1217
+ packageInstances = {};
1218
+ /** What this attempt's analysis read of the discovered packages; the
1219
+ * build's shared record once the analysis publishes. */
1220
+ packageReadRecord = null;
806
1221
  /** Component count of the last published analysis, null before the first —
807
1222
  * read by drivers instead of re-parsing the manifest JSON every cycle. */
808
1223
  lastComponentCount = null;
@@ -813,18 +1228,15 @@ var ExtractionSession = class {
813
1228
  /** When superseded asset copies leave `assets/`: `'full-pipeline'` suits a
814
1229
  * driver serving the dir in place, `'every-cycle'` one republishing it. */
815
1230
  staleAssetPruning = "full-pipeline";
816
- /** Driver-owned STRUCTURAL exclusions, joined to the never-replaceable set
817
- * — never the user list, whose presence flips the replace semantics. */
1231
+ /** The host's output directories (`outputExclusion`): root-relative,
1232
+ * excluded as literal path prefixes from discovery and from package
1233
+ * collection, whatever the user's list. */
818
1234
  structuralExclude = [];
819
1235
  scanConfigMemo = null;
820
- /** Per-pattern exclusion hit counts from this session's matcher —
821
- * driver-consumed reporting surface (dead-pattern visibility). */
822
- getExcludeStats() {
823
- return this.resolveScanConfig().excludeMatcher.stats();
824
- }
825
1236
  /** Resolve the scan configuration from options — the single source of the
826
- * exclude/extension policy shared by the full and incremental pipelines. */
827
- resolveScanConfig() {
1237
+ * exclude/extension policy shared by the full and incremental pipelines,
1238
+ * and by a host classifying its own file events. */
1239
+ get scanConfig() {
828
1240
  if (this.scanConfigMemo === null) {
829
1241
  const extensionsSet = new Set(this.options.extensions ?? DEFAULT_EXTENSIONS);
830
1242
  this.scanConfigMemo = {
@@ -834,8 +1246,8 @@ var ExtractionSession = class {
834
1246
  }
835
1247
  return this.scanConfigMemo;
836
1248
  }
837
- /** Concurrent entries wait for publication, then ingest their own batch;
838
- * content hashes coalesce observations the prior analysis covered. */
1249
+ /** Records the batch before any await, so an attempt that rejects never
1250
+ * erases it, and settles with the attempt that takes it (SA20). */
839
1251
  async handleWatchUpdate(changes) {
840
1252
  try {
841
1253
  await this.routeWatchUpdate(changes);
@@ -846,36 +1258,131 @@ var ExtractionSession = class {
846
1258
  this.onCycleSettled?.(null);
847
1259
  }
848
1260
  async routeWatchUpdate(changes) {
849
- const hasBatch = (changes.modifiedFiles?.size ?? 0) > 0 || (changes.removedFiles?.size ?? 0) > 0;
850
- const inflight = getWatchTransaction();
1261
+ if ((changes.modifiedFiles?.size ?? 0) > 0 || (changes.removedFiles?.size ?? 0) > 0) {
1262
+ await this.awaitObservation(this.recordObservation(changes));
1263
+ return;
1264
+ }
1265
+ const inflight = getWatchTransaction(this.key);
851
1266
  if (inflight) {
852
1267
  await inflight;
853
- if (!hasBatch) return;
854
- }
855
- if (!this.system) {
856
- const owner = getOwningWatchSession();
857
- if (owner && owner !== this && hasBatch) await owner.ingestForwardedBatch(changes);
858
1268
  return;
859
1269
  }
860
- await this.ingestForwardedBatch(changes);
1270
+ if (this.system && !this.detached) await this.runWatchAttempt(changes);
1271
+ }
1272
+ /** Queues the batch's observations under a new revision, synchronously.
1273
+ * A batch equivalent to what the active attempt took joins that attempt:
1274
+ * a second compiler's report of the same edit never analyzes it twice. */
1275
+ recordObservation(changes) {
1276
+ const queue = observationQueue(this.key);
1277
+ const observed = /* @__PURE__ */ new Map();
1278
+ for (const path of changes.modifiedFiles ?? []) {
1279
+ let state;
1280
+ try {
1281
+ state = contentHash(changes.sources?.get(path) ?? readFileSync(path, "utf-8"));
1282
+ } catch {
1283
+ state = UNREADABLE_OBSERVATION;
1284
+ }
1285
+ observed.set(path, state);
1286
+ }
1287
+ for (const path of changes.removedFiles ?? []) observed.set(path, REMOVED_OBSERVATION);
1288
+ const revision = ++queue.revision;
1289
+ const active = queue.active;
1290
+ if (active !== null && [...observed].every(([path, state]) => !queue.untaken.has(path) && active.taken.get(path) === state)) {
1291
+ queue.revisions.set(revision, active.transaction);
1292
+ return revision;
1293
+ }
1294
+ for (const [path, state] of observed) {
1295
+ queue.untaken.set(path, state);
1296
+ const supplied = changes.sources?.get(path);
1297
+ if (supplied === void 0) queue.suppliedSources.delete(path);
1298
+ else queue.suppliedSources.set(path, supplied);
1299
+ }
1300
+ queue.revisions.set(revision, null);
1301
+ return revision;
861
1302
  }
862
- /** The shared owner gate for local and forwarded batches. Every waiter
863
- * rechecks the slot so distinct batches publish in arrival order. */
864
- async ingestForwardedBatch(changes) {
865
- if (!this.system) return;
1303
+ /** Settles with the attempt that takes `revision`. Another attempt's
1304
+ * rejection is its own callers': this observation runs after it. */
1305
+ async awaitObservation(revision) {
1306
+ const queue = observationQueue(this.key);
866
1307
  for (;;) {
867
- const inflight = getWatchTransaction();
868
- if (!inflight) break;
869
- await inflight;
870
- }
871
- const transaction = Promise.resolve().then(() => this.processWatchUpdate(changes));
872
- setWatchTransaction(transaction);
873
- try {
874
- await transaction;
875
- } finally {
876
- setWatchTransaction(null);
1308
+ const taken = queue.revisions.get(revision);
1309
+ if (taken !== null && taken !== void 0) {
1310
+ queue.revisions.delete(revision);
1311
+ await taken;
1312
+ return;
1313
+ }
1314
+ const inflight = getWatchTransaction(this.key);
1315
+ if (inflight) {
1316
+ await inflight.catch(() => void 0);
1317
+ continue;
1318
+ }
1319
+ const owner = this.system && !this.detached ? this : getOwningWatchSession(this.key);
1320
+ if (owner === null && !this.detached && getManifestJson(this.key) !== null) {
1321
+ await this.runFullPipeline();
1322
+ continue;
1323
+ }
1324
+ if (owner === null || !owner.startObservedAttempt()) {
1325
+ queue.revisions.delete(revision);
1326
+ return;
1327
+ }
877
1328
  }
878
1329
  }
1330
+ /** Starts one attempt over every untaken observation of the build. */
1331
+ startObservedAttempt() {
1332
+ if (!this.system || this.detached) return false;
1333
+ const { taken, rescan, sources } = this.takeObservations();
1334
+ const modifiedFiles = /* @__PURE__ */ new Set();
1335
+ const removedFiles = /* @__PURE__ */ new Set();
1336
+ for (const [path, state] of taken) (state === REMOVED_OBSERVATION ? removedFiles : modifiedFiles).add(path);
1337
+ this.settleTaken(taken, this.runWatchAttempt({
1338
+ modifiedFiles,
1339
+ removedFiles,
1340
+ sources
1341
+ }, rescan));
1342
+ return true;
1343
+ }
1344
+ /** Every untaken observation of the build, and whether a full rescan is
1345
+ * requested, which the caller's attempt now covers. */
1346
+ takeObservations() {
1347
+ const queue = observationQueue(this.key);
1348
+ const taken = new Map(queue.untaken);
1349
+ const rescan = queue.rescan;
1350
+ const sources = new Map(queue.suppliedSources);
1351
+ queue.untaken.clear();
1352
+ queue.suppliedSources.clear();
1353
+ queue.rescan = false;
1354
+ return {
1355
+ taken,
1356
+ rescan,
1357
+ sources
1358
+ };
1359
+ }
1360
+ /** `transaction` settles every waiting revision, and is the active
1361
+ * attempt equivalent observations join until it settles. */
1362
+ settleTaken(taken, transaction) {
1363
+ const queue = observationQueue(this.key);
1364
+ for (const [revision, attempt] of queue.revisions) if (attempt === null) queue.revisions.set(revision, transaction);
1365
+ queue.active = {
1366
+ taken,
1367
+ transaction
1368
+ };
1369
+ const settle = () => {
1370
+ if (queue.active?.transaction === transaction) queue.active = null;
1371
+ };
1372
+ transaction.then(settle, settle);
1373
+ }
1374
+ /** Registers one attempt in the build's single-flight slot before it
1375
+ * processes, so a nested full system reload inherits the gate rather
1376
+ * than clear it, and clears the slot when it settles either way. */
1377
+ runWatchAttempt(changes, rescan = false) {
1378
+ const transaction = Promise.resolve().then(() => rescan ? this.runFullPipeline([], changes.sources) : this.processWatchUpdate(changes));
1379
+ setWatchTransaction(this.key, transaction);
1380
+ const settle = () => {
1381
+ if (getWatchTransaction(this.key) === transaction) setWatchTransaction(this.key, null);
1382
+ };
1383
+ transaction.then(settle, settle);
1384
+ return transaction;
1385
+ }
879
1386
  async processWatchUpdate(changes) {
880
1387
  const rootDir = this.rootDir;
881
1388
  try {
@@ -886,31 +1393,25 @@ var ExtractionSession = class {
886
1393
  const changed = [...changes.modifiedFiles ?? [], ...changes.removedFiles ?? []];
887
1394
  let assetChanged = false;
888
1395
  let systemHit;
1396
+ let configHit;
889
1397
  try {
890
- if (!changes.modifiedFiles && !changes.removedFiles) for (const [dep, hash] of this.systemDependencyHashes) {
891
- let current = "";
892
- try {
893
- current = contentHash(readFileSync(dep, "utf-8"));
894
- } catch {}
895
- if (current !== hash) {
896
- systemHit = dep;
897
- break;
898
- }
899
- }
900
- else systemHit = changed.find((path) => toWatchKeys(path).some((key) => this.systemDependencyKeys.has(key)));
1398
+ if (changed.length === 0) {
1399
+ systemHit = firstChangedFile(this.systemDependencyHashes);
1400
+ if (systemHit === void 0) configHit = firstChangedFile(this.configInputHashes);
1401
+ } else systemHit = changed.find((path) => this.isSystemDependency(path));
901
1402
  } catch (err) {
902
1403
  this.warn(`HMR system-reload check failed: ${String(err)}`);
903
1404
  }
904
- if (systemHit) {
905
- this.log(`system reload: system dependency changed (${relative(rootDir, systemHit)})`);
1405
+ configHit ??= changed.find((path) => toWatchKeys(path).some((key) => this.configInputKeys.has(key)));
1406
+ const debtRetry = this.systemReloadOwed && this.heldRejection === null;
1407
+ if (systemHit || configHit || debtRetry) {
1408
+ this.log(systemHit ? `system reload: system dependency changed (${relative(rootDir, systemHit)})` : configHit ? `system reload: configuration changed (${relative(rootDir, configHit)})` : "system reload: retrying the reload a failure left owed");
906
1409
  this.resetForHmr();
907
1410
  try {
908
- const promise = this.runFullPipeline(this.pendingFromBatch(changes));
909
- setAnalysisStartedPromise(promise);
1411
+ const promise = this.runFullPipeline(this.pendingFromBatch(changes), changes.sources);
1412
+ setAnalysisStartedPromise(this.key, promise);
910
1413
  await promise;
911
- this.systemReloadOwed = false;
912
1414
  } catch (err) {
913
- if (err instanceof AbortedParseRejection) this.systemReloadOwed = true;
914
1415
  try {
915
1416
  this.beginStatusAttempt();
916
1417
  this.writeAnalysisStatus("failed", this.pendingFromBatch(changes), String(err));
@@ -925,7 +1426,7 @@ var ExtractionSession = class {
925
1426
  this.assetCopyCache.clear();
926
1427
  assetChanged = true;
927
1428
  }
928
- const { excludeMatcher, extensionsSet } = this.resolveScanConfig();
1429
+ const { excludeMatcher, extensionsSet } = this.scanConfig;
929
1430
  const priorCacheEntries = /* @__PURE__ */ new Map();
930
1431
  const priorExternalFileOwners = { ...this.externalFileOwners };
931
1432
  const recordPrior = (key) => {
@@ -933,6 +1434,7 @@ var ExtractionSession = class {
933
1434
  };
934
1435
  const inventoryUpdates = [];
935
1436
  const ownerRemovals = [];
1437
+ const removedProjectFiles = /* @__PURE__ */ new Map();
936
1438
  let removedAny = false;
937
1439
  if (changes.removedFiles) for (const removedPath of changes.removedFiles) {
938
1440
  let key;
@@ -946,8 +1448,10 @@ var ExtractionSession = class {
946
1448
  }
947
1449
  recordPrior(key);
948
1450
  this.clearIngestionFailure(key);
1451
+ this.hostSuppliedKeys.delete(key);
949
1452
  if (this.fileCache.delete(key)) {
950
1453
  removedAny = true;
1454
+ if (!owningRoot) removedProjectFiles.set(key, removedPath);
951
1455
  if (owningRoot) {
952
1456
  inventoryUpdates.push({
953
1457
  root: owningRoot,
@@ -978,15 +1482,29 @@ var ExtractionSession = class {
978
1482
  owningRoot: null
979
1483
  }));
980
1484
  const changedPaths = [];
1485
+ const inOutput = outputSourceGuard(rootDir, this.structuralExclude);
1486
+ let readFailuresChanged = false;
981
1487
  for (const target of targets) {
982
1488
  const relPath = target.key;
1489
+ const outputDir = inOutput(target.abs);
1490
+ if (outputDir !== null) {
1491
+ this.warn(outputSourceMessage(relPath, outputDir));
1492
+ continue;
1493
+ }
983
1494
  let source;
984
1495
  try {
985
- source = readFileSync(target.abs, "utf-8");
986
- } catch {
1496
+ const supplied = changes.sources?.get(target.abs);
1497
+ source = supplied ?? readFileSync(target.abs, "utf-8");
1498
+ if (supplied === void 0) this.hostSuppliedKeys.delete(relPath);
1499
+ else this.hostSuppliedKeys.add(relPath);
1500
+ } catch (err) {
1501
+ if (isDeletedSource(err)) continue;
1502
+ this.clearIngestionFailure(relPath);
1503
+ this.ingestionFailureDiagnostics = [...this.ingestionFailureDiagnostics, unreadableSourceDiagnostic(relPath, err)];
1504
+ readFailuresChanged = true;
987
1505
  continue;
988
1506
  }
989
- this.clearIngestionFailure(relPath);
1507
+ if (this.clearIngestionFailure(relPath)) readFailuresChanged = true;
990
1508
  const rawHash = contentHash(source);
991
1509
  const cached = this.fileCache.get(relPath);
992
1510
  const hash = rawHash;
@@ -1009,7 +1527,7 @@ var ExtractionSession = class {
1009
1527
  }
1010
1528
  }
1011
1529
  }
1012
- if (changedPaths.length > 0 || removedAny || assetChanged) {
1530
+ if (changedPaths.length > 0 || removedAny || readFailuresChanged || assetChanged) {
1013
1531
  const pending = changedPaths.map((rel) => [rel, this.fileCache.get(rel).hash]);
1014
1532
  const commitObservedBatch = () => {
1015
1533
  for (const update of inventoryUpdates) {
@@ -1035,23 +1553,30 @@ var ExtractionSession = class {
1035
1553
  });
1036
1554
  this.throwOnAbortedParse(ingested);
1037
1555
  this.skippedSources = ingested.skippedOriginals ?? [];
1556
+ this.ingestionOpenings = ingested.openings ?? [];
1557
+ this.ingestionAdapted = adaptedOriginals(ingested.ownership);
1038
1558
  if (this.systemReloadOwed) {
1039
1559
  this.log("system reload: publishing the reload a parse hold deferred");
1560
+ const stale = this.staleHostSuppliedSources(changes.sources);
1561
+ if (stale.length > 0) throw new DeferredReloadRefusal(`[${this.driverLabel}] system reload not published: its full read would take ${stale.join(", ")} from disk, which does not hold the bytes the host supplied; the last generation stays served, and the reload publishes with the next change once the disk holds them`);
1040
1562
  this.resetForHmr();
1041
- await this.runFullPipeline(pending);
1042
- this.systemReloadOwed = false;
1563
+ await this.runFullPipeline(pending, changes.sources);
1043
1564
  return;
1044
1565
  }
1045
1566
  this.externalFileOwners = projectExternalFileOwners(ingested, this.externalFileOwners);
1046
- resetAnalysisStartedPromise();
1567
+ resetAnalysisStartedPromise(this.key);
1047
1568
  const promise = this.runIncrementalPipeline(ingested.analysisEntries, pending);
1048
- setAnalysisStartedPromise(promise);
1569
+ setAnalysisStartedPromise(this.key, promise);
1049
1570
  await promise;
1050
1571
  } catch (err) {
1051
- if (err instanceof AbortedParseRejection) commitObservedBatch();
1572
+ if (err instanceof AbortedParseRejection || err instanceof DeferredReloadRefusal) commitObservedBatch();
1052
1573
  else {
1053
- for (const [key, prior] of priorCacheEntries) if (prior === null) this.fileCache.delete(key);
1054
- else this.fileCache.set(key, prior);
1574
+ for (const [key, prior] of priorCacheEntries) {
1575
+ const removed = removedProjectFiles.get(key);
1576
+ if (removed !== void 0 && !existsSync(removed)) continue;
1577
+ if (prior === null) this.fileCache.delete(key);
1578
+ else this.fileCache.set(key, prior);
1579
+ }
1055
1580
  this.externalFileOwners = priorExternalFileOwners;
1056
1581
  }
1057
1582
  if (this.statusAttemptOpen) {
@@ -1060,7 +1585,7 @@ var ExtractionSession = class {
1060
1585
  }
1061
1586
  throw err;
1062
1587
  }
1063
- this.publishSourceIngestion(ingested);
1588
+ this.publishSourceIngestion(ingested, "incremental", (path) => this.fileCache.get(path));
1064
1589
  commitObservedBatch();
1065
1590
  } else if (this.statusAttemptOpen) {
1066
1591
  this.debouncePending.clear();
@@ -1075,88 +1600,233 @@ var ExtractionSession = class {
1075
1600
  const rejection = this.corpus.rejection(prepared);
1076
1601
  if (!rejection) return;
1077
1602
  this.heldRejection = rejection.message;
1603
+ this.onOutcome?.({
1604
+ kind: "held",
1605
+ abortedOriginals: rejection.originals
1606
+ });
1078
1607
  throw new AbortedParseRejection(rejection.message);
1079
1608
  }
1080
- publishSourceIngestion(result) {
1609
+ /** Held sources whose bytes the host supplied and which the disk no
1610
+ * longer matches: a full read not carrying them (`carried`) would
1611
+ * publish the disk's bytes in their place. */
1612
+ staleHostSuppliedSources(carried) {
1613
+ const stale = [];
1614
+ for (const key of this.hostSuppliedKeys) {
1615
+ const held = this.fileCache.get(key);
1616
+ const abs = resolve(this.rootDir, key);
1617
+ if (held === void 0 || carried?.has(abs)) continue;
1618
+ let onDisk;
1619
+ try {
1620
+ onDisk = readFileSync(abs, "utf-8");
1621
+ } catch {
1622
+ onDisk = null;
1623
+ }
1624
+ if (onDisk === null || contentHash(onDisk) !== held.hash) stale.push(key);
1625
+ }
1626
+ return stale.sort();
1627
+ }
1628
+ /** `raw` is the attempt's input by original path, from which a
1629
+ * quarantined original stays held: every later pass re-ingests it, so it
1630
+ * keeps counting as skipped (nothing it renders is pruned) and its fix
1631
+ * edit compares against its bytes. */
1632
+ publishSourceIngestion(result, pass, raw) {
1633
+ const endedHold = this.heldRejection !== null;
1634
+ const previousOwnership = this.corpus.published.ownership;
1081
1635
  this.heldRejection = null;
1082
- this.fileCache = new Map(result.originalEntries.map((entry) => [entry.path, {
1636
+ const held = new Map(result.originalEntries.map((entry) => [entry.path, {
1083
1637
  hash: entry.hash,
1084
1638
  source: entry.source
1085
1639
  }]));
1640
+ for (const path of result.skippedOriginals ?? []) {
1641
+ const entry = raw(path);
1642
+ if (entry) held.set(path, {
1643
+ hash: entry.hash ?? contentHash(entry.source),
1644
+ source: entry.source
1645
+ });
1646
+ }
1647
+ this.fileCache = held;
1086
1648
  this.corpus.publish(result);
1649
+ const staged = this.stagedPublication;
1650
+ if (staged === null) return;
1651
+ this.stagedPublication = null;
1652
+ const previous = this.publication;
1653
+ const publication = {
1654
+ ...staged,
1655
+ ownership: result.ownership
1656
+ };
1657
+ this.publication = publication;
1658
+ this.onOutcome?.({
1659
+ kind: "published",
1660
+ publication,
1661
+ pass,
1662
+ planChanged: diffFilePlans(snapshotFilePlans(previous?.manifest ?? null), snapshotFilePlans(publication.manifest)),
1663
+ endedHold,
1664
+ redeliver: endedHold ? Object.values(result.ownership).filter((owner) => previousOwnership[owner.originalPath]?.originalHash !== owner.originalHash).map((owner) => owner.originalPath) : []
1665
+ });
1087
1666
  }
1088
1667
  /** Full analysis + publication, registered in the one in-flight
1089
- * transaction slot; a nested reload leaves the enclosing registration. */
1090
- async runFullPipeline(pending = []) {
1668
+ * transaction slot; a nested reload leaves the enclosing registration.
1669
+ * `sources` are bytes a host already read (see WatchChanges.sources):
1670
+ * the full read uses them, and those of the observations it takes, in
1671
+ * place of the disk. */
1672
+ async runFullPipeline(pending = [], sources = /* @__PURE__ */ new Map()) {
1091
1673
  if (this.releasePublicationClaim === null) {
1092
- const releaseOwner = claimExclusiveSessionOwner(`${this.driverLabel}:${this.sessionDir}`);
1093
- mkdirSync(this.sessionDir, { recursive: true });
1094
- const releaseClaimRecord = holdDirectoryClaim(this.sessionDir);
1095
- this.releasePublicationClaim = () => {
1096
- releaseOwner();
1097
- releaseClaimRecord();
1098
- };
1674
+ const releaseOwner = claimExclusiveSessionOwner(this.key, `${this.driverLabel}:${this.sessionDir}`);
1675
+ if (this.persistArtifacts) {
1676
+ mkdirSync(this.sessionDir, { recursive: true });
1677
+ holdBuildClaimRecord(this.key, () => holdDirectoryClaim(this.sessionDir, { build: this.sessionId }));
1678
+ }
1679
+ this.releasePublicationClaim = releaseOwner;
1099
1680
  }
1100
1681
  const priorSystem = this.system;
1101
- const nested = getWatchTransaction() !== null;
1102
- const transaction = this.runPipelineTransaction(pending);
1103
- if (!nested) setWatchTransaction(transaction);
1682
+ const priorRegistration = this.snapshotWatchRegistration();
1683
+ this.attemptRecordedSystemDependencies = false;
1684
+ const nested = getWatchTransaction(this.key) !== null;
1685
+ const observations = nested ? null : this.takeObservations();
1686
+ const taken = observations?.taken ?? null;
1687
+ const transaction = this.runPipelineTransaction(pending, new Map([...observations?.sources ?? [], ...sources]));
1688
+ if (taken !== null) {
1689
+ setWatchTransaction(this.key, transaction);
1690
+ this.settleTaken(taken, transaction);
1691
+ }
1104
1692
  try {
1105
1693
  await transaction;
1106
1694
  } catch (error) {
1107
1695
  this.system = priorSystem;
1696
+ this.restoreWatchRegistration(priorRegistration);
1697
+ if (priorSystem !== null && (this.development || error instanceof AbortedParseRejection)) this.systemReloadOwed = true;
1108
1698
  throw error;
1109
1699
  } finally {
1110
- if (!nested) setWatchTransaction(null);
1700
+ if (!nested) setWatchTransaction(this.key, null);
1701
+ }
1702
+ this.systemReloadOwed = false;
1703
+ this.lastGoodSystemDependencies = {
1704
+ keys: this.systemDependencyKeys,
1705
+ paths: this.systemDependencyPaths,
1706
+ missing: this.systemMissingPaths,
1707
+ hashes: this.systemDependencyHashes
1708
+ };
1709
+ this.commitSystemAuthority();
1710
+ }
1711
+ /** This attempt's system dependencies: the files its load read, and the
1712
+ * paths it found absent, whose creation is a system change. */
1713
+ recordSystemDependencies(systemPath, dependencies, missing) {
1714
+ const keys = /* @__PURE__ */ new Set();
1715
+ for (const path of [
1716
+ systemPath,
1717
+ ...dependencies,
1718
+ ...missing
1719
+ ]) for (const key of toWatchKeys(path)) keys.add(key);
1720
+ const hashes = /* @__PURE__ */ new Map();
1721
+ for (const dep of dependencies.length > 0 ? dependencies : [systemPath]) try {
1722
+ hashes.set(dep, contentHash(readFileSync(dep, "utf-8")));
1723
+ } catch {
1724
+ hashes.set(dep, "");
1725
+ }
1726
+ for (const path of missing) hashes.set(path, "");
1727
+ this.systemDependencyKeys = keys;
1728
+ this.systemDependencyPaths = dependencies;
1729
+ this.systemMissingPaths = missing;
1730
+ this.systemDependencyHashes = hashes;
1731
+ this.attemptRecordedSystemDependencies = true;
1732
+ }
1733
+ /** Records the configuration inputs and, for a tsconfig-aliased host,
1734
+ * re-reads its aliases. */
1735
+ readConfigInputs(rootDir) {
1736
+ this.configInputPaths = [join(rootDir, "package.json"), ...tsconfigChainPaths(rootDir)];
1737
+ this.configInputKeys = new Set(this.configInputPaths.flatMap((path) => toWatchKeys(path)));
1738
+ this.configInputHashes = new Map(this.configInputPaths.map((path) => [path, fileHashOrEmpty(path)]));
1739
+ if (this.aliasesFromTsconfig) this.pathAliasesJson = buildPathAliasesJson(readTsconfigAliasPairs(rootDir), rootDir)?.json ?? null;
1740
+ }
1741
+ /** What hosts watch for assets, and what the last-good generation
1742
+ * reported about its kits and packages, which a failed full pass must
1743
+ * leave in place. */
1744
+ snapshotWatchRegistration() {
1745
+ return {
1746
+ assetDependencyPaths: new Set(this.assetDependencyPaths),
1747
+ assetDependencyKeys: new Set(this.assetDependencyKeys),
1748
+ kitDescriptors: this.kitDescriptors,
1749
+ lastExternalOutcomes: this.lastExternalOutcomes
1750
+ };
1751
+ }
1752
+ /** Restores the last-good registration. The system dependencies are the
1753
+ * last committed pass's and, when its load got that far, this failed
1754
+ * attempt's (spec §2.3): a fix to any of them must reach the owed reload,
1755
+ * and an earlier failure's are replaced, never accumulated. */
1756
+ restoreWatchRegistration(prior) {
1757
+ Object.assign(this, prior);
1758
+ const lastGood = this.lastGoodSystemDependencies;
1759
+ if (!this.attemptRecordedSystemDependencies) {
1760
+ this.systemDependencyKeys = lastGood.keys;
1761
+ this.systemDependencyPaths = lastGood.paths;
1762
+ this.systemMissingPaths = lastGood.missing;
1763
+ this.systemDependencyHashes = lastGood.hashes;
1764
+ return;
1111
1765
  }
1766
+ this.systemMissingPaths = [.../* @__PURE__ */ new Set([...lastGood.missing, ...this.systemMissingPaths])];
1767
+ this.systemDependencyKeys = /* @__PURE__ */ new Set([...lastGood.keys, ...this.systemDependencyKeys]);
1768
+ this.systemDependencyPaths = [.../* @__PURE__ */ new Set([...lastGood.paths, ...this.systemDependencyPaths])];
1769
+ this.systemDependencyHashes = new Map([...this.systemDependencyHashes, ...lastGood.hashes]);
1770
+ }
1771
+ /** The build's authority becomes the system this session committed, with
1772
+ * it and never before: a failed reload leaves the committed one, and the
1773
+ * context proofs made against it. A committed reload discards every
1774
+ * other context's proof, even for an unchanged system: the reload may
1775
+ * have changed what another context resolves, so each loads again. */
1776
+ commitSystemAuthority() {
1777
+ setSystemAuthority(this.key, {
1778
+ ...systemLoad(this.system),
1779
+ context: this.resolutionContextText(),
1780
+ label: this.resolutionContextLabel(),
1781
+ others: /* @__PURE__ */ new Map()
1782
+ });
1112
1783
  }
1113
- async runPipelineTransaction(pending) {
1784
+ async runPipelineTransaction(pending, hostSources) {
1114
1785
  const pipelineStart = this.now();
1115
1786
  const bt = {};
1116
1787
  this.runSessionStartHygiene();
1117
- setSessionArtifactDir(this.sessionDir);
1788
+ setSessionArtifactDir(this.key, this.sessionDir);
1118
1789
  const rootDir = this.rootDir;
1119
1790
  const priorSourceState = this.snapshotSourceState();
1120
1791
  const resolvedSystemPath = resolve(rootDir, this.options.system);
1792
+ this.readConfigInputs(rootDir);
1793
+ const context = {
1794
+ conditions: this.conditions,
1795
+ workspaces: declaredWorkspacePackages(rootDir),
1796
+ extensions: this.extensionPolicy
1797
+ };
1798
+ this.log(`extension order: ${context.extensions.resolverVersion}`);
1121
1799
  let t = this.now();
1122
- this.system = loadSystemConfig(engineApi, {
1123
- systemPath: resolvedSystemPath,
1124
- rootDir,
1125
- prefix: this.options.prefix,
1126
- prefixContextualVars: this.options.prefixContextualVars,
1127
- conditions: this.conditions
1128
- });
1800
+ try {
1801
+ this.system = this.loadSystemUnder(context);
1802
+ } catch (error) {
1803
+ const reads = systemLoadReads(error);
1804
+ this.recordSystemDependencies(resolvedSystemPath, reads.dependencies, reads.missing);
1805
+ throw error;
1806
+ }
1129
1807
  this.assetCopyCache.clear();
1130
1808
  this.assetDependencyPaths.clear();
1131
1809
  this.assetDependencyKeys.clear();
1132
- {
1133
- const deps = this.system.dependencies ?? [];
1134
- const keys = /* @__PURE__ */ new Set();
1135
- for (const key of toWatchKeys(resolvedSystemPath)) keys.add(key);
1136
- for (const dep of deps) for (const key of toWatchKeys(dep)) keys.add(key);
1137
- this.systemDependencyKeys = keys;
1138
- this.systemDependencyPaths = deps;
1139
- const hashes = /* @__PURE__ */ new Map();
1140
- for (const dep of deps.length > 0 ? deps : [resolvedSystemPath]) try {
1141
- hashes.set(dep, contentHash(readFileSync(dep, "utf-8")));
1142
- } catch {
1143
- hashes.set(dep, "");
1144
- }
1145
- this.systemDependencyHashes = hashes;
1146
- }
1810
+ this.recordSystemDependencies(resolvedSystemPath, this.system.dependencies ?? [], this.system.missing ?? []);
1147
1811
  bt.systemLoad = this.elapsed(t);
1148
1812
  t = this.now();
1149
- const { excludeMatcher, extensionsSet } = this.resolveScanConfig();
1813
+ const { excludeMatcher, extensionsSet } = this.scanConfig;
1150
1814
  const files = discoverFiles(rootDir, rootDir, excludeMatcher, extensionsSet);
1815
+ if (claimUnmatchedExcludeReport(this.key)) for (const line of unmatchedExcludeLines(excludeMatcher)) this.logger.error(line);
1151
1816
  bt.fileDiscovery = this.elapsed(t);
1152
1817
  t = this.now();
1153
1818
  const rawEntries = [];
1154
1819
  const ingestionFailures = [];
1820
+ const readPaths = /* @__PURE__ */ new Set();
1155
1821
  for (const filePath of files) {
1156
1822
  const relPath = relative(rootDir, filePath);
1823
+ readPaths.add(filePath);
1157
1824
  let source;
1158
1825
  try {
1159
- source = readFileSync(filePath, "utf-8");
1826
+ const supplied = hostSources.get(filePath);
1827
+ source = supplied ?? readFileSync(filePath, "utf-8");
1828
+ if (supplied === void 0) this.hostSuppliedKeys.delete(relPath);
1829
+ else this.hostSuppliedKeys.add(relPath);
1160
1830
  } catch (err) {
1161
1831
  if (!isDeletedSource(err)) ingestionFailures.push(unreadableSourceDiagnostic(relPath, err));
1162
1832
  continue;
@@ -1168,21 +1838,36 @@ var ExtractionSession = class {
1168
1838
  });
1169
1839
  }
1170
1840
  bt.fileRead = this.elapsed(t);
1841
+ const rawPaths = new Set(rawEntries.map((entry) => entry.path));
1171
1842
  t = this.now();
1172
- const discoveryDiagnostics = [];
1173
- const packageNames = await extractSystemFilePackages(resolvedSystemPath, engineModuleParser(engineApi()), (diagnostic) => discoveryDiagnostics.push(diagnostic), (name) => {
1174
- const entry = resolvePackagesByName(rootDir, [name])[name];
1175
- return entry ? resolve(rootDir, entry) : null;
1843
+ const resolver = discoveryResolver(this.hostResolve, this.engine(), {
1844
+ rootDir,
1845
+ conditions: context.conditions,
1846
+ workspaces: context.workspaces,
1847
+ extensions: context.extensions
1176
1848
  });
1177
- discoveryDiagnostics.push(...importedKitDiagnostics(rawEntries, engineApi(), rootDir, packageNames));
1178
- const preResolved = resolvePackagesByName(rootDir, packageNames);
1849
+ const discoveryDiagnostics = [];
1850
+ const systemPackageRoot = findPackageRoot(resolvedSystemPath);
1851
+ const kitMembers = /* @__PURE__ */ new Map();
1852
+ const packageNames = await extractSystemFilePackages(resolvedSystemPath, { systemMembership: this.engine().systemMembership ?? requireEngine().systemMembership }, (diagnostic) => discoveryDiagnostics.push(diagnostic), (specifier, importer) => findPackageRoot(importer) === systemPackageRoot ? resolver.resolveSpecifier(specifier) : resolver.loaderResolve(specifier, dirname(importer)), context.extensions, kitMembers);
1853
+ discoveryDiagnostics.push(...importedKitDiagnostics(rawEntries, this.engine(), rootDir, packageNames, (entries) => this.corpus.facts(entries)));
1179
1854
  const rawExternalFiles = /* @__PURE__ */ new Map();
1180
1855
  const collected = await collectExternalPackageSources({
1856
+ extensions: context.extensions,
1857
+ kitSourceRoot: nativeKitSourceRoot(this.engine(), rootDir, context.conditions, context.workspaces, extensionPolicyJson(context.extensions)),
1858
+ locatePackage: nativePackageLocator(this.engine(), context.workspaces),
1181
1859
  specifiers: packageNames,
1182
- resolveSpecifier: (name) => preResolved[name] ? resolve(rootDir, preResolved[name]) : null,
1860
+ resolveSpecifier: async (specifier) => kitMembers.get(specifier) ?? await resolver.resolveSpecifier(specifier),
1861
+ locateFrom: (specifier) => {
1862
+ const file = kitMembers.get(specifier);
1863
+ return file === void 0 ? rootDir : dirname(file);
1864
+ },
1865
+ unresolvedReason: resolver.unresolvedReason,
1866
+ requireSourceEntry: resolver.requireSourceEntry,
1183
1867
  rootDir,
1184
1868
  extensionsSet,
1185
- hasEntry: (relPath) => rawEntries.some((e) => e.path === relPath),
1869
+ hasEntry: (relPath) => rawPaths.has(relPath),
1870
+ hostOutputDirs: this.structuralExclude.map((dir) => resolve(rootDir, dir)),
1186
1871
  onSourceRead: (source, _relPath, absPath) => {
1187
1872
  rawExternalFiles.set(absPath, contentHash(source));
1188
1873
  },
@@ -1201,21 +1886,22 @@ var ExtractionSession = class {
1201
1886
  });
1202
1887
  this.lastExternalOutcomes = collected.outcomes;
1203
1888
  this.ingestionFailureDiagnostics = ingestionFailures;
1889
+ if (resolver.hostFirst) discoveryDiagnostics.push(...systemInstanceMismatches({
1890
+ rootDir,
1891
+ specifiers: packageNames,
1892
+ instances: collected.packageInstances,
1893
+ evaluated: this.systemDependencyPaths,
1894
+ loaderResolve: (specifier) => resolver.loaderResolve(specifier, dirname(resolvedSystemPath))
1895
+ }));
1204
1896
  surfaceManifestDiagnostics({ diagnostics: [...discoveryDiagnostics, ...collected.diagnostics] }, (message) => this.warn(message), {
1205
1897
  levels: this.options.diagnostics,
1206
1898
  strict: this.options.strict,
1207
1899
  reportErrors: this.reportErrors()
1208
1900
  });
1209
- const unresolvableMessage = unresolvableIncludesMessage(collected.outcomes);
1210
- if (unresolvableMessage !== null) {
1211
- if (this.options.strict) this.strictFailure(unresolvableMessage);
1212
- else this.warn(unresolvableMessage);
1213
- }
1901
+ const unresolvableMessage = unresolvableIncludesMessage(collected.outcomes, relative(rootDir, resolvedSystemPath));
1902
+ if (unresolvableMessage !== null) this.strictOrWarn(unresolvableMessage);
1214
1903
  const staleDistMessage = staleDistIncludesMessage(collected.outcomes);
1215
- if (staleDistMessage !== null) {
1216
- if (this.options.strict) this.strictFailure(staleDistMessage);
1217
- else this.warn(staleDistMessage);
1218
- }
1904
+ if (staleDistMessage !== null) this.strictOrWarn(staleDistMessage);
1219
1905
  const rejectedSpecifiers = /* @__PURE__ */ new Set();
1220
1906
  const crossVolumeDetails = [];
1221
1907
  for (const [dir, specifiers] of Object.entries(collected.dirOwnerSets)) {
@@ -1232,6 +1918,7 @@ var ExtractionSession = class {
1232
1918
  for (const message of this.stickyDiagnostics.values()) this.warn(message);
1233
1919
  const admitted = excludeCollectedPackages(collected, rejectedSpecifiers, rootDir);
1234
1920
  this.kitDescriptors = admitted.kitDescriptors;
1921
+ this.packageInstances = admitted.packageInstances;
1235
1922
  try {
1236
1923
  const identity = createSourceIdentity(rootDir);
1237
1924
  this.externalRootOwners = /* @__PURE__ */ new Map();
@@ -1267,23 +1954,29 @@ var ExtractionSession = class {
1267
1954
  this.externalSourceEntries = admitted.sourceEntries;
1268
1955
  this.externalSourceSideEffects = admitted.sourceEntrySideEffects;
1269
1956
  for (const entry of admitted.entries) {
1270
- const hash = contentHash(entry.source);
1957
+ const entryPath = resolve(rootDir, entry.path);
1958
+ readPaths.add(entryPath);
1959
+ const source = hostSources.get(entryPath) ?? entry.source;
1271
1960
  rawEntries.push({
1272
1961
  path: entry.path,
1273
- source: entry.source,
1274
- hash
1962
+ source,
1963
+ hash: contentHash(source)
1275
1964
  });
1276
1965
  }
1277
1966
  this.externalPackageDirs = admitted.packageDirs;
1278
1967
  this.externalLinkedDirs = admitted.linkedDirs;
1279
1968
  this.externalOutputDirs = admitted.outputDirs;
1280
1969
  bt.packageResolve = this.elapsed(t);
1970
+ const misspelled = misspelledHostSources(hostSources, readPaths).map(({ path, read }) => `${relative(rootDir, path)} (read as ${read.map((readPath) => relative(rootDir, readPath)).join(", ")})`);
1971
+ if (misspelled.length > 0) throw new DeferredReloadRefusal(`[${this.driverLabel}] reload not published: the host supplied bytes for ${misspelled.join(", ")}, which the full read reaches only under another spelling; the last generation stays served`);
1281
1972
  this.beginStatusAttempt();
1282
1973
  let accepted;
1283
1974
  try {
1284
- accepted = await this.corpus.prepare(rawEntries);
1975
+ accepted = await this.corpus.prepare(this.withoutOutputSources(rawEntries));
1285
1976
  this.throwOnAbortedParse(accepted);
1286
1977
  this.skippedSources = accepted.skippedOriginals ?? [];
1978
+ this.ingestionOpenings = accepted.openings ?? [];
1979
+ this.ingestionAdapted = adaptedOriginals(accepted.ownership);
1287
1980
  } catch (err) {
1288
1981
  this.debouncePending.clear();
1289
1982
  this.writeAnalysisStatus("failed", pending, String(err));
@@ -1291,19 +1984,30 @@ var ExtractionSession = class {
1291
1984
  }
1292
1985
  this.externalFileOwners = projectExternalFileOwners(accepted, this.externalFileOwners);
1293
1986
  bt.fileCount = accepted.analysisEntries.length;
1294
- clearEngineCache(engineApi);
1987
+ clearEngineCache(() => this.engine());
1295
1988
  await this.analyzeAndEmit(accepted.analysisEntries, packageMap, false, bt, pipelineStart, pending);
1296
- this.publishSourceIngestion(accepted);
1297
- setSharedExternalDirs(admitted.packageDirs);
1298
- setSharedExternalEntries(admitted.sourceEntries);
1299
- setSharedExternalSideEffects(admitted.sourceEntrySideEffects);
1989
+ this.publishSourceIngestion(accepted, "full", (path) => rawEntries.find((entry) => entry.path === path));
1990
+ setSharedExternalDirs(this.key, admitted.packageDirs);
1991
+ setSharedExternalEntries(this.key, admitted.sourceEntries);
1992
+ setSharedExternalSideEffects(this.key, admitted.sourceEntrySideEffects);
1300
1993
  this.onExternalRootsCommitted?.(this.externalWatchRoots);
1301
- setOwningWatchSession(this);
1994
+ setOwningWatchSession(this.key, this);
1302
1995
  } catch (err) {
1303
1996
  this.restoreSourceState(priorSourceState);
1304
1997
  throw err;
1305
1998
  }
1306
1999
  }
2000
+ /** The full pass's analysis inputs, less any file whose real path lies in
2001
+ * an output directory, each reported. */
2002
+ withoutOutputSources(entries) {
2003
+ const inOutput = outputSourceGuard(this.rootDir, this.structuralExclude);
2004
+ return entries.filter((entry) => {
2005
+ const outputDir = inOutput(resolve(this.rootDir, entry.path));
2006
+ if (outputDir === null) return true;
2007
+ this.warn(outputSourceMessage(entry.path, outputDir));
2008
+ return false;
2009
+ });
2010
+ }
1307
2011
  /** Per-generation source state captured as ONE object so the restore
1308
2012
  * cannot omit a member; deep-copied fields are mutated in place. */
1309
2013
  snapshotSourceState() {
@@ -1325,16 +2029,92 @@ var ExtractionSession = class {
1325
2029
  restoreSourceState(snapshot) {
1326
2030
  Object.assign(this, snapshot);
1327
2031
  }
1328
- /** Release this session's publication claim so a successor may publish.
1329
- * Idempotent, and scoped to this session's own claim. */
2032
+ /** The system under `context`: the one load path for the build's
2033
+ * authority and for a participant checking its own context. */
2034
+ loadSystemUnder(context) {
2035
+ return loadSystemConfig(() => this.engine(), {
2036
+ systemPath: resolve(this.rootDir, this.options.system),
2037
+ rootDir: this.rootDir,
2038
+ prefix: this.options.prefix,
2039
+ prefixContextualVars: this.options.prefixContextualVars,
2040
+ conditions: context.conditions,
2041
+ workspaces: context.workspaces,
2042
+ extensions: context.extensions
2043
+ });
2044
+ }
2045
+ /** What the system loader reads of this participant besides the root, as
2046
+ * stable text: its conditions and extension order. */
2047
+ resolutionContextText() {
2048
+ return JSON.stringify([this.conditions, extensionPolicyJson(this.extensionPolicy)]);
2049
+ }
2050
+ resolutionContextLabel() {
2051
+ return `${this.driverLabel} with conditions [${this.conditions.join(", ")}] and extension order ${this.extensionPolicy.resolverVersion}`;
2052
+ }
2053
+ /** One agreed system authority (SA5): a participant whose resolution
2054
+ * context differs from the one the build's system was loaded under loads
2055
+ * it once under its own, and proceeds only on the same system. Otherwise
2056
+ * it throws, naming both contexts, whichever participant came first. */
2057
+ agreeWithSystemAuthority() {
2058
+ const authority = getSystemAuthority(this.key);
2059
+ if (authority === null) return;
2060
+ const context = this.resolutionContextText();
2061
+ if (context === authority.context) return;
2062
+ let own = authority.others.get(context);
2063
+ if (own === void 0) {
2064
+ own = systemLoad(this.loadSystemUnder({
2065
+ conditions: this.conditions,
2066
+ workspaces: declaredWorkspacePackages(this.rootDir),
2067
+ extensions: this.extensionPolicy
2068
+ }));
2069
+ authority.others.set(context, own);
2070
+ }
2071
+ if (own.identity === authority.identity) return;
2072
+ const shared = new Set(authority.dependencies);
2073
+ const mine = new Set(own.dependencies);
2074
+ const differing = [...authority.dependencies.filter((path) => !mine.has(path)), ...own.dependencies.filter((path) => !shared.has(path))].map((path) => relative(this.rootDir, path));
2075
+ throw new SessionConfigurationConflict(this.key, authority.label, this.resolutionContextLabel(), differing.length > 0 ? [`system modules ${differing.join(", ")}`] : ["the evaluated system"], "Every participant of one build must evaluate the same system: give each compiler conditions that select the same system modules.");
2076
+ }
2077
+ /** Release this session's publication claim so a successor may publish,
2078
+ * and leave the build, which retires with its last participant.
2079
+ * Idempotent; `reattach` joins the build again. */
1330
2080
  close() {
1331
2081
  this.releasePublicationClaim?.();
1332
2082
  this.releasePublicationClaim = null;
2083
+ if (this.joinedKey === null || this.participant === null) return;
2084
+ if (getOwningWatchSession(this.joinedKey) === this) setOwningWatchSession(this.joinedKey, null);
2085
+ leaveSession(this.joinedKey, this.participant);
2086
+ this.participant = null;
2087
+ this.detached = true;
2088
+ }
2089
+ /** A closed participant attaching again: its private analysis is stale,
2090
+ * so it is discarded, and it joins its build again. Idempotent. */
2091
+ reattach() {
2092
+ if (!this.detached) return;
2093
+ this.detached = false;
2094
+ this.system = null;
2095
+ this.joinedKey = null;
2096
+ }
2097
+ /** Brings a reattached participant up to date with a full rescan of a
2098
+ * live build, which reconciles deletions too; true then. A retired build
2099
+ * is false: the host starts it as at first. */
2100
+ async catchUp() {
2101
+ if (getManifestJson(this.key) === null) return false;
2102
+ await this.awaitObservation(this.recordRescan());
2103
+ return true;
2104
+ }
2105
+ /** Queues a full rescan under a new revision. It is never equivalent to
2106
+ * an active attempt: only a full read covers it. */
2107
+ recordRescan() {
2108
+ const queue = observationQueue(this.key);
2109
+ queue.rescan = true;
2110
+ const revision = ++queue.revision;
2111
+ queue.revisions.set(revision, null);
2112
+ return revision;
1333
2113
  }
1334
2114
  /** Reset analysis state for a system reload: write guards go back to null
1335
2115
  * and reseed from the disk envelopes on the next publication. */
1336
2116
  resetForHmr() {
1337
- resetAnalysisStartedPromise();
2117
+ resetAnalysisStartedPromise(this.key);
1338
2118
  this.artifactRecords = {
1339
2119
  manifest: null,
1340
2120
  inputs: null,
@@ -1345,7 +2125,7 @@ var ExtractionSession = class {
1345
2125
  /** The (sourceKey, observed hash) pairs of a watch batch — the status
1346
2126
  * file's pending set; loaders wait only on observed inputs. */
1347
2127
  pendingFromBatch(changes) {
1348
- const { excludeMatcher, extensionsSet } = this.resolveScanConfig();
2128
+ const { excludeMatcher, extensionsSet } = this.scanConfig;
1349
2129
  const pending = [];
1350
2130
  for (const path of changes.modifiedFiles ?? []) {
1351
2131
  const classified = this.classifyWatchPath(path, {
@@ -1354,7 +2134,7 @@ var ExtractionSession = class {
1354
2134
  });
1355
2135
  if (!classified) continue;
1356
2136
  try {
1357
- pending.push([classified.key, contentHash(readFileSync(path, "utf-8"))]);
2137
+ pending.push([classified.key, contentHash(changes.sources?.get(path) ?? readFileSync(path, "utf-8"))]);
1358
2138
  } catch {}
1359
2139
  }
1360
2140
  return pending;
@@ -1366,7 +2146,7 @@ var ExtractionSession = class {
1366
2146
  if (!identity) return changes;
1367
2147
  if (!changes.modifiedFiles && !changes.removedFiles) return changes;
1368
2148
  if (this.externalWatchRoots.length === 0) return changes;
1369
- const { extensionsSet } = this.resolveScanConfig();
2149
+ const { extensionsSet } = this.scanConfig;
1370
2150
  const dirtyRoots = /* @__PURE__ */ new Set();
1371
2151
  const modified = /* @__PURE__ */ new Set();
1372
2152
  const removed = /* @__PURE__ */ new Set();
@@ -1404,13 +2184,14 @@ var ExtractionSession = class {
1404
2184
  } catch {
1405
2185
  continue;
1406
2186
  }
1407
- if (previous.get(resolved.sourceKey)?.hash !== rawHash) modified.add(abs);
2187
+ if (previous.get(resolved.sourceKey)?.hash !== rawHash || this.ingestionFailureDiagnostics.some((diagnostic) => diagnostic.file === resolved.sourceKey)) modified.add(abs);
1408
2188
  }
1409
2189
  for (const [key, entry] of previous) if (!seen.has(key)) removed.add(entry.abs);
1410
2190
  }
1411
2191
  return {
1412
2192
  modifiedFiles: modified,
1413
- removedFiles: removed
2193
+ removedFiles: removed,
2194
+ sources: changes.sources
1414
2195
  };
1415
2196
  }
1416
2197
  /** Route one watcher path: a project-root member (consumer excludes apply)
@@ -1477,33 +2258,40 @@ var ExtractionSession = class {
1477
2258
  packageMap,
1478
2259
  system,
1479
2260
  emitter: {
1480
- runtimeImport: "@animus-ui/system/runtime",
1481
- cssModuleId: ANIMUS_CSS_MODULE_ID,
2261
+ runtimeImport: this.runtimeImport,
2262
+ cssModuleId: this.cssModuleId,
1482
2263
  systemPropsModuleId: this.systemPropsModuleId ?? systemPropsPath(this.sessionDir)
1483
2264
  },
1484
2265
  pathAliasesJson: this.pathAliasesJson,
1485
2266
  staticCssJson: this.staticCssJson,
1486
2267
  externalDirs: this.externalPackageDirs.map((dir) => relative(this.rootDir, dir)),
2268
+ rootDir: this.rootDir,
2269
+ assets: this.assetClassification,
2270
+ adapted: this.ingestionAdapted,
2271
+ loadExpander: this.loadExpander,
1487
2272
  devMode: this.engineDevMode(devMode),
1488
2273
  analysisContext: {
1489
2274
  skippedSources: [...this.skippedSources, ...this.ingestionFailureDiagnostics.map((d) => d.file)].map((file) => isAbsolute(file) ? relative(this.rootDir, file) : file),
1490
2275
  unbundledComputedImports: this.host.unbundledComputedImports,
2276
+ openings: [...this.ingestionOpenings],
1491
2277
  packageDirs: this.externalPackageDirs.map((dir) => relative(this.rootDir, dir)),
1492
2278
  linkedDirs: this.externalLinkedDirs.map((dir) => relative(this.rootDir, dir)),
1493
- outputDirs: this.externalOutputDirs.map((dir) => relative(this.rootDir, dir))
2279
+ outputDirs: this.externalOutputDirs.map((dir) => relative(this.rootDir, dir)),
2280
+ extensions: this.extensionPolicy
1494
2281
  }
1495
2282
  };
1496
2283
  this.writeAnalysisStatus("analyzing", pending);
1497
- const result = runProjectAnalysis(engineApi, {
2284
+ const result = runProjectAnalysis(() => this.engine(), {
1498
2285
  ...analysisOptions,
1499
2286
  warn: (message) => this.warn(message),
1500
2287
  info: (message) => this.log(message),
1501
2288
  strict: this.options.strict,
1502
2289
  diagnostics: this.options.diagnostics,
1503
2290
  reportErrors: this.reportErrors(),
1504
- extraDiagnostics: this.ingestionFailureDiagnostics,
2291
+ extraDiagnostics: [...this.ingestionFailureDiagnostics, ...await this.compareAliasesWithHost(fileEntries)],
1505
2292
  kitDescriptors: this.kitDescriptors
1506
2293
  });
2294
+ if (result.manifest) assertRuntimeImportSuppliesTerminals(this.runtimeImport, result.manifest);
1507
2295
  enforceExternalTokenContracts({
1508
2296
  diagnostics: result.manifest?.diagnostics,
1509
2297
  fileOwners: this.externalFileOwners,
@@ -1538,12 +2326,12 @@ var ExtractionSession = class {
1538
2326
  declaration,
1539
2327
  variables,
1540
2328
  postProcessCss(body, {
1541
- minify: this.options.minify ?? resolveMode(this.options.mode, () => process.env.NODE_ENV === "production" ? "production" : "development").mode === "production",
2329
+ minify: this.options.minify ?? this.startupMode === "production",
1542
2330
  targets: this.lcssTargets,
1543
2331
  warnFn: (msg) => this.warn(msg)
1544
2332
  })
1545
2333
  ].filter(Boolean).join("\n");
1546
- reportSurvivingAssetPlaceholders(fullCss, {
2334
+ if (!this.hostSubstitutesAssets) reportSurvivingAssetPlaceholders(fullCss, {
1547
2335
  strict: this.options.strict,
1548
2336
  warn: (message) => this.warn(message),
1549
2337
  reportErrors: this.reportErrors(),
@@ -1556,7 +2344,7 @@ var ExtractionSession = class {
1556
2344
  prefix: `[${this.driverLabel}]`,
1557
2345
  surface: "generated runtime modules"
1558
2346
  });
1559
- setSharedCss(fullCss);
2347
+ setSharedCss(this.key, fullCss);
1560
2348
  const systemPropsContent = buildSystemPropsModule({
1561
2349
  systemPropMapJson: JSON.stringify(manifest?.system_prop_map ?? {}),
1562
2350
  groupRegistryJson: system.groupRegistryJson,
@@ -1564,9 +2352,20 @@ var ExtractionSession = class {
1564
2352
  admittedTransforms: manifest?.admitted_transforms ?? {},
1565
2353
  typedSystemProps: manifest?.typed_system_props ?? []
1566
2354
  });
1567
- setSharedSystemProps(systemPropsContent);
1568
- setManifestJson(result.manifestJson);
1569
- setAnalyzedHashes(new Map(fileEntries.map((entry) => [entry.path, entry.hash])));
2355
+ setSharedSystemProps(this.key, systemPropsContent);
2356
+ setManifestJson(this.key, result.manifestJson);
2357
+ if (manifest) this.stagedPublication = {
2358
+ manifest,
2359
+ manifestJson: result.manifestJson,
2360
+ analysisGlobalCss: sheets.globalCss,
2361
+ analysisComponentCss: sheets.componentCss,
2362
+ styles: fullCss,
2363
+ systemPropsModule: systemPropsContent,
2364
+ system,
2365
+ packageMap
2366
+ };
2367
+ setAnalyzedHashes(this.key, new Map(fileEntries.map((entry) => [entry.path, entry.hash])));
2368
+ if (this.packageReadRecord !== null) setSharedPackageReadRecord(this.key, this.packageReadRecord);
1570
2369
  this.writeAnalysisStatus("committing", pending);
1571
2370
  const epoch = hashReplacementPlans(snapshotFilePlans(manifest), systemPropsContent);
1572
2371
  if (this.lastCommit === null) this.lastCommit = this.seedCommitFromDisk();
@@ -1599,6 +2398,86 @@ var ExtractionSession = class {
1599
2398
  }
1600
2399
  /** Write the session's analysis-status artifact. Terminal states
1601
2400
  * (idle/failed) close the attempt. */
2401
+ /** Where the host resolves a path alias the analysis reads to another
2402
+ * file (spec §2.2 A7, report only), and how the two were compared. */
2403
+ async compareAliasesWithHost(fileEntries) {
2404
+ const record = packageReadRecord({
2405
+ rootDir: this.rootDir,
2406
+ roots: this.packageInstances,
2407
+ redirected: new Set(this.externalSourceEntries.keys()),
2408
+ analysed: fileEntries.map((entry) => entry.path),
2409
+ rendersBuilder: (file) => this.rendersBuilder(file)
2410
+ });
2411
+ this.packageReadRecord = record;
2412
+ const { moduleSpecifiers } = this.engine();
2413
+ if (this.host.noBundler) return [];
2414
+ if (this.hostResolve === null || moduleSpecifiers === void 0) {
2415
+ this.log("path aliases: no host resolver, comparison unavailable");
2416
+ return [];
2417
+ }
2418
+ try {
2419
+ const { findings, unavailable } = await compareAliases({
2420
+ rootDir: this.rootDir,
2421
+ fileEntries,
2422
+ pathAliasesJson: this.pathAliasesJson,
2423
+ order: this.extensionPolicy,
2424
+ readings: {
2425
+ isAsset: this.assetClassification.isAsset,
2426
+ adapted: this.ingestionAdapted
2427
+ },
2428
+ moduleSpecifiers,
2429
+ resolve: this.hostResolve,
2430
+ cache: this.aliasCache,
2431
+ packages: record
2432
+ });
2433
+ for (const finding of findings) record.reported.add(`${finding.file}\0${finding.component}`);
2434
+ if (unavailable > 0) this.log(`path aliases: ${unavailable} aliased import(s) the host's resolver failed on are unavailable, not agreeing`);
2435
+ return findings;
2436
+ } catch (error) {
2437
+ this.log(`path aliases: comparison unavailable (${String(error)})`);
2438
+ return [];
2439
+ }
2440
+ }
2441
+ /** Whether the file at a real path declares builder chains, by the
2442
+ * engine's own parse: a module that does, served unread, renders with
2443
+ * empty classes. One the engine cannot read counts as one that does. */
2444
+ rendersBuilder(file) {
2445
+ if (!isEngineTransformExtension(file)) return false;
2446
+ const { extractFacts } = this.engine();
2447
+ if (extractFacts === void 0) return true;
2448
+ const path = relative(this.rootDir, file);
2449
+ try {
2450
+ const facts = parseInternalWire(extractFacts(JSON.stringify([{
2451
+ path,
2452
+ source: readFileSync(file, "utf-8")
2453
+ }])), "extractFacts").files[path];
2454
+ return facts === void 0 || facts.parsePanicked || facts.chains.length > 0;
2455
+ } catch {
2456
+ return true;
2457
+ }
2458
+ }
2459
+ /**
2460
+ * An import the host resolved while bundling (spec §2.2 A4), reported as
2461
+ * the comparison reports it: webpack resolves each module's requests with
2462
+ * the resolve options of the rules that module matches, which its
2463
+ * resolver, asked outside a compilation, does not apply. `importer` and
2464
+ * `resolved` are absolute. A finding the comparison already reported is
2465
+ * not repeated; an error-level one throws in a build.
2466
+ */
2467
+ reportHostResolution(importer, request, resolved) {
2468
+ const record = getSharedPackageReadRecord(this.key);
2469
+ if (record === null) return;
2470
+ const finding = packageInstanceFinding(record, relative(record.rootDir, importer).split(sep).join(posix.sep), request, resolved);
2471
+ if (finding === null) return;
2472
+ const key = `${finding.file}\0${finding.component}`;
2473
+ if (record.reported.has(key)) return;
2474
+ record.reported.add(key);
2475
+ surfaceManifestDiagnostics({ diagnostics: [finding] }, (message) => this.warn(message), {
2476
+ levels: this.options.diagnostics,
2477
+ strict: this.options.strict,
2478
+ reportErrors: this.reportErrors()
2479
+ });
2480
+ }
1602
2481
  writeAnalysisStatus(state, pending, diagnostic) {
1603
2482
  const status = {
1604
2483
  schema: 2,
@@ -1708,6 +2587,11 @@ var ExtractionSession = class {
1708
2587
  /** Maintain the session's epoch witness, rewritten ONLY when the value
1709
2588
  * changes, then expose it through the singleton. */
1710
2589
  publishReplacementEpoch(epoch) {
2590
+ if (!this.persistArtifacts) {
2591
+ this.lastEpochValue = epoch;
2592
+ setReplacementEpoch(this.key, epoch);
2593
+ return;
2594
+ }
1711
2595
  const onDisk = this.diskEpochValue();
1712
2596
  if (this.lastEpochValue === null) this.lastEpochValue = onDisk;
1713
2597
  if (epoch !== onDisk) this.writeSessionArtifact(REPLACEMENT_EPOCH_ARTIFACT, JSON.stringify({
@@ -1719,7 +2603,7 @@ var ExtractionSession = class {
1719
2603
  this.lastEpochValue = epoch;
1720
2604
  this.reconcileSiblingEpochs(epoch);
1721
2605
  }
1722
- setReplacementEpoch(epoch);
2606
+ setReplacementEpoch(this.key, epoch);
1723
2607
  }
1724
2608
  /** sessions-root listing memo, keyed by the root dir's mtime — a new or
1725
2609
  * pruned sibling DIRECTORY moves it; agreeing siblings stay listed. */
@@ -1764,10 +2648,14 @@ var ExtractionSession = class {
1764
2648
  }
1765
2649
  }
1766
2650
  /** The pid of `siblingDir`'s live owner, or null when nothing proves one.
1767
- * Our own pid is never one — publication is process-exclusive. A missing,
1768
- * undecodable or unreadable claim all mean no owner: the opposite default
1769
- * from the CLI lock check on purpose, so do not unify the two. */
2651
+ * A tree whose build is registered in this process is ours and live,
2652
+ * claim or no claim: a publisher's close() releases the claim while the
2653
+ * build's other participants still serve from it. Otherwise a missing,
2654
+ * undecodable or unreadable claim, or one naming our own pid, means no
2655
+ * owner: the opposite default from the CLI lock check on purpose, so do
2656
+ * not unify the two. */
1770
2657
  liveSiblingOwnerPid(siblingDir) {
2658
+ if (findSession(sessionKey(basename(siblingDir), this.rootDir)) !== void 0) return process.pid;
1771
2659
  let claim;
1772
2660
  try {
1773
2661
  claim = readCliLockRecord(siblingDir);
@@ -1798,7 +2686,7 @@ var ExtractionSession = class {
1798
2686
  /** Delete legacy flat artifacts and prune sibling session directories past
1799
2687
  * the retention window — never this session's own dir. */
1800
2688
  runSessionStartHygiene() {
1801
- if (this.sessionStartHygieneDone) return;
2689
+ if (this.sessionStartHygieneDone || !this.persistArtifacts) return;
1802
2690
  this.sessionStartHygieneDone = true;
1803
2691
  const animusDir = join(this.rootDir, ANIMUS_ARTIFACT_DIR);
1804
2692
  const isCliPublishedSet = this.cliWriterHoldsLock(animusDir) || verifyCommitRecord(animusDir).length === 0;
@@ -1839,13 +2727,29 @@ var ExtractionSession = class {
1839
2727
  /** Write one artifact write-then-rename, so a cross-process reader never
1840
2728
  * sees a torn file; the tmp name carries the pid to keep writers apart. */
1841
2729
  writeSessionArtifact(name, content) {
1842
- const dir = this.sessionDir;
1843
- mkdirSync(dir, { recursive: true });
1844
- const tmpPath = join(dir, `.${name}.${process.pid}.tmp`);
1845
- writeFileSync(tmpPath, content);
1846
- renameSync(tmpPath, join(dir, name));
2730
+ if (this.persistArtifacts) {
2731
+ const dir = this.sessionDir;
2732
+ mkdirSync(dir, { recursive: true });
2733
+ const tmpPath = join(dir, `.${name}.${process.pid}.tmp`);
2734
+ writeFileSync(tmpPath, content);
2735
+ renameSync(tmpPath, join(dir, name));
2736
+ }
1847
2737
  this.onArtifactWrite?.(name, content);
1848
2738
  }
2739
+ /** How an asset's bytes are fingerprinted, of a file or of bytes read:
2740
+ * the engine's fast fingerprint where it has both, the content hash
2741
+ * otherwise, so the two always agree. */
2742
+ assetFingerprints() {
2743
+ const { fingerprintFile, fingerprintBytes } = this.engine();
2744
+ if (fingerprintFile && fingerprintBytes) return {
2745
+ ofFile: (path) => fingerprintFile(path) ?? contentHash(readFileSync(path)),
2746
+ ofBytes: fingerprintBytes
2747
+ };
2748
+ return {
2749
+ ofFile: (path) => contentHash(readFileSync(path)),
2750
+ ofBytes: contentHash
2751
+ };
2752
+ }
1849
2753
  /** Copy each asset() specifier's bytes, from every sheet, into `assets/`
1850
2754
  * under a content-hashed name and substitute a url relative to
1851
2755
  * styles.css. */
@@ -1855,12 +2759,12 @@ var ExtractionSession = class {
1855
2759
  const expected = /* @__PURE__ */ new Set();
1856
2760
  this.assetDependencyPaths.clear();
1857
2761
  this.assetDependencyKeys.clear();
2762
+ if (this.hostSubstitutesAssets) return sheets;
1858
2763
  const urlBySpecifier = /* @__PURE__ */ new Map();
1859
2764
  for (const specifier of specifiers) {
1860
2765
  const cached = this.assetCopyCache.get(specifier);
1861
2766
  if (cached && existsSync(join(assetsDir, cached.fileName))) try {
1862
- const current = statSync(cached.sourcePath);
1863
- if (current.mtimeMs === cached.mtimeMs && current.size === cached.size) {
2767
+ if (this.assetFingerprints().ofFile(cached.sourcePath) === cached.fingerprint) {
1864
2768
  this.trackAssetDependency(cached.sourcePath);
1865
2769
  expected.add(cached.fileName);
1866
2770
  urlBySpecifier.set(specifier, cached.url);
@@ -1869,27 +2773,18 @@ var ExtractionSession = class {
1869
2773
  } catch {}
1870
2774
  const resolvedPath = this.resolveAssetSpecifier(specifier);
1871
2775
  if (!resolvedPath) {
1872
- const message = `unresolvable asset() specifier: ${specifier}`;
1873
- if (this.options.strict) this.strictFailure(`[${this.driverLabel}] ${message}`);
1874
- else this.warn(message);
2776
+ this.reportUnresolvableAsset(specifier);
1875
2777
  urlBySpecifier.set(specifier, specifier);
1876
2778
  continue;
1877
2779
  }
1878
- const bytes = readFileSync(resolvedPath);
1879
- const sourceStat = statSync(resolvedPath);
2780
+ const { fileName, bytes } = writeAssetCopy(resolvedPath, assetsDir);
1880
2781
  this.trackAssetDependency(resolvedPath);
1881
- const ext = extname(resolvedPath);
1882
- const fileName = `${basename(resolvedPath, ext)}.${contentHash(bytes).slice(0, 8)}${ext}`;
1883
2782
  expected.add(fileName);
1884
- if (!existsSync(assetsDir)) mkdirSync(assetsDir, { recursive: true });
1885
- const assetPath = join(assetsDir, fileName);
1886
- if (!existsSync(assetPath)) writeFileSync(assetPath, bytes);
1887
2783
  const url = `./${SESSION_ASSETS_DIR}/${fileName}`;
1888
2784
  urlBySpecifier.set(specifier, url);
1889
2785
  this.assetCopyCache.set(specifier, {
1890
2786
  sourcePath: resolvedPath,
1891
- mtimeMs: sourceStat.mtimeMs,
1892
- size: sourceStat.size,
2787
+ fingerprint: this.assetFingerprints().ofBytes(bytes),
1893
2788
  fileName,
1894
2789
  url
1895
2790
  });
@@ -1897,6 +2792,10 @@ var ExtractionSession = class {
1897
2792
  if (pruneSuperseded) pruneStaleAssets(assetsDir, expected);
1898
2793
  return substituteSheetAssets(sheets, urlBySpecifier);
1899
2794
  }
2795
+ reportUnresolvableAsset(specifier) {
2796
+ const message = `unresolvable asset() specifier: ${specifier}`;
2797
+ this.strictOrWarn(message, `[${this.driverLabel}] ${message}`);
2798
+ }
1900
2799
  trackAssetDependency(path) {
1901
2800
  this.assetDependencyPaths.add(path);
1902
2801
  for (const key of toWatchKeys(path)) this.assetDependencyKeys.add(key);
@@ -1918,23 +2817,6 @@ var ExtractionSession = class {
1918
2817
  return null;
1919
2818
  }
1920
2819
  };
1921
- /** Sync `assets/` to the current build's file set; per-entry failures are
1922
- * tolerated — a concurrent session may have removed the same file. */
1923
- function pruneStaleAssets(assetsDir, expected) {
1924
- if (!existsSync(assetsDir)) return;
1925
- let entries;
1926
- try {
1927
- entries = readdirSync(assetsDir);
1928
- } catch {
1929
- return;
1930
- }
1931
- for (const entry of entries) {
1932
- if (expected.has(entry)) continue;
1933
- try {
1934
- unlinkSync(join(assetsDir, entry));
1935
- } catch {}
1936
- }
1937
- }
1938
2820
  //#endregion
1939
2821
  //#region session/turbopack-orchestrator.ts
1940
2822
  async function runSessionPipeline(session) {
@@ -1942,6 +2824,7 @@ async function runSessionPipeline(session) {
1942
2824
  session.persistAnalysisInputs = true;
1943
2825
  await session.runFullPipeline();
1944
2826
  }
2827
+ /** Each build's watched root: another build of the root watches it too. */
1945
2828
  const activeWatcherRoots = /* @__PURE__ */ new Set();
1946
2829
  const IGNORED_SEGMENTS = /* @__PURE__ */ new Set([
1947
2830
  ANIMUS_ARTIFACT_DIR,
@@ -1968,17 +2851,18 @@ function isIntrinsicString(value) {
1968
2851
  }
1969
2852
  function startTurbopackWatcher(session, rootDir, options = {}) {
1970
2853
  const { debounceMs = 75, watchFn = watch, holdEvents = false } = options;
1971
- if (activeWatcherRoots.has(rootDir)) return { kind: "already-watched" };
1972
- activeWatcherRoots.add(rootDir);
2854
+ const watched = JSON.stringify([session.sessionId, rootDir]);
2855
+ if (activeWatcherRoots.has(watched)) return { kind: "already-watched" };
2856
+ activeWatcherRoots.add(watched);
1973
2857
  session.debounceCeilingMs = debounceMs;
1974
2858
  const pendingPaths = /* @__PURE__ */ new Set();
1975
2859
  const watchers = /* @__PURE__ */ new Map();
1976
- let timer = null;
1977
2860
  let updateChain = Promise.resolve();
1978
2861
  let closed = false;
1979
2862
  let died = false;
1980
2863
  let holdingEvents = holdEvents;
1981
2864
  const externalWatchers = /* @__PURE__ */ new Map();
2865
+ const dependencyWatchers = /* @__PURE__ */ new Map();
1982
2866
  let pendingOpened = /* @__PURE__ */ new Map();
1983
2867
  let capturedDuringSnapshot = [];
1984
2868
  const externalDiagnosticKey = (root) => `external-watch:${root}`;
@@ -1997,24 +2881,28 @@ function startTurbopackWatcher(session, rootDir, options = {}) {
1997
2881
  session.stickyDiagnostics?.set(externalDiagnosticKey(root), message);
1998
2882
  console.warn(`[animus-extract] ${message}`);
1999
2883
  };
2884
+ let cyclesInFlight = 0;
2000
2885
  const flush = () => {
2001
- timer = null;
2002
2886
  const modifiedFiles = /* @__PURE__ */ new Set();
2003
2887
  const removedFiles = /* @__PURE__ */ new Set();
2004
2888
  for (const path of pendingPaths) (existsSync(path) ? modifiedFiles : removedFiles).add(path);
2005
2889
  pendingPaths.clear();
2006
- updateChain = updateChain.then(() => {
2007
- if (closed) return;
2008
- return session.handleWatchUpdate({
2009
- modifiedFiles,
2010
- removedFiles
2011
- }).catch((err) => {
2012
- console.warn(`[animus-extract] watch update failed: ${String(err)}`);
2013
- }).then(() => {
2014
- if (pendingOpened.size > 0 || capturedDuringSnapshot.length > 0) rollbackPendingExternal();
2015
- });
2890
+ if (closed) return;
2891
+ cyclesInFlight++;
2892
+ const cycle = session.handleWatchUpdate({
2893
+ modifiedFiles,
2894
+ removedFiles
2895
+ }).catch((err) => {
2896
+ console.warn(`[animus-extract] watch update failed: ${String(err)}`);
2897
+ }).finally(() => {
2898
+ cyclesInFlight--;
2899
+ if (cyclesInFlight > 0) return;
2900
+ if (pendingOpened.size > 0 || capturedDuringSnapshot.length > 0) rollbackPendingExternal();
2901
+ syncDependencyWatchers();
2016
2902
  });
2903
+ updateChain = updateChain.then(() => cycle);
2017
2904
  };
2905
+ const coalescing = new CoalescingWindow(flush, debounceMs);
2018
2906
  const rollbackPendingExternal = () => {
2019
2907
  for (const watcher of pendingOpened.values()) watcher.close();
2020
2908
  pendingOpened = /* @__PURE__ */ new Map();
@@ -2023,16 +2911,17 @@ function startTurbopackWatcher(session, rootDir, options = {}) {
2023
2911
  const closeAll = () => {
2024
2912
  if (closed) return;
2025
2913
  closed = true;
2026
- if (timer) clearTimeout(timer);
2027
- timer = null;
2914
+ coalescing.cancel();
2028
2915
  for (const watcher of watchers.values()) watcher.close();
2029
2916
  watchers.clear();
2917
+ for (const { watcher } of dependencyWatchers.values()) watcher.close();
2918
+ dependencyWatchers.clear();
2030
2919
  for (const watcher of externalWatchers.values()) watcher.close();
2031
2920
  externalWatchers.clear();
2032
2921
  rollbackPendingExternal();
2033
2922
  session.onExternalRootResolved = null;
2034
2923
  session.onExternalRootsCommitted = null;
2035
- activeWatcherRoots.delete(rootDir);
2924
+ activeWatcherRoots.delete(watched);
2036
2925
  };
2037
2926
  const onWatcherError = (err) => {
2038
2927
  died = true;
@@ -2048,10 +2937,7 @@ function startTurbopackWatcher(session, rootDir, options = {}) {
2048
2937
  } catch (err) {
2049
2938
  console.warn(`[animus-extract] Turbopack debounce status update failed: ${String(err)}`);
2050
2939
  }
2051
- if (!timer) {
2052
- timer = setTimeout(flush, debounceMs);
2053
- timer.unref?.();
2054
- }
2940
+ coalescing.note();
2055
2941
  };
2056
2942
  const onEvent = (baseDir, filename) => {
2057
2943
  if (closed || !filename) return;
@@ -2095,6 +2981,59 @@ function startTurbopackWatcher(session, rootDir, options = {}) {
2095
2981
  degradeExternalRoot(root, err);
2096
2982
  }
2097
2983
  };
2984
+ /** Whether the root or an external root watcher already reports `path`. */
2985
+ const watchedByRoot = (path) => {
2986
+ if (isPathWithinRoot(rootDir, path) && !relative(rootDir, path).split(/[\\/]/).some((segment) => IGNORED_SEGMENTS.has(segment))) return true;
2987
+ return [...externalWatchers.keys()].some((root) => isPathWithinRoot(root, path));
2988
+ };
2989
+ /** Watches the system, asset and configuration dependencies no root
2990
+ * watcher reports, by their directories, for those files only. */
2991
+ const syncDependencyWatchers = () => {
2992
+ if (closed) return;
2993
+ const wanted = /* @__PURE__ */ new Map();
2994
+ for (const path of [
2995
+ ...session.systemDependencyPaths,
2996
+ ...session.assetDependencyPaths,
2997
+ ...session.configInputPaths
2998
+ ]) {
2999
+ if (watchedByRoot(path)) continue;
3000
+ const files = wanted.get(dirname(path)) ?? /* @__PURE__ */ new Set();
3001
+ files.add(path);
3002
+ wanted.set(dirname(path), files);
3003
+ }
3004
+ for (const [dir, { watcher }] of [...dependencyWatchers]) {
3005
+ if (wanted.has(dir)) continue;
3006
+ watcher.close();
3007
+ dependencyWatchers.delete(dir);
3008
+ }
3009
+ for (const [dir, files] of wanted) {
3010
+ const existing = dependencyWatchers.get(dir);
3011
+ if (existing) {
3012
+ existing.files = files;
3013
+ continue;
3014
+ }
3015
+ try {
3016
+ const watcher = watchFn(dir, { recursive: false }, (_event, name) => {
3017
+ if (closed || !name) return;
3018
+ const abs = join(dir, name.toString());
3019
+ if (dependencyWatchers.get(dir)?.files.has(abs)) enqueuePath(abs);
3020
+ });
3021
+ watcher.on("error", (err) => {
3022
+ watcher.close();
3023
+ dependencyWatchers.delete(dir);
3024
+ console.warn(`[animus-extract] dependency watch on ${dir} failed (${String(err)}); its changes require a dev-server restart`);
3025
+ });
3026
+ watcher.unref?.();
3027
+ dependencyWatchers.set(dir, {
3028
+ watcher,
3029
+ files
3030
+ });
3031
+ } catch (err) {
3032
+ if (errnoCode(err) === "ENOENT") continue;
3033
+ console.warn(`[animus-extract] dependency watch on ${dir} unavailable (${String(err)}); its changes require a dev-server restart`);
3034
+ }
3035
+ }
3036
+ };
2098
3037
  const addWatcher = (dir, recursive) => {
2099
3038
  const watcher = watchFn(dir, { recursive }, (_event, filename) => onEvent(dir, filename));
2100
3039
  watcher.on("error", onWatcherError);
@@ -2113,6 +3052,7 @@ function startTurbopackWatcher(session, rootDir, options = {}) {
2113
3052
  return { kind: "unavailable" };
2114
3053
  }
2115
3054
  for (const root of session.externalWatchRoots ?? []) openExternalWatcher(root, externalWatchers);
3055
+ syncDependencyWatchers();
2116
3056
  session.onExternalRootResolved = (root) => {
2117
3057
  if (closed) return;
2118
3058
  if (externalWatchers.has(root) || pendingOpened.has(root)) return;
@@ -2137,7 +3077,8 @@ function startTurbopackWatcher(session, rootDir, options = {}) {
2137
3077
  onDied: null,
2138
3078
  deliverHeldEvents: () => {
2139
3079
  holdingEvents = false;
2140
- if (pendingPaths.size > 0) flush();
3080
+ syncDependencyWatchers();
3081
+ if (pendingPaths.size > 0) coalescing.flush();
2141
3082
  return updateChain;
2142
3083
  },
2143
3084
  settle: () => updateChain
@@ -2148,4 +3089,4 @@ function startTurbopackWatcher(session, rootDir, options = {}) {
2148
3089
  };
2149
3090
  }
2150
3091
  //#endregion
2151
- export { ANIMUS_ARTIFACT_DIR, ANIMUS_CSS_MODULE_ID, CLI_COMMIT_ARTIFACT, CLI_LOCK_ARTIFACT, CLI_LOCK_HEARTBEAT_INTERVAL_MS, CLI_LOCK_STALE_AFTER_MS, ExtractionSession, MANIFEST_ARTIFACT, SESSION_ASSETS_DIR, STYLES_ARTIFACT, SYSTEM_PROPS_ARTIFACT, TURBOPACK_SYSTEM_PROPS_ID, analysisCommitPath, analysisInputsPath, analysisStatusPath, checkLockLiveness, collectSessionAssets, decodeCommitRecord, engineApi, getAnalysisStartedPromise, getAnalyzedHashes, getManifestJson, getReplacementEpoch, getSessionArtifactDir, getSharedCss, getSharedExternalDirs, getSharedExternalEntries, getSharedExternalSideEffects, getSharedSystemProps, holdDirectoryClaim, isLockHolderAlive, lockRecordBytes, manifestPath, pruneStaleAssets, readCliLockRecord, readJsonEnvelope, replacementEpochPath, resetAnalysisStartedPromise, runSessionPipeline, sessionArtifactDir, setAnalysisStartedPromise, setSharedEngine, startTurbopackWatcher, stylesPath, systemPropsPath, verifyCommitRecord };
3092
+ export { ANIMUS_ARTIFACT_DIR, ANIMUS_CSS_MODULE_ID, CLI_COMMIT_ARTIFACT, CLI_LOCK_ARTIFACT, CLI_LOCK_HEARTBEAT_INTERVAL_MS, CLI_LOCK_STALE_AFTER_MS, COALESCING_WINDOW_MS, CoalescingWindow, EnvironmentFailure, ExtractionSession, MANIFEST_ARTIFACT, SESSION_ASSETS_DIR, STYLES_ARTIFACT, SYSTEM_PROPS_ARTIFACT, SessionConfigurationConflict, TURBOPACK_SYSTEM_PROPS_ID, analysisCommitPath, analysisInputsPath, analysisStatusPath, checkLockLiveness, collectSessionAssets, createBuildId, decodeCommitRecord, engineApi, engineFactory, engineHandle, findSession, getAnalysisStartedPromise, getAnalyzedHashes, getManifestJson, getReplacementEpoch, getSessionArtifactDir, getSharedCss, getSharedExternalDirs, getSharedExternalEntries, getSharedExternalSideEffects, getSharedSystemProps, holdDirectoryClaim, isLockHolderAlive, joinSession, lockRecordBytes, manifestPath, pruneStaleAssets, readCliLockRecord, readJsonEnvelope, releaseEngineHandle, replacementEpochPath, requireEngine, resetAnalysisStartedPromise, runSessionPipeline, sessionArtifactDir, sessionKey, setAnalysisStartedPromise, startTurbopackWatcher, stylesPath, systemPropsPath, verifyCommitRecord };