@animus-ui/extract 0.1.25 → 0.1.27

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 (87) 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 +36 -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 +61 -0
  43. package/dist/pipeline/probe-evidence.d.ts.map +1 -0
  44. package/dist/pipeline/registry-packages.d.ts +27 -0
  45. package/dist/pipeline/registry-packages.d.ts.map +1 -0
  46. package/dist/pipeline/run-analysis.d.ts +26 -0
  47. package/dist/pipeline/run-analysis.d.ts.map +1 -1
  48. package/dist/pipeline/runtime-import.d.ts +10 -0
  49. package/dist/pipeline/runtime-import.d.ts.map +1 -0
  50. package/dist/pipeline/served-files.d.ts +16 -0
  51. package/dist/pipeline/served-files.d.ts.map +1 -0
  52. package/dist/pipeline/source-corpus.d.ts +4 -0
  53. package/dist/pipeline/source-corpus.d.ts.map +1 -1
  54. package/dist/pipeline/source-ingestion.d.ts +37 -5
  55. package/dist/pipeline/source-ingestion.d.ts.map +1 -1
  56. package/dist/pipeline/system-config.d.ts +18 -0
  57. package/dist/pipeline/system-config.d.ts.map +1 -1
  58. package/dist/pipeline/system-instances.d.ts +27 -0
  59. package/dist/pipeline/system-instances.d.ts.map +1 -0
  60. package/dist/pipeline/tsconfig-paths.d.ts +4 -0
  61. package/dist/pipeline/tsconfig-paths.d.ts.map +1 -1
  62. package/dist/pipeline/vite-expansions.d.ts +40 -0
  63. package/dist/pipeline/vite-expansions.d.ts.map +1 -0
  64. package/dist/pipeline/webpack-expansions.d.ts +55 -0
  65. package/dist/pipeline/webpack-expansions.d.ts.map +1 -0
  66. package/dist/pipeline/workspace-packages.d.ts +13 -0
  67. package/dist/pipeline/workspace-packages.d.ts.map +1 -0
  68. package/dist/{pipeline-YGWpNr_Z.mjs → pipeline-C0AcBXuQ.mjs} +4244 -2595
  69. package/dist/{pipeline-DOgKR4VZ.cjs → pipeline-CPSPw522.cjs} +4562 -2637
  70. package/dist/session/coalescing-window.d.ts +34 -0
  71. package/dist/session/coalescing-window.d.ts.map +1 -0
  72. package/dist/session/environment-builds.d.ts +26 -0
  73. package/dist/session/environment-builds.d.ts.map +1 -0
  74. package/dist/session/extraction-session.d.ts +338 -38
  75. package/dist/session/extraction-session.d.ts.map +1 -1
  76. package/dist/session/index.d.ts +9 -3
  77. package/dist/session/index.d.ts.map +1 -1
  78. package/dist/session/published-set.d.ts +6 -1
  79. package/dist/session/published-set.d.ts.map +1 -1
  80. package/dist/session/resolve-packages.d.ts +9 -5
  81. package/dist/session/resolve-packages.d.ts.map +1 -1
  82. package/dist/session/singleton.d.ts +216 -54
  83. package/dist/session/singleton.d.ts.map +1 -1
  84. package/dist/session/turbopack-orchestrator.d.ts.map +1 -1
  85. package/dist/session.cjs +1539 -525
  86. package/dist/session.mjs +1505 -504
  87. package/package.json +4 -4
package/dist/session.cjs CHANGED
@@ -1,8 +1,151 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_pipeline = require("./pipeline-DOgKR4VZ.cjs");
2
+ const require_pipeline = require("./pipeline-CPSPw522.cjs");
3
3
  let crypto = require("crypto");
4
4
  let fs = require("fs");
5
5
  let path = require("path");
6
+ //#region session/coalescing-window.ts
7
+ /**
8
+ * The one coalescing window (SA19). It opens on the first pending event and
9
+ * fires a fixed interval later: a later event joins the open window and never
10
+ * pushes it back, so a steady stream of edits cannot starve the work it
11
+ * coalesces. Builds, startup and explicit flushes bypass it through `flush`.
12
+ * Hosts add no timers of their own.
13
+ */
14
+ const COALESCING_WINDOW_MS = 75;
15
+ const SYSTEM_CLOCK = {
16
+ schedule: (fn, ms) => {
17
+ const timer = setTimeout(fn, ms);
18
+ timer.unref?.();
19
+ return timer;
20
+ },
21
+ cancel: (timer) => clearTimeout(timer)
22
+ };
23
+ var CoalescingWindow = class {
24
+ fire;
25
+ windowMs;
26
+ clock;
27
+ timer = null;
28
+ constructor(fire, windowMs = 75, clock = SYSTEM_CLOCK) {
29
+ this.fire = fire;
30
+ this.windowMs = windowMs;
31
+ this.clock = clock;
32
+ }
33
+ /** A pending event: opens the window, or joins the open one. */
34
+ note() {
35
+ if (this.timer !== null) return;
36
+ this.timer = this.clock.schedule(() => {
37
+ this.timer = null;
38
+ return this.fire();
39
+ }, this.windowMs);
40
+ }
41
+ /** Fires now, closing any open window: the bypass. */
42
+ flush() {
43
+ this.cancel();
44
+ return this.fire();
45
+ }
46
+ /** Closes an open window without firing it. */
47
+ cancel() {
48
+ if (this.timer === null) return;
49
+ this.clock.cancel(this.timer);
50
+ this.timer = null;
51
+ }
52
+ };
53
+ //#endregion
54
+ //#region session/environment-builds.ts
55
+ /**
56
+ * Each environment build that started and has not ended, by name. A host
57
+ * that builds a shared plugin's environments in parallel (a Vite `buildApp`)
58
+ * shares one build among them, retired by the last to end. A build under a
59
+ * name whose build is open (a reused builder, or a second environment under
60
+ * one name) waits for that build's end, so each build ends only its own
61
+ * generation, once.
62
+ */
63
+ var EnvironmentBuilds = class {
64
+ /** What a host without environments builds as. */
65
+ single = { name: "build" };
66
+ builds = /* @__PURE__ */ new Map();
67
+ /** Opens `environment`'s build once no build under its name is open;
68
+ * whether another environment's build is open beside it. */
69
+ async open(environment) {
70
+ for (;;) {
71
+ const open = this.builds.get(environment.name);
72
+ if (open === void 0) break;
73
+ await open.ended;
74
+ }
75
+ let end = () => {};
76
+ const ended = new Promise((settle) => {
77
+ end = settle;
78
+ });
79
+ this.builds.set(environment.name, {
80
+ environment,
81
+ ended,
82
+ end
83
+ });
84
+ return this.builds.size > 1;
85
+ }
86
+ /** Ends `environment`'s build if this environment opened it; whether that
87
+ * left none open. A host can end an environment it never opened (a dev
88
+ * server ends every environment, started or not), which ends nothing. */
89
+ end(environment) {
90
+ const open = this.builds.get(environment.name);
91
+ if (open === void 0 || open.environment !== environment) return false;
92
+ this.builds.delete(environment.name);
93
+ open.end();
94
+ return this.builds.size === 0;
95
+ }
96
+ };
97
+ //#endregion
98
+ //#region pipeline/system-instances.ts
99
+ /**
100
+ * The system and the analysis read one instance of each kit (spec §2.2
101
+ * A4). Discovery asks the host which instance a kit specifier names; the
102
+ * system loader resolves the system file's imports itself. Until the loader
103
+ * takes the host's instance, a kit whose analysed instance differs from the
104
+ * one the system evaluated is refused: its components would be extracted
105
+ * against another instance's vocabulary.
106
+ */
107
+ function realPath(path$39) {
108
+ try {
109
+ return (0, fs.realpathSync)(path$39);
110
+ } catch {
111
+ return path$39;
112
+ }
113
+ }
114
+ /**
115
+ * One error for each system-file kit specifier whose analysed package
116
+ * instance is not the one the system load evaluated: the file the loader's
117
+ * own resolution names must be among the files the load read, and lie in
118
+ * another package root than the instance discovery read for its package.
119
+ */
120
+ function systemInstanceMismatches(input) {
121
+ const evaluated = new Set(input.evaluated.map(realPath));
122
+ const shown = (path$40) => (0, path.relative)(input.rootDir, path$40).split(path.sep).join("/");
123
+ const diagnostics = [];
124
+ for (const specifier of input.specifiers) {
125
+ if (!require_pipeline.isPackageSpecifier(specifier)) continue;
126
+ const analysed = input.instances[require_pipeline.bareSpecifierPackageName(specifier)];
127
+ if (analysed === void 0) continue;
128
+ let file;
129
+ try {
130
+ file = input.loaderResolve(specifier);
131
+ } catch {
132
+ continue;
133
+ }
134
+ if (file === null || !evaluated.has(realPath(file))) continue;
135
+ const system = realPath(require_pipeline.findPackageRoot(realPath(file)));
136
+ if (system === analysed) continue;
137
+ diagnostics.push({
138
+ file: specifier,
139
+ component: "kit",
140
+ kind: "error",
141
+ 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`,
142
+ code: require_pipeline.SYSTEM_INSTANCE_MISMATCH,
143
+ severity: require_pipeline.severityFor(require_pipeline.SYSTEM_INSTANCE_MISMATCH)
144
+ });
145
+ }
146
+ return diagnostics;
147
+ }
148
+ //#endregion
6
149
  //#region session/session-paths.ts
7
150
  /**
8
151
  * Session artifact names, path derivations, and artifact shapes. Imports no
@@ -169,9 +312,11 @@ function readCliLockRecord(dir) {
169
312
  if (!isPublishedSetRecord(candidate) || !isPublishedSetNumber(candidate.pid)) return { kind: "indeterminate" };
170
313
  const startedAt = candidate.startedAt;
171
314
  const heartbeatAt = candidate.heartbeatAt;
315
+ const build = candidate.build;
172
316
  const record = { pid: candidate.pid };
173
317
  if (isPublishedSetString(startedAt)) record.startedAt = startedAt;
174
318
  if (isPublishedSetString(heartbeatAt)) record.heartbeatAt = heartbeatAt;
319
+ if (isPublishedSetString(build)) record.build = build;
175
320
  return {
176
321
  kind: "held",
177
322
  record
@@ -202,17 +347,19 @@ function checkLockLiveness(record) {
202
347
  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`
203
348
  };
204
349
  }
205
- function lockRecordBytes(startedAt) {
206
- return JSON.stringify({
350
+ function lockRecordBytes(startedAt, build) {
351
+ const record = {
207
352
  pid: process.pid,
208
353
  startedAt,
209
354
  heartbeatAt: (/* @__PURE__ */ new Date()).toISOString()
210
- });
355
+ };
356
+ if (build !== void 0) record.build = build;
357
+ return JSON.stringify(record);
211
358
  }
212
359
  const HEARTBEAT_TEMP_PREFIX = ".lock-heartbeat-";
213
360
  function holdDirectoryClaim(dir, options = {}) {
214
361
  const startedAt = options.startedAt ?? (/* @__PURE__ */ new Date()).toISOString();
215
- const path$9 = (0, path.join)(dir, CLI_LOCK_ARTIFACT);
362
+ const path$38 = (0, path.join)(dir, CLI_LOCK_ARTIFACT);
216
363
  const tempPath = (0, path.join)(dir, `${HEARTBEAT_TEMP_PREFIX}${process.pid}`);
217
364
  const rename = options.write === "rename";
218
365
  let refresh = null;
@@ -225,13 +372,13 @@ function holdDirectoryClaim(dir, options = {}) {
225
372
  const stamp = () => {
226
373
  try {
227
374
  if (rename) {
228
- (0, fs.writeFileSync)(tempPath, lockRecordBytes(startedAt));
229
- (0, fs.renameSync)(tempPath, path$9);
230
- } else (0, fs.writeFileSync)(path$9, lockRecordBytes(startedAt));
375
+ (0, fs.writeFileSync)(tempPath, lockRecordBytes(startedAt, options.build));
376
+ (0, fs.renameSync)(tempPath, path$38);
377
+ } else (0, fs.writeFileSync)(path$38, lockRecordBytes(startedAt, options.build));
231
378
  } catch (error) {
232
379
  stop();
233
380
  if (isMissingFile(error)) return;
234
- console.error(`[animus] Could not write the directory claim ${path$9}: ${String(error)}`);
381
+ console.error(`[animus] Could not write the directory claim ${path$38}: ${String(error)}`);
235
382
  }
236
383
  };
237
384
  stamp();
@@ -242,7 +389,7 @@ function holdDirectoryClaim(dir, options = {}) {
242
389
  return () => {
243
390
  stop();
244
391
  if (rename) (0, fs.rmSync)(tempPath, { force: true });
245
- (0, fs.rmSync)(path$9, { force: true });
392
+ (0, fs.rmSync)(path$38, { force: true });
246
393
  };
247
394
  }
248
395
  /** Snapshot the session's copied asset() files, sorted, BEFORE the session
@@ -257,320 +404,395 @@ function collectSessionAssets(sessionDir) {
257
404
  }));
258
405
  }
259
406
  //#endregion
260
- //#region session/resolve-packages.ts
261
- /** An exports target under the `import` condition, then `default`, as the
262
- * system loader reads it; an array offers its targets in order. */
263
- function importTarget(value) {
264
- if (require_pipeline.isJsonString(value)) return value;
265
- if (Array.isArray(value)) {
266
- for (const entry of value) {
267
- const target = importTarget(entry);
268
- if (target !== null) return target;
269
- }
270
- return null;
271
- }
272
- if (!require_pipeline.isJsonBlock(value)) return null;
273
- return importTarget(value.import) ?? importTarget(value.default);
274
- }
275
- /** The exports entry for `subpath`: an exact key answers alone; otherwise
276
- * the pattern with the longest prefix, then the longest suffix, wins. */
277
- function exportsTarget(exports, subpath) {
278
- const map = require_pipeline.isJsonBlock(exports) && Object.keys(exports).some((k) => k.startsWith(".")) ? exports : { ".": exports };
279
- if (subpath in map) return importTarget(map[subpath]);
280
- let best = null;
281
- for (const [pattern, value] of Object.entries(map)) {
282
- const star = pattern.indexOf("*");
283
- if (star === -1 || pattern.indexOf("*", star + 1) !== -1) continue;
284
- const prefix = pattern.slice(0, star);
285
- const suffix = pattern.slice(star + 1);
286
- if (!subpath.startsWith(prefix) || !subpath.endsWith(suffix) || subpath.length < prefix.length + suffix.length) continue;
287
- if (!best || prefix.length > best.prefix.length || prefix.length === best.prefix.length && suffix.length > best.suffix.length) best = {
288
- prefix,
289
- suffix,
290
- value
291
- };
292
- }
293
- if (!best) return null;
294
- const matched = subpath.slice(best.prefix.length, subpath.length - best.suffix.length);
295
- return importTarget(best.value)?.replaceAll("*", matched) ?? null;
296
- }
297
- /** The entry an ES import of `name` resolves to through the package's
298
- * `exports`, under the `import` condition, then `default`: what the Vite
299
- * host and the system loader resolve. Null without an `exports` field, or
300
- * when no target exists. */
301
- function resolveImportEntry(rootDir, name) {
302
- const packageName = name.split("/").slice(0, name.startsWith("@") ? 2 : 1).join("/");
303
- const subpath = `.${name.slice(packageName.length)}`;
304
- for (let dir = rootDir;; dir = (0, path.dirname)(dir)) {
305
- const manifest = (0, path.join)(dir, "node_modules", packageName, "package.json");
306
- if ((0, fs.existsSync)(manifest)) {
307
- let exports;
308
- try {
309
- const parsed = require_pipeline.parseInternalWire((0, fs.readFileSync)(manifest, "utf-8"), `${manifest} (a package manifest)`);
310
- exports = require_pipeline.isJsonBlock(parsed) ? parsed.exports : void 0;
311
- } catch {
312
- return null;
313
- }
314
- if (exports === void 0 || exports === null) return null;
315
- const target = exportsTarget(exports, subpath);
316
- if (target === null) return null;
317
- const entryPath = (0, path.resolve)((0, path.dirname)(manifest), target);
318
- return (0, fs.existsSync)(entryPath) ? (0, fs.realpathSync)(entryPath) : null;
319
- }
320
- if (dir === (0, path.dirname)(dir)) return null;
321
- }
322
- }
323
- function resolvePackagesByName(rootDir, names) {
324
- if (names.length === 0) return {};
325
- const nameSet = new Set(names);
326
- const resolved = /* @__PURE__ */ new Set();
327
- const packageMap = {};
328
- try {
329
- const rootPkg = JSON.parse((0, fs.readFileSync)((0, path.join)(rootDir, "package.json"), "utf-8"));
330
- const workspaces = Array.isArray(rootPkg.workspaces) ? rootPkg.workspaces : rootPkg.workspaces?.packages ?? [];
331
- for (const ws of workspaces) {
332
- const wsDir = (0, path.resolve)(rootDir, ws);
333
- if (!(0, fs.existsSync)(wsDir)) continue;
334
- try {
335
- const pkg = JSON.parse((0, fs.readFileSync)((0, path.join)(wsDir, "package.json"), "utf-8"));
336
- const name = pkg.name || "";
337
- if (nameSet.has(name)) {
338
- const main = pkg.main || pkg.module || "index.ts";
339
- const entryPath = (0, path.resolve)(wsDir, main);
340
- if ((0, fs.existsSync)(entryPath)) {
341
- packageMap[name] = (0, path.relative)(rootDir, entryPath);
342
- resolved.add(name);
343
- }
344
- }
345
- } catch {}
346
- }
347
- } catch {}
348
- for (const name of nameSet) {
349
- if (resolved.has(name)) continue;
350
- const importEntry = resolveImportEntry(rootDir, name);
351
- if (importEntry !== null) {
352
- packageMap[name] = (0, path.relative)(rootDir, importEntry);
353
- continue;
354
- }
355
- try {
356
- const entryPath = require.resolve(name, { paths: [rootDir] });
357
- packageMap[name] = (0, path.relative)(rootDir, entryPath);
358
- } catch {}
359
- }
360
- return packageMap;
361
- }
362
- //#endregion
363
407
  //#region session/singleton.ts
364
408
  /**
365
- * globalThis-backed so the ESM plugin copy and the CJS webpack-loader copy
366
- * share one store; module scope alone gives each copy its own instance.
409
+ * A build's shared state lives in one registry on `globalThis`, keyed by its
410
+ * `SessionKey`, so the ESM plugin copy and the CJS webpack-loader copy of
411
+ * this module find the same entry, and two builds of one root never share
412
+ * one. A reader of a build no participant has joined gets the empty value;
413
+ * a writer must have joined it.
367
414
  */
368
- const MANIFEST_KEY = "__animus_manifest_json__";
369
- const ANALYSIS_STARTED_KEY = "__animus_analysis_started__";
370
- const SHARED_CSS_KEY = "__animus_shared_css__";
371
- const SHARED_SYSTEM_PROPS_KEY = "__animus_shared_system_props__";
372
- const SHARED_EXTERNAL_DIRS_KEY = "__animus_external_pkg_dirs__";
373
- const SHARED_EXTERNAL_ENTRIES_KEY = "__animus_external_source_entries__";
374
- const SHARED_EXTERNAL_SIDE_EFFECTS_KEY = "__animus_external_side_effects__";
375
- const ANALYZED_HASHES_KEY = "__animus_analyzed_hashes__";
376
- const REPLACEMENT_EPOCH_KEY = "__animus_replacement_epoch__";
377
- const WATCH_TRANSACTION_KEY = "__animus_watch_transaction__";
378
- const PROCESS_SESSION_ID_KEY = "__animus_process_session_id__";
379
- const SESSION_ARTIFACT_DIR_KEY = "__animus_session_artifact_dir__";
380
- const OWNING_WATCH_SESSION_KEY = "__animus_owning_watch_session__";
381
- const EXCLUSIVE_SESSION_OWNER_KEY = "__animus_exclusive_session_owner__";
382
- const ENGINE_KEY = "__animus_engine__";
383
- const V2_ENGINE_KEY = "__animus_v2_engine__";
384
- const V2_SENT_SOURCES_KEY = "__animus_v2_sent_sources__";
385
- const V2_DRIFT_WARNED_KEY = "__animus_v2_drift_warned__";
386
- const ENGINE_API_OVERRIDE_KEY = "__animus_engine_api_override__";
387
- const singletonStore = globalThis;
388
- const isSingletonString = (value) => {
389
- if (Object(value) === value) return false;
390
- try {
391
- String.prototype.valueOf.call(value);
392
- return true;
393
- } catch {
394
- return false;
395
- }
396
- };
397
- function getManifestJson() {
398
- return singletonStore[MANIFEST_KEY];
415
+ function joined(key) {
416
+ const state = findSession(key);
417
+ if (state === void 0) throw new Error(`[animus] no participant has joined build ${key.build} of ${key.root}`);
418
+ return state;
419
+ }
420
+ /** Commits the build's publication, the one write of everything below. */
421
+ function setPublication(key, publication) {
422
+ joined(key).publication = publication;
399
423
  }
400
- function setManifestJson(json) {
401
- singletonStore[MANIFEST_KEY] = json;
424
+ function publicationOf(key) {
425
+ return findSession(key)?.publication ?? null;
426
+ }
427
+ function getManifestJson(key) {
428
+ return publicationOf(key)?.manifestJson ?? null;
402
429
  }
403
430
  /**
404
431
  * A latch over the most recently STARTED analysis, never cleared on settle:
405
432
  * non-null means one was scheduled. In-flight lives in getWatchTransaction.
406
433
  */
407
- function getAnalysisStartedPromise() {
408
- return singletonStore[ANALYSIS_STARTED_KEY];
409
- }
410
- function setAnalysisStartedPromise(promise) {
411
- singletonStore[ANALYSIS_STARTED_KEY] = promise;
434
+ function getAnalysisStartedPromise(key) {
435
+ return findSession(key)?.analysisStarted ?? null;
412
436
  }
413
- function resetAnalysisStartedPromise() {
414
- singletonStore[ANALYSIS_STARTED_KEY] = null;
437
+ function setAnalysisStartedPromise(key, promise) {
438
+ joined(key).analysisStarted = promise;
415
439
  }
416
- function getSharedCss() {
417
- return singletonStore[SHARED_CSS_KEY] || "";
440
+ function resetAnalysisStartedPromise(key) {
441
+ const state = findSession(key);
442
+ if (state !== void 0) state.analysisStarted = null;
418
443
  }
419
- function setSharedCss(css) {
420
- singletonStore[SHARED_CSS_KEY] = css;
444
+ /** True for the build's first caller only: the one that reports its
445
+ * unmatched user excludes. */
446
+ function claimUnmatchedExcludeReport(key) {
447
+ const state = joined(key);
448
+ if (state.unmatchedExcludesReported) return false;
449
+ state.unmatchedExcludesReported = true;
450
+ return true;
421
451
  }
422
- function getSharedSystemProps() {
423
- return singletonStore[SHARED_SYSTEM_PROPS_KEY] || "";
452
+ function getSharedCss(key) {
453
+ return publicationOf(key)?.styles ?? "";
424
454
  }
425
- function setSharedSystemProps(content) {
426
- singletonStore[SHARED_SYSTEM_PROPS_KEY] = content;
455
+ function getSharedSystemProps(key) {
456
+ return publicationOf(key)?.systemPropsModule ?? "";
427
457
  }
428
- function getSharedExternalDirs() {
429
- return singletonStore[SHARED_EXTERNAL_DIRS_KEY] || [];
458
+ function getSharedExternalDirs(key) {
459
+ return publicationOf(key)?.externalPackageDirs ?? [];
430
460
  }
431
- function setSharedExternalDirs(dirs) {
432
- singletonStore[SHARED_EXTERNAL_DIRS_KEY] = dirs;
433
- }
434
- function getSharedExternalEntries() {
435
- return singletonStore[SHARED_EXTERNAL_ENTRIES_KEY] || /* @__PURE__ */ new Map();
436
- }
437
- function setSharedExternalEntries(entries) {
438
- singletonStore[SHARED_EXTERNAL_ENTRIES_KEY] = entries;
461
+ function getSharedExternalEntries(key) {
462
+ return publicationOf(key)?.externalSourceEntries ?? /* @__PURE__ */ new Map();
439
463
  }
440
464
  /** Specifier → its source entry's side effects, published with the shared
441
465
  * entries; absent leaves the bundler's own classification. */
442
- function getSharedExternalSideEffects() {
443
- return singletonStore[SHARED_EXTERNAL_SIDE_EFFECTS_KEY] || /* @__PURE__ */ new Map();
444
- }
445
- function setSharedExternalSideEffects(sideEffects) {
446
- singletonStore[SHARED_EXTERNAL_SIDE_EFFECTS_KEY] = sideEffects;
466
+ function getSharedExternalSideEffects(key) {
467
+ return publicationOf(key)?.externalSourceSideEffects ?? /* @__PURE__ */ new Map();
447
468
  }
448
- function globalSlot(key) {
449
- return {
450
- get: () => singletonStore[key] ?? null,
451
- set: (value) => {
452
- singletonStore[key] = value;
453
- }
454
- };
455
- }
456
- const analyzedHashesSlot = globalSlot(ANALYZED_HASHES_KEY);
457
- const replacementEpochSlot = globalSlot(REPLACEMENT_EPOCH_KEY);
458
- const watchTransactionSlot = globalSlot(WATCH_TRANSACTION_KEY);
459
- const sessionArtifactDirSlot = globalSlot(SESSION_ARTIFACT_DIR_KEY);
460
- const owningWatchSessionSlot = globalSlot(OWNING_WATCH_SESSION_KEY);
461
469
  /**
462
- * The only session that can run watch analysis. A non-owning session forwards
463
- * its batch here; dropping it loses files only that watcher sees.
470
+ * The only session of a build that can run watch analysis. A non-owning
471
+ * session's observations queue for it; dropping them loses files only that
472
+ * watcher sees.
464
473
  */
465
- function getOwningWatchSession() {
466
- return owningWatchSessionSlot.get();
474
+ function getOwningWatchSession(key) {
475
+ return findSession(key)?.authority ?? null;
467
476
  }
468
- function setOwningWatchSession(session) {
469
- owningWatchSessionSlot.set(session);
477
+ function setOwningWatchSession(key, session) {
478
+ joined(key).authority = session;
470
479
  }
471
480
  /**
472
481
  * Per-file content hashes of the last published analysis (relPath → hash of
473
482
  * the exact bytes analyzed), including files that produced zero entries.
474
483
  */
475
- function getAnalyzedHashes() {
476
- return analyzedHashesSlot.get();
484
+ function getAnalyzedHashes(key) {
485
+ return publicationOf(key)?.analyzedHashes ?? null;
477
486
  }
478
- function setAnalyzedHashes(hashes) {
479
- analyzedHashesSlot.set(hashes);
480
- }
481
- /**
482
- * Session identity is per process: the first caller claims a fresh id and
483
- * every later one adopts it, so all compilers share one artifact tree.
484
- */
485
- function claimProcessSessionId() {
486
- const existing = singletonStore[PROCESS_SESSION_ID_KEY];
487
- if (isSingletonString(existing) && existing.length > 0) return existing;
488
- const fresh = (0, crypto.randomUUID)();
489
- singletonStore[PROCESS_SESSION_ID_KEY] = fresh;
490
- return fresh;
487
+ /** What the build's last published analysis read of the discovered
488
+ * packages (spec §2.2 A4); null before one publishes. */
489
+ function getSharedPackageReadRecord(key) {
490
+ return publicationOf(key)?.packageReadRecord ?? null;
491
491
  }
492
492
  /**
493
- * Two CONCURRENT drive loops in one process would share one session tree and
494
- * clobber each other; sequential claim/release cycles are legal.
493
+ * One participant publishes a build's analysis, holding this claim until it
494
+ * closes: a second publisher would clobber its artifacts. Two builds never
495
+ * exclude each other; sequential claim and release cycles are legal.
495
496
  */
496
- function claimExclusiveSessionOwner(label) {
497
- const active = singletonStore[EXCLUSIVE_SESSION_OWNER_KEY];
498
- 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.`);
499
- singletonStore[EXCLUSIVE_SESSION_OWNER_KEY] = label;
497
+ function claimExclusiveSessionOwner(key, label) {
498
+ const state = joined(key);
499
+ const active = state.activeAttempt;
500
+ 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.`);
501
+ state.activeAttempt = label;
500
502
  return () => {
501
- if (singletonStore[EXCLUSIVE_SESSION_OWNER_KEY] === label) delete singletonStore[EXCLUSIVE_SESSION_OWNER_KEY];
503
+ if (state.activeAttempt === label) state.activeAttempt = null;
502
504
  };
503
505
  }
504
506
  /**
505
- * Absolute artifact directory of the OWNING session's last publication. The
507
+ * Absolute artifact directory of the build's last publication. The
506
508
  * Turbopack loader runs out of process and receives it via options instead.
507
509
  */
508
- function getSessionArtifactDir() {
509
- return sessionArtifactDirSlot.get();
510
+ function getSessionArtifactDir(key) {
511
+ return findSession(key)?.artifactDir ?? null;
510
512
  }
511
- function setSessionArtifactDir(dir) {
512
- sessionArtifactDirSlot.set(dir);
513
+ function setSessionArtifactDir(key, dir) {
514
+ joined(key).artifactDir = dir;
513
515
  }
514
- /**
515
- * Replacement epoch of the last published analysis, written AFTER the
516
- * manifest so an epoch reader always sees at least that generation's manifest.
517
- */
518
- function getReplacementEpoch() {
519
- return replacementEpochSlot.get();
520
- }
521
- function setReplacementEpoch(epoch) {
522
- replacementEpochSlot.set(epoch);
516
+ /** Replacement epoch of the last published analysis: committed in the same
517
+ * record as its manifest, so an epoch reader sees that generation's. */
518
+ function getReplacementEpoch(key) {
519
+ return publicationOf(key)?.replacementEpoch ?? null;
523
520
  }
524
521
  /**
525
- * The one in-flight watch-analysis transaction: a compiler that did not start
526
- * it joins this promise rather than read a pre-transaction generation.
522
+ * The build's one in-flight watch-analysis transaction: a participant that
523
+ * did not start it joins this promise rather than read a pre-transaction
524
+ * generation.
527
525
  */
528
- function getWatchTransaction() {
529
- return watchTransactionSlot.get();
526
+ function getWatchTransaction(key) {
527
+ return findSession(key)?.watchTransaction ?? null;
528
+ }
529
+ function observationQueue(key) {
530
+ return joined(key).observations;
530
531
  }
531
- function setWatchTransaction(transaction) {
532
- watchTransactionSlot.set(transaction);
532
+ function getSystemAuthority(key) {
533
+ return findSession(key)?.systemAuthority ?? null;
533
534
  }
534
- /** Engine selection travels through the singleton so non-owning compilers
535
- * and the webpack loader honor the owner's choice. */
536
- function setSharedEngine(engine) {
537
- singletonStore[ENGINE_KEY] = engine;
535
+ function setSystemAuthority(key, authority) {
536
+ joined(key).systemAuthority = authority;
538
537
  }
539
- function getSharedEngine() {
540
- return singletonStore[ENGINE_KEY] || "v2";
538
+ function setWatchTransaction(key, transaction) {
539
+ joined(key).watchTransaction = transaction;
541
540
  }
542
541
  /** Single choke-point for every native extraction call; the NAPI module's
543
542
  * own `.d.ts` is the authoritative surface for what it returns. */
544
543
  function requireEngine() {
545
544
  return require("@animus-ui/extract");
546
545
  }
546
+ const ENGINE_API_OVERRIDE_KEY = "__animus_engine_api_override__";
547
+ const overrideGlobal = globalThis;
548
+ /** The native engine module could not be loaded: an environment failure,
549
+ * never answered by another engine. */
550
+ var EnvironmentFailure = class extends Error {
551
+ code = "EnvironmentFailure";
552
+ constructor(cause) {
553
+ super(`[animus] EnvironmentFailure: the native extraction engine could not be loaded: ${String(cause)}`, { cause });
554
+ this.name = "EnvironmentFailure";
555
+ }
556
+ };
557
+ let loadEngineFactory = requireEngine;
558
+ /** The native factory, loaded once for every build in the process. A load
559
+ * failure is an EnvironmentFailure, and is not cached: the next use tries
560
+ * the load again. */
561
+ function engineFactory() {
562
+ const registry = sessionRegistry();
563
+ if (registry.factory === null) try {
564
+ registry.factory = loadEngineFactory();
565
+ } catch (error) {
566
+ throw new EnvironmentFailure(error instanceof Error ? error : String(error));
567
+ }
568
+ return registry.factory;
569
+ }
547
570
  /**
548
- * The one engine API for this process: the instance, its analyze-time
549
- * sources, and the drift flag live on globalThis so both copies share them.
571
+ * One engine-handle store (SA18): the engine API over `key`'s own engine
572
+ * state, made once per key in the registry every module copy joins. The
573
+ * native factory is shared; each key's engine instance, analyze-time
574
+ * sources and drift flag are its own, and its build's retirement releases
575
+ * them. A key need not be a joined build's: a Vite context or a Turbopack
576
+ * worker keys its own.
550
577
  */
551
- const v2EngineApi = require_pipeline.createV2EngineApi({
552
- label: "animus",
553
- isV2: () => getSharedEngine() === "v2",
554
- loadNativeEngine: requireEngine,
555
- passThroughUnknownPaths: true,
556
- store: {
557
- getEngine: () => singletonStore[V2_ENGINE_KEY],
558
- setEngine: (engine) => {
559
- singletonStore[V2_ENGINE_KEY] = engine;
560
- },
561
- getSentSources: () => singletonStore[V2_SENT_SOURCES_KEY] ?? null,
562
- setSentSources: (sources) => {
563
- singletonStore[V2_SENT_SOURCES_KEY] = sources;
564
- },
565
- getDriftWarned: () => Boolean(singletonStore[V2_DRIFT_WARNED_KEY]),
566
- setDriftWarned: (value) => {
567
- singletonStore[V2_DRIFT_WARNED_KEY] = value;
578
+ function engineHandle(key, options) {
579
+ const engines = sessionRegistry().engines;
580
+ const id = sessionKeyId(key);
581
+ let api = engines.get(id);
582
+ if (api === void 0) {
583
+ let engine = null;
584
+ let sentSources = null;
585
+ let driftWarned = false;
586
+ api = require_pipeline.createV2EngineApi({
587
+ ...options,
588
+ isV2: () => true,
589
+ loadNativeEngine: engineFactory,
590
+ store: {
591
+ getEngine: () => engine,
592
+ setEngine: (next) => {
593
+ engine = next;
594
+ },
595
+ getSentSources: () => sentSources,
596
+ setSentSources: (sources) => {
597
+ sentSources = sources;
598
+ },
599
+ getDriftWarned: () => driftWarned,
600
+ setDriftWarned: (value) => {
601
+ driftWarned = value;
602
+ }
603
+ }
604
+ });
605
+ engines.set(id, api);
606
+ }
607
+ return api;
608
+ }
609
+ /** Releases a standalone handle, one no joined build retires: its host
610
+ * calls this when it closes. */
611
+ function releaseEngineHandle(key) {
612
+ sessionRegistry().engines.delete(sessionKeyId(key));
613
+ }
614
+ /**
615
+ * The build's engine API, from the one engine-handle store.
616
+ */
617
+ function engineApi(key) {
618
+ const override = overrideGlobal[ENGINE_API_OVERRIDE_KEY];
619
+ if (override) return override();
620
+ joined(key);
621
+ return engineHandle(key, {
622
+ label: "animus",
623
+ passThroughUnknownPaths: true
624
+ })();
625
+ }
626
+ /** A fresh build id, created once by the owning host wrapper. */
627
+ function createBuildId() {
628
+ return (0, crypto.randomUUID)();
629
+ }
630
+ /** The key of build `build` of `root`, by the root's canonical path. */
631
+ function sessionKey(build, root) {
632
+ const absolute = (0, path.resolve)(root);
633
+ let canonical = absolute;
634
+ try {
635
+ canonical = (0, fs.realpathSync)(absolute);
636
+ } catch {}
637
+ return {
638
+ build,
639
+ root: canonical,
640
+ protocol: 1
641
+ };
642
+ }
643
+ function sessionKeyId(key) {
644
+ return JSON.stringify([
645
+ key.protocol,
646
+ key.build,
647
+ key.root
648
+ ]);
649
+ }
650
+ /** A participant joined a session with semantic options that differ from
651
+ * those the session was created with. */
652
+ var SessionConfigurationConflict = class extends Error {
653
+ key;
654
+ established;
655
+ joining;
656
+ differing;
657
+ code = "SessionConfigurationConflict";
658
+ constructor(key, established, joining, differing, requirement = "Every participant of one build must use the same Animus options.") {
659
+ super(`[animus] SessionConfigurationConflict: "${joining}" joined build ${key.build} of ${key.root} with ${differing.join(", ")} differing from "${established}". ${requirement}`);
660
+ this.key = key;
661
+ this.established = established;
662
+ this.joining = joining;
663
+ this.differing = differing;
664
+ this.name = "SessionConfigurationConflict";
665
+ }
666
+ };
667
+ function digestOf(field) {
668
+ return {
669
+ field,
670
+ text: (options) => {
671
+ const value = options[field];
672
+ return value === void 0 ? void 0 : require_pipeline.stableStringify(value);
568
673
  }
674
+ };
675
+ }
676
+ /**
677
+ * Every option, classified: one a build's analysis depends on, which every
678
+ * participant must agree on, or one deliberately excluded. `verbose` changes
679
+ * no output, and the root is the key's. The type checker rejects an option
680
+ * added to `AnimusCoreOptions` until it is classified here.
681
+ */
682
+ const OPTION_CLASSES = {
683
+ system: digestOf("system"),
684
+ exclude: digestOf("exclude"),
685
+ extensions: digestOf("extensions"),
686
+ strict: digestOf("strict"),
687
+ diagnostics: digestOf("diagnostics"),
688
+ verbose: "excluded",
689
+ prefix: digestOf("prefix"),
690
+ prefixContextualVars: digestOf("prefixContextualVars"),
691
+ targets: digestOf("targets"),
692
+ minify: digestOf("minify"),
693
+ staticCss: digestOf("staticCss"),
694
+ layers: digestOf("layers"),
695
+ engine: digestOf("engine"),
696
+ mode: digestOf("mode"),
697
+ runtimeImport: digestOf("runtimeImport"),
698
+ root: "excluded"
699
+ };
700
+ /** The options a build's analysis depends on, each as stable text; each is
701
+ * data by its type, so equal text is an equal value. A host's own options
702
+ * are not the build's. */
703
+ function semanticOptions(options) {
704
+ const present = [];
705
+ for (const [name, digest] of Object.entries(OPTION_CLASSES)) {
706
+ if (digest === "excluded") continue;
707
+ const text = digest.text(options);
708
+ if (text !== void 0) present.push([name, text]);
709
+ }
710
+ return new Map(present);
711
+ }
712
+ const SESSION_REGISTRY = Symbol.for("animus.session-registry.v1");
713
+ const registryGlobal = globalThis;
714
+ function sessionRegistry() {
715
+ return registryGlobal[SESSION_REGISTRY] ??= {
716
+ sessions: /* @__PURE__ */ new Map(),
717
+ engines: /* @__PURE__ */ new Map(),
718
+ factory: null
719
+ };
720
+ }
721
+ /**
722
+ * Joins `participant` to the session of `key`, creating it with `options` on
723
+ * the first join. A later participant must bring the same semantic options.
724
+ */
725
+ function joinSession(key, participant, options) {
726
+ const sessions = sessionRegistry().sessions;
727
+ const id = sessionKeyId(key);
728
+ const joining = semanticOptions(options);
729
+ const existing = sessions.get(id);
730
+ if (existing === void 0) {
731
+ const state = {
732
+ key,
733
+ established: participant,
734
+ options: joining,
735
+ participants: /* @__PURE__ */ new Set([participant]),
736
+ activeAttempt: null,
737
+ authority: null,
738
+ analysisStarted: null,
739
+ watchTransaction: null,
740
+ observations: {
741
+ revision: 0,
742
+ rescan: false,
743
+ untaken: /* @__PURE__ */ new Map(),
744
+ suppliedSources: /* @__PURE__ */ new Map(),
745
+ revisions: /* @__PURE__ */ new Map(),
746
+ active: null
747
+ },
748
+ systemAuthority: null,
749
+ claimRecord: null,
750
+ publication: null,
751
+ artifactDir: null,
752
+ unmatchedExcludesReported: false
753
+ };
754
+ sessions.set(id, state);
755
+ return state;
756
+ }
757
+ const differing = [.../* @__PURE__ */ new Set([...existing.options.keys(), ...joining.keys()])].filter((name) => existing.options.get(name) !== joining.get(name));
758
+ if (differing.length > 0) throw new SessionConfigurationConflict(key, existing.established, participant, differing.sort());
759
+ existing.participants.add(participant);
760
+ return existing;
761
+ }
762
+ /** The session of `key`, if a participant has joined it. */
763
+ function findSession(key) {
764
+ return sessionRegistry().sessions.get(sessionKeyId(key));
765
+ }
766
+ /** The build's on-disk claim record, which stops another process pruning
767
+ * its tree: held once by the build, whichever participant publishes, and
768
+ * released only when the build retires. */
769
+ function holdBuildClaimRecord(key, hold) {
770
+ const state = joined(key);
771
+ state.claimRecord ??= hold();
772
+ }
773
+ /** `participant` leaves the build of `key`. The build retires with its
774
+ * last participant, once no attempt is in flight: it leaves the registry
775
+ * and releases its claim record, so its tree is pruned once stale. */
776
+ function leaveSession(key, participant) {
777
+ const state = findSession(key);
778
+ if (state === void 0) return;
779
+ state.participants.delete(participant);
780
+ retireIfUnused(sessionKeyId(key), state);
781
+ }
782
+ function retireIfUnused(id, state) {
783
+ if (state.participants.size > 0) return;
784
+ const inflight = state.watchTransaction;
785
+ if (inflight !== null) {
786
+ const retry = () => retireIfUnused(id, state);
787
+ inflight.then(retry, retry);
788
+ return;
569
789
  }
570
- });
571
- function engineApi() {
572
- const override = singletonStore[ENGINE_API_OVERRIDE_KEY];
573
- return override ? override() : v2EngineApi();
790
+ const sessions = sessionRegistry().sessions;
791
+ if (sessions.get(id) !== state) return;
792
+ sessions.delete(id);
793
+ sessionRegistry().engines.delete(id);
794
+ state.claimRecord?.();
795
+ state.claimRecord = null;
574
796
  }
575
797
  //#endregion
576
798
  //#region session/timing.ts
@@ -606,6 +828,23 @@ function logBuildTimings(bt, rustTiming, log, verbose) {
606
828
  }
607
829
  //#endregion
608
830
  //#region session/extraction-session.ts
831
+ /** An observation's state when its file is gone, or cannot be read; any
832
+ * other state is the content hash it was observed with. */
833
+ const REMOVED_OBSERVATION = "removed";
834
+ const UNREADABLE_OBSERVATION = "unreadable";
835
+ /** A loaded system's identity: its evaluated modules and a fingerprint of
836
+ * the whole load. */
837
+ function systemLoad(system) {
838
+ return {
839
+ identity: require_pipeline.contentHash(JSON.stringify(system)),
840
+ dependencies: system.dependencies ?? []
841
+ };
842
+ }
843
+ const CONSOLE_LOGGER = {
844
+ info: (line) => console.error(line),
845
+ warn: (line) => console.warn(line),
846
+ error: (line) => console.error(line)
847
+ };
609
848
  const SESSION_DIR_MAX_AGE_MS = 864e5;
610
849
  const STATUS_WATCHDOG_MS = 2e3;
611
850
  const LEGACY_FLAT_ARTIFACTS = [
@@ -624,29 +863,120 @@ function isDiskString(value) {
624
863
  }
625
864
  /** Rejection of an attempt whose corpus holds an aborted parse. */
626
865
  var AbortedParseRejection = class extends Error {};
866
+ /** A reload refused because its full read would replace host-supplied
867
+ * bytes with the disk's: a system reload a parse hold deferred, or a full
868
+ * read that reaches a host-supplied file only under another spelling (a
869
+ * case-insensitive volume). Like a hold, it keeps the prior publication
870
+ * and the observed sources. */
871
+ var DeferredReloadRefusal = class extends Error {};
872
+ /** The file's canonical path, or null when it cannot be resolved. */
873
+ function canonicalPath(path$4) {
874
+ try {
875
+ return fs.realpathSync.native(path$4);
876
+ } catch {
877
+ return null;
878
+ }
879
+ }
880
+ /** The host sources a full read reaches only under another spelling: a key
881
+ * the read did not reach exactly, whose file is one the read reached by
882
+ * another path (a case-insensitive volume, a symlinked alias). A key with
883
+ * no such file is not read either way, and is left alone. */
884
+ function misspelledHostSources(hostSources, readPaths) {
885
+ const misses = [...hostSources.keys()].filter((path$5) => !readPaths.has(path$5));
886
+ if (misses.length === 0) return [];
887
+ const readByCanonical = /* @__PURE__ */ new Map();
888
+ for (const readPath of readPaths) {
889
+ const canonical = canonicalPath(readPath);
890
+ if (canonical === null) continue;
891
+ readByCanonical.set(canonical, [...readByCanonical.get(canonical) ?? [], readPath]);
892
+ }
893
+ return misses.flatMap((path$6) => {
894
+ const canonical = canonicalPath(path$6);
895
+ const read = canonical === null ? void 0 : readByCanonical.get(canonical);
896
+ return read === void 0 ? [] : [{
897
+ path: path$6,
898
+ read
899
+ }];
900
+ });
901
+ }
627
902
  function isDiskNumber(value) {
628
903
  return Object(value) !== value && Object.prototype.toString.call(value) === "[object Number]";
629
904
  }
905
+ /** The session's own system-props file, imported by absolute path: what a
906
+ * host that serves no system-props module (the CLI) delivers. */
907
+ const SESSION_ARTIFACT = Symbol("animus.session-artifact");
908
+ /** A file's content hash, or '' when it cannot be read. */
909
+ function fileHashOrEmpty(path$7) {
910
+ try {
911
+ return require_pipeline.contentHash((0, fs.readFileSync)(path$7, "utf-8"));
912
+ } catch {
913
+ return "";
914
+ }
915
+ }
916
+ /** The first file whose content no longer has its recorded hash: an
917
+ * unreadable or deleted file reads as '', which mismatches a real one. */
918
+ function firstChangedFile(hashes) {
919
+ for (const [path$8, hash] of hashes) if (fileHashOrEmpty(path$8) !== hash) return path$8;
920
+ }
630
921
  /** Bundler-agnostic extraction pipeline: system load, source ingestion,
631
922
  * analysis, emission, and watch-cycle diffing. Carries no bundler types. */
632
923
  var ExtractionSession = class {
633
924
  host;
634
925
  /** Set by the adapter before any pipeline run. */
635
926
  rootDir = null;
927
+ /** A host that resolves imports itself (Vite's `this.resolve`): package
928
+ * discovery asks it first, the loader only for a declared workspace it
929
+ * cannot resolve. Null resolves through the loader alone. */
930
+ hostResolve = null;
931
+ /** Each file's alias comparison, kept by content hash across analyses. */
932
+ aliasCache = new require_pipeline.AliasAgreementCache();
636
933
  /** Serialized path aliases, harvested by the adapter from bundler config. */
637
934
  pathAliasesJson = null;
935
+ /** The host's aliases are the project's tsconfig `paths`: each full pass
936
+ * re-reads them, so a tsconfig edit takes effect. */
937
+ aliasesFromTsconfig = false;
938
+ /** The root package.json and the tsconfig chain, absent files included:
939
+ * a change to any is a configuration change, which runs the full pass. */
940
+ configInputPaths = [];
941
+ configInputKeys = /* @__PURE__ */ new Set();
942
+ /** Each configuration input's content hash at the last full pass, ''
943
+ * for an absent one: the probe's basis when the host reports nothing. */
944
+ configInputHashes = /* @__PURE__ */ new Map();
638
945
  /** The host's export conditions, in its order, for the system loader. */
639
946
  conditions = [];
947
+ /** The host's extension order, as its adapter captured it; the built-in
948
+ * policy where no host resolves. */
949
+ extensionPolicy = require_pipeline.BUILT_IN_EXTENSION_POLICY;
950
+ /** Which files the host serves as assets (spec §2.2 A8); the built-in
951
+ * classification where no host's can be read. */
952
+ assetClassification = require_pipeline.builtInAssetClassification("noHost");
953
+ /** How the host's bundler expands computed loads (spec §6.3); without
954
+ * one, every computed load keeps its widest reading. */
955
+ loadExpander;
640
956
  assetCopyCache = /* @__PURE__ */ new Map();
641
957
  /** Physical asset files registered with the host watcher. */
642
958
  assetDependencyPaths = /* @__PURE__ */ new Set();
643
959
  assetDependencyKeys = /* @__PURE__ */ new Set();
644
- /** System-props module id override. Turbopack rejects absolute-path
645
- * imports, so its driver sets a virtual id; null injects the abs path. */
646
- systemPropsModuleId = null;
960
+ /** What generated code imports; a host that serves its own modules
961
+ * declares them before planning. */
962
+ delivery = {
963
+ cssModuleId: ANIMUS_CSS_MODULE_ID,
964
+ systemPropsModuleId: SESSION_ARTIFACT
965
+ };
647
966
  /** Whether the analysis-inputs hydration corpus is written to disk. Only
648
967
  * isolated loader workers replay it; an in-process loader reads memory. */
649
968
  persistAnalysisInputs = false;
969
+ /** The host substitutes asset() URLs itself (Vite: dev copies, build
970
+ * emitFile markers): the published sheets keep their placeholders, and
971
+ * the session makes no copy and tracks no asset file. */
972
+ hostSubstitutesAssets = false;
973
+ /** Whether the session keeps its artifacts on disk: status, commit,
974
+ * manifest, styles, the epoch witness, asset copies and its directory
975
+ * claim, with the sibling reconciliation they need. A host that serves
976
+ * every result from memory (Vite) turns it off; the shared state still
977
+ * publishes. Such a host also substitutes assets (above): asset copies
978
+ * are artifacts. */
979
+ persistArtifacts = true;
650
980
  /** Whether this session serves development (a watch), where an error-level
651
981
  * diagnostic is reported and the session keeps running. Only a build
652
982
  * fails on one. */
@@ -694,29 +1024,90 @@ var ExtractionSession = class {
694
1024
  /** Fires exactly once per settled `handleWatchUpdate` — the failure cause,
695
1025
  * or null when clean. Observation never alters delivery. */
696
1026
  onCycleSettled = null;
1027
+ /** Fires once per published generation, and per held attempt. */
1028
+ onOutcome = null;
1029
+ /** The last published generation; null before the first. */
1030
+ publication = null;
1031
+ /** The original sources (raw, before adaptation) the session holds, by
1032
+ * root-relative path: the published generation's, plus any that an
1033
+ * attempt in flight or held by an aborted parse has read. */
1034
+ get heldSources() {
1035
+ return this.fileCache;
1036
+ }
1037
+ /** The generation an emit produced, until it commits with the
1038
+ * ingestion's ownership and the external package state it analysed. */
1039
+ stagedPublication = null;
697
1040
  options;
698
1041
  staticCssJson;
699
1042
  system = null;
700
- /** Configured external files that could not be read. Incremental passes
701
- * re-collect nothing, so they replay these instead of passing strict. */
1043
+ /** Configured files that could not be read. Incremental passes replay
1044
+ * these instead of passing strict, and add a watched change they cannot
1045
+ * read. */
702
1046
  ingestionFailureDiagnostics = [];
703
1047
  /** Whether the corpus being analyzed left a file out, whose renders the
704
1048
  * analysis cannot see. */
705
1049
  skippedSources = [];
1050
+ /** Resolver calls ingestion ended under another spelling. */
1051
+ ingestionOpenings = [];
1052
+ /** The adapted originals of the ingestion being analysed: it publishes
1053
+ * only after its analysis does. */
1054
+ ingestionAdapted = /* @__PURE__ */ new Map();
706
1055
  /** Full package-resolution map from the last full pipeline — replayed by
707
1056
  * incremental passes (sourceEntries alone omits dist-resolved packages). */
708
1057
  lastPackageMap = {};
709
1058
  fileCache = /* @__PURE__ */ new Map();
710
- corpus = require_pipeline.createSourceCorpus({
711
- engineApi: () => engineApi(),
712
- prefix: "[animus-next]",
713
- strict: () => !!this.options.strict,
714
- warn: (message) => this.warn(message),
715
- reportErrors: () => this.reportErrors()
716
- });
1059
+ /** Held sources whose bytes the host supplied (WatchChanges.sources),
1060
+ * by root-relative path; a read from disk drops the path. */
1061
+ hostSuppliedKeys = /* @__PURE__ */ new Set();
1062
+ corpus = require_pipeline.createSourceCorpus(this.corpusHost());
1063
+ /** The corpus reads the driver label when it reports, so a host that
1064
+ * sets `driverLabel` after construction is named in its messages. */
1065
+ corpusHost() {
1066
+ const label = () => this.driverLabel;
1067
+ return {
1068
+ engineApi: () => this.engine(),
1069
+ get prefix() {
1070
+ return `[${label()}]`;
1071
+ },
1072
+ strict: () => !!this.options.strict,
1073
+ warn: (message) => this.warn(message),
1074
+ reportErrors: () => this.reportErrors(),
1075
+ extensions: () => this.extensionPolicy,
1076
+ rootDir: () => this.rootDir
1077
+ };
1078
+ }
717
1079
  systemDependencyKeys = /* @__PURE__ */ new Set();
1080
+ /** What a host watches for this Session, one list for every host: the
1081
+ * files whose change it acts on (the system module and the modules it
1082
+ * read, asset() sources, the configuration inputs), the paths the system
1083
+ * load found absent, whose creation reloads it, and the external package
1084
+ * roots, watched recursively. An absent path inside `covered` (roots the
1085
+ * host already watches recursively) is left out: a host's recursive
1086
+ * watch reports its creation, and watching an absent file inside a
1087
+ * watched directory can stop that directory's events (inotify). */
1088
+ watchPlan(covered = []) {
1089
+ const system = this.rootDir === null ? [] : [(0, path.resolve)(this.rootDir, this.options.system)];
1090
+ return {
1091
+ files: [.../* @__PURE__ */ new Set([
1092
+ ...system,
1093
+ ...this.systemDependencyPaths,
1094
+ ...this.assetDependencyPaths,
1095
+ ...this.configInputPaths
1096
+ ])],
1097
+ missing: this.systemMissingPaths.filter((path$9) => !covered.some((root) => require_pipeline.isPathWithinRoot(root, path$9))),
1098
+ directories: [...this.externalWatchRoots]
1099
+ };
1100
+ }
1101
+ /** Whether `path` is the system module, one of its dependencies, or a
1102
+ * path its load found absent: a change to it reloads the system. */
1103
+ isSystemDependency(path$10) {
1104
+ return require_pipeline.toWatchKeys(path$10).some((key) => this.systemDependencyKeys.has(key));
1105
+ }
718
1106
  /** Loader-reported dependency paths for bundler watch registration. */
719
1107
  systemDependencyPaths = [];
1108
+ /** Paths the system load looked for and found absent: a host registers
1109
+ * them for creation, which reloads the system. */
1110
+ systemMissingPaths = [];
720
1111
  systemDependencyHashes = /* @__PURE__ */ new Map();
721
1112
  lastSystemPropsHash = null;
722
1113
  artifactRecords = {
@@ -730,9 +1121,20 @@ var ExtractionSession = class {
730
1121
  /** Release handle for the in-process owner slot AND the directory's
731
1122
  * on-disk owner record, taken and released as one; null when unheld. */
732
1123
  releasePublicationClaim = null;
733
- /** Session identity, claimed once per PROCESS and adopted by every later
734
- * session instance so all compilers share one artifact tree. */
735
- sessionId = claimProcessSessionId();
1124
+ /** The build this session takes part in: every participant of one build
1125
+ * shares its artifact tree and state; another build of the root never
1126
+ * does. */
1127
+ sessionId;
1128
+ /** The log tier, from the option and `ANIMUS_DEBUG` at construction. */
1129
+ verbosity;
1130
+ /** The emission mode at construction: explicit, else `NODE_ENV`'s. It
1131
+ * defaults minify. */
1132
+ startupMode;
1133
+ joinedKey = null;
1134
+ /** This instance as a participant of its build, once joined. */
1135
+ participant = null;
1136
+ /** Closed: it left its build and starts no attempt until it reattaches. */
1137
+ detached = false;
736
1138
  /** Epoch of the last published analysis; null seeds from disk so a
737
1139
  * same-session restart with unchanged plans rewrites no bytes. */
738
1140
  lastEpochValue = null;
@@ -747,10 +1149,23 @@ var ExtractionSession = class {
747
1149
  * the cache keeps every observed source so the repair publishes them in
748
1150
  * one generation; a successful publication clears it. */
749
1151
  heldRejection = null;
750
- /** A system reload an aborted parse rejected. Its dependency hashes already
751
- * advanced, so the first batch that passes the parse check publishes
752
- * through the full pipeline, which loads the latest system. */
1152
+ /** Reload debt: a full pass that failed with a committed system to
1153
+ * hold, in development or under an aborted parse, owed until one
1154
+ * succeeds. runFullPipeline alone sets and clears it. While it stands, every batch retries the full reload and
1155
+ * nothing publishes against the stale system. Under an aborted parse's
1156
+ * hold, the first batch that passes the parse check pays it instead. */
753
1157
  systemReloadOwed = false;
1158
+ /** The system dependencies the last committed full pass recorded: a
1159
+ * failed reload watches these and its own, never an earlier failure's. */
1160
+ lastGoodSystemDependencies = {
1161
+ keys: /* @__PURE__ */ new Set(),
1162
+ paths: [],
1163
+ missing: [],
1164
+ hashes: /* @__PURE__ */ new Map()
1165
+ };
1166
+ /** Whether the current full pass's system load recorded its
1167
+ * dependencies. */
1168
+ attemptRecordedSystemDependencies = false;
754
1169
  /** Monotonic readiness witness: flips true on the first complete
755
1170
  * publication and never regresses; every later status write carries it. */
756
1171
  firstEmissionComplete = false;
@@ -761,27 +1176,57 @@ var ExtractionSession = class {
761
1176
  constructor(options, host = {}) {
762
1177
  this.host = host;
763
1178
  this.options = options;
1179
+ this.sessionId = host.build ?? createBuildId();
764
1180
  this.staticCssJson = require_pipeline.serializeStaticCss(options.staticCss);
1181
+ this.verbosity = require_pipeline.resolveVerbosity(options.verbose, process.env.ANIMUS_DEBUG);
1182
+ this.startupMode = require_pipeline.resolveMode(options.mode, () => process.env.NODE_ENV === "production" ? "production" : "development").mode;
765
1183
  }
766
1184
  /** Forgets that `relPath` could not be read once an incremental pass reads
767
- * or deletes it, so its cost stops holding pruning off. */
1185
+ * or deletes it, so its cost stops holding pruning off. Whether one was
1186
+ * recorded. */
768
1187
  clearIngestionFailure(relPath) {
1188
+ const before = this.ingestionFailureDiagnostics.length;
769
1189
  this.ingestionFailureDiagnostics = this.ingestionFailureDiagnostics.filter((diagnostic) => diagnostic.file !== relPath);
1190
+ return this.ingestionFailureDiagnostics.length !== before;
1191
+ }
1192
+ /** The module extracted terminals import their factories from. */
1193
+ get runtimeImport() {
1194
+ return this.options.runtimeImport ?? "@animus-ui/system/runtime";
770
1195
  }
771
1196
  get sessionDir() {
772
1197
  return sessionArtifactDir(this.rootDir, this.sessionId);
773
1198
  }
1199
+ /** This session's build of its root, joined on first use: a participant
1200
+ * whose options differ from the build's fails with
1201
+ * `SessionConfigurationConflict`. */
1202
+ get key() {
1203
+ if (this.joinedKey === null) {
1204
+ const key = sessionKey(this.sessionId, this.rootDir);
1205
+ const participant = `${this.driverLabel}#${(0, crypto.randomUUID)()}`;
1206
+ joinSession(key, participant, this.options);
1207
+ this.participant = participant;
1208
+ this.joinedKey = key;
1209
+ }
1210
+ return this.joinedKey;
1211
+ }
774
1212
  get verbose() {
775
- return require_pipeline.resolveVerbosity(this.options.verbose, process.env.ANIMUS_DEBUG) !== "quiet";
1213
+ return this.verbosity !== "quiet";
1214
+ }
1215
+ /** The engine of this session's build. */
1216
+ engine() {
1217
+ return this.host.engine?.() ?? engineApi(this.key);
1218
+ }
1219
+ get logger() {
1220
+ return this.host.logger ?? CONSOLE_LOGGER;
776
1221
  }
777
1222
  log(msg) {
778
- if (this.verbose) console.error(`[animus] ${msg}`);
1223
+ if (this.verbose) this.logger.info(`[animus] ${msg}`);
779
1224
  }
780
1225
  warn(msg) {
781
- console.warn(`[animus] ${msg}`);
1226
+ this.logger.warn(`[animus] ${msg}`);
782
1227
  }
783
1228
  reportErrors() {
784
- return this.development ? (message) => console.error(message) : void 0;
1229
+ return this.development ? (message) => this.logger.error(message) : void 0;
785
1230
  }
786
1231
  /** A build-strictness check that failed: a build throws `message`, and
787
1232
  * development reports it at error level and takes the non-strict path. */
@@ -790,6 +1235,11 @@ var ExtractionSession = class {
790
1235
  if (!report) throw new Error(message);
791
1236
  report(message);
792
1237
  }
1238
+ /** A finding `strict` turns into a strict failure; otherwise a warning. */
1239
+ strictOrWarn(message, strictMessage = message) {
1240
+ if (this.options.strict) this.strictFailure(strictMessage);
1241
+ else this.warn(message);
1242
+ }
793
1243
  now() {
794
1244
  return this.verbose ? performance.now() : 0;
795
1245
  }
@@ -804,6 +1254,11 @@ var ExtractionSession = class {
804
1254
  lastExternalOutcomes = [];
805
1255
  /** The analysed kits' descriptors, from the last discovery. */
806
1256
  kitDescriptors = [];
1257
+ /** Package name → the real root of the instance discovery read. */
1258
+ packageInstances = {};
1259
+ /** What this attempt's analysis read of the discovered packages; the
1260
+ * build's shared record once the analysis publishes. */
1261
+ packageReadRecord = null;
807
1262
  /** Component count of the last published analysis, null before the first —
808
1263
  * read by drivers instead of re-parsing the manifest JSON every cycle. */
809
1264
  lastComponentCount = null;
@@ -814,18 +1269,15 @@ var ExtractionSession = class {
814
1269
  /** When superseded asset copies leave `assets/`: `'full-pipeline'` suits a
815
1270
  * driver serving the dir in place, `'every-cycle'` one republishing it. */
816
1271
  staleAssetPruning = "full-pipeline";
817
- /** Driver-owned STRUCTURAL exclusions, joined to the never-replaceable set
818
- * — never the user list, whose presence flips the replace semantics. */
1272
+ /** The host's output directories (`outputExclusion`): root-relative,
1273
+ * excluded as literal path prefixes from discovery and from package
1274
+ * collection, whatever the user's list. */
819
1275
  structuralExclude = [];
820
1276
  scanConfigMemo = null;
821
- /** Per-pattern exclusion hit counts from this session's matcher —
822
- * driver-consumed reporting surface (dead-pattern visibility). */
823
- getExcludeStats() {
824
- return this.resolveScanConfig().excludeMatcher.stats();
825
- }
826
1277
  /** Resolve the scan configuration from options — the single source of the
827
- * exclude/extension policy shared by the full and incremental pipelines. */
828
- resolveScanConfig() {
1278
+ * exclude/extension policy shared by the full and incremental pipelines,
1279
+ * and by a host classifying its own file events. */
1280
+ get scanConfig() {
829
1281
  if (this.scanConfigMemo === null) {
830
1282
  const extensionsSet = new Set(this.options.extensions ?? require_pipeline.DEFAULT_EXTENSIONS);
831
1283
  this.scanConfigMemo = {
@@ -835,8 +1287,8 @@ var ExtractionSession = class {
835
1287
  }
836
1288
  return this.scanConfigMemo;
837
1289
  }
838
- /** Concurrent entries wait for publication, then ingest their own batch;
839
- * content hashes coalesce observations the prior analysis covered. */
1290
+ /** Records the batch before any await, so an attempt that rejects never
1291
+ * erases it, and settles with the attempt that takes it (SA20). */
840
1292
  async handleWatchUpdate(changes) {
841
1293
  try {
842
1294
  await this.routeWatchUpdate(changes);
@@ -847,36 +1299,131 @@ var ExtractionSession = class {
847
1299
  this.onCycleSettled?.(null);
848
1300
  }
849
1301
  async routeWatchUpdate(changes) {
850
- const hasBatch = (changes.modifiedFiles?.size ?? 0) > 0 || (changes.removedFiles?.size ?? 0) > 0;
851
- const inflight = getWatchTransaction();
1302
+ if ((changes.modifiedFiles?.size ?? 0) > 0 || (changes.removedFiles?.size ?? 0) > 0) {
1303
+ await this.awaitObservation(this.recordObservation(changes));
1304
+ return;
1305
+ }
1306
+ const inflight = getWatchTransaction(this.key);
852
1307
  if (inflight) {
853
1308
  await inflight;
854
- if (!hasBatch) return;
855
- }
856
- if (!this.system) {
857
- const owner = getOwningWatchSession();
858
- if (owner && owner !== this && hasBatch) await owner.ingestForwardedBatch(changes);
859
1309
  return;
860
1310
  }
861
- await this.ingestForwardedBatch(changes);
1311
+ if (this.system && !this.detached) await this.runWatchAttempt(changes);
1312
+ }
1313
+ /** Queues the batch's observations under a new revision, synchronously.
1314
+ * A batch equivalent to what the active attempt took joins that attempt:
1315
+ * a second compiler's report of the same edit never analyzes it twice. */
1316
+ recordObservation(changes) {
1317
+ const queue = observationQueue(this.key);
1318
+ const observed = /* @__PURE__ */ new Map();
1319
+ for (const path$11 of changes.modifiedFiles ?? []) {
1320
+ let state;
1321
+ try {
1322
+ state = require_pipeline.contentHash(changes.sources?.get(path$11) ?? (0, fs.readFileSync)(path$11, "utf-8"));
1323
+ } catch {
1324
+ state = UNREADABLE_OBSERVATION;
1325
+ }
1326
+ observed.set(path$11, state);
1327
+ }
1328
+ for (const path$12 of changes.removedFiles ?? []) observed.set(path$12, REMOVED_OBSERVATION);
1329
+ const revision = ++queue.revision;
1330
+ const active = queue.active;
1331
+ if (active !== null && [...observed].every(([path$13, state]) => !queue.untaken.has(path$13) && active.taken.get(path$13) === state)) {
1332
+ queue.revisions.set(revision, active.transaction);
1333
+ return revision;
1334
+ }
1335
+ for (const [path$14, state] of observed) {
1336
+ queue.untaken.set(path$14, state);
1337
+ const supplied = changes.sources?.get(path$14);
1338
+ if (supplied === void 0) queue.suppliedSources.delete(path$14);
1339
+ else queue.suppliedSources.set(path$14, supplied);
1340
+ }
1341
+ queue.revisions.set(revision, null);
1342
+ return revision;
862
1343
  }
863
- /** The shared owner gate for local and forwarded batches. Every waiter
864
- * rechecks the slot so distinct batches publish in arrival order. */
865
- async ingestForwardedBatch(changes) {
866
- if (!this.system) return;
1344
+ /** Settles with the attempt that takes `revision`. Another attempt's
1345
+ * rejection is its own callers': this observation runs after it. */
1346
+ async awaitObservation(revision) {
1347
+ const queue = observationQueue(this.key);
867
1348
  for (;;) {
868
- const inflight = getWatchTransaction();
869
- if (!inflight) break;
870
- await inflight;
871
- }
872
- const transaction = Promise.resolve().then(() => this.processWatchUpdate(changes));
873
- setWatchTransaction(transaction);
874
- try {
875
- await transaction;
876
- } finally {
877
- setWatchTransaction(null);
1349
+ const taken = queue.revisions.get(revision);
1350
+ if (taken !== null && taken !== void 0) {
1351
+ queue.revisions.delete(revision);
1352
+ await taken;
1353
+ return;
1354
+ }
1355
+ const inflight = getWatchTransaction(this.key);
1356
+ if (inflight) {
1357
+ await inflight.catch(() => void 0);
1358
+ continue;
1359
+ }
1360
+ const owner = this.system && !this.detached ? this : getOwningWatchSession(this.key);
1361
+ if (owner === null && !this.detached && getManifestJson(this.key) !== null) {
1362
+ await this.runFullPipeline();
1363
+ continue;
1364
+ }
1365
+ if (owner === null || !owner.startObservedAttempt()) {
1366
+ queue.revisions.delete(revision);
1367
+ return;
1368
+ }
878
1369
  }
879
1370
  }
1371
+ /** Starts one attempt over every untaken observation of the build. */
1372
+ startObservedAttempt() {
1373
+ if (!this.system || this.detached) return false;
1374
+ const { taken, rescan, sources } = this.takeObservations();
1375
+ const modifiedFiles = /* @__PURE__ */ new Set();
1376
+ const removedFiles = /* @__PURE__ */ new Set();
1377
+ for (const [path$15, state] of taken) (state === REMOVED_OBSERVATION ? removedFiles : modifiedFiles).add(path$15);
1378
+ this.settleTaken(taken, this.runWatchAttempt({
1379
+ modifiedFiles,
1380
+ removedFiles,
1381
+ sources
1382
+ }, rescan));
1383
+ return true;
1384
+ }
1385
+ /** Every untaken observation of the build, and whether a full rescan is
1386
+ * requested, which the caller's attempt now covers. */
1387
+ takeObservations() {
1388
+ const queue = observationQueue(this.key);
1389
+ const taken = new Map(queue.untaken);
1390
+ const rescan = queue.rescan;
1391
+ const sources = new Map(queue.suppliedSources);
1392
+ queue.untaken.clear();
1393
+ queue.suppliedSources.clear();
1394
+ queue.rescan = false;
1395
+ return {
1396
+ taken,
1397
+ rescan,
1398
+ sources
1399
+ };
1400
+ }
1401
+ /** `transaction` settles every waiting revision, and is the active
1402
+ * attempt equivalent observations join until it settles. */
1403
+ settleTaken(taken, transaction) {
1404
+ const queue = observationQueue(this.key);
1405
+ for (const [revision, attempt] of queue.revisions) if (attempt === null) queue.revisions.set(revision, transaction);
1406
+ queue.active = {
1407
+ taken,
1408
+ transaction
1409
+ };
1410
+ const settle = () => {
1411
+ if (queue.active?.transaction === transaction) queue.active = null;
1412
+ };
1413
+ transaction.then(settle, settle);
1414
+ }
1415
+ /** Registers one attempt in the build's single-flight slot before it
1416
+ * processes, so a nested full system reload inherits the gate rather
1417
+ * than clear it, and clears the slot when it settles either way. */
1418
+ runWatchAttempt(changes, rescan = false) {
1419
+ const transaction = Promise.resolve().then(() => rescan ? this.runFullPipeline([], changes.sources) : this.processWatchUpdate(changes));
1420
+ setWatchTransaction(this.key, transaction);
1421
+ const settle = () => {
1422
+ if (getWatchTransaction(this.key) === transaction) setWatchTransaction(this.key, null);
1423
+ };
1424
+ transaction.then(settle, settle);
1425
+ return transaction;
1426
+ }
880
1427
  async processWatchUpdate(changes) {
881
1428
  const rootDir = this.rootDir;
882
1429
  try {
@@ -887,31 +1434,25 @@ var ExtractionSession = class {
887
1434
  const changed = [...changes.modifiedFiles ?? [], ...changes.removedFiles ?? []];
888
1435
  let assetChanged = false;
889
1436
  let systemHit;
1437
+ let configHit;
890
1438
  try {
891
- if (!changes.modifiedFiles && !changes.removedFiles) for (const [dep, hash] of this.systemDependencyHashes) {
892
- let current = "";
893
- try {
894
- current = require_pipeline.contentHash((0, fs.readFileSync)(dep, "utf-8"));
895
- } catch {}
896
- if (current !== hash) {
897
- systemHit = dep;
898
- break;
899
- }
900
- }
901
- else systemHit = changed.find((path$2) => require_pipeline.toWatchKeys(path$2).some((key) => this.systemDependencyKeys.has(key)));
1439
+ if (changed.length === 0) {
1440
+ systemHit = firstChangedFile(this.systemDependencyHashes);
1441
+ if (systemHit === void 0) configHit = firstChangedFile(this.configInputHashes);
1442
+ } else systemHit = changed.find((path$16) => this.isSystemDependency(path$16));
902
1443
  } catch (err) {
903
1444
  this.warn(`HMR system-reload check failed: ${String(err)}`);
904
1445
  }
905
- if (systemHit) {
906
- this.log(`system reload: system dependency changed (${(0, path.relative)(rootDir, systemHit)})`);
1446
+ configHit ??= changed.find((path$17) => require_pipeline.toWatchKeys(path$17).some((key) => this.configInputKeys.has(key)));
1447
+ const debtRetry = this.systemReloadOwed && this.heldRejection === null;
1448
+ if (systemHit || configHit || debtRetry) {
1449
+ this.log(systemHit ? `system reload: system dependency changed (${(0, path.relative)(rootDir, systemHit)})` : configHit ? `system reload: configuration changed (${(0, path.relative)(rootDir, configHit)})` : "system reload: retrying the reload a failure left owed");
907
1450
  this.resetForHmr();
908
1451
  try {
909
- const promise = this.runFullPipeline(this.pendingFromBatch(changes));
910
- setAnalysisStartedPromise(promise);
1452
+ const promise = this.runFullPipeline(this.pendingFromBatch(changes), changes.sources);
1453
+ setAnalysisStartedPromise(this.key, promise);
911
1454
  await promise;
912
- this.systemReloadOwed = false;
913
1455
  } catch (err) {
914
- if (err instanceof AbortedParseRejection) this.systemReloadOwed = true;
915
1456
  try {
916
1457
  this.beginStatusAttempt();
917
1458
  this.writeAnalysisStatus("failed", this.pendingFromBatch(changes), String(err));
@@ -922,18 +1463,20 @@ var ExtractionSession = class {
922
1463
  }
923
1464
  return;
924
1465
  }
925
- if (changed.some((path$3) => require_pipeline.toWatchKeys(path$3).some((key) => this.assetDependencyKeys.has(key)))) {
1466
+ if (changed.some((path$18) => require_pipeline.toWatchKeys(path$18).some((key) => this.assetDependencyKeys.has(key)))) {
926
1467
  this.assetCopyCache.clear();
927
1468
  assetChanged = true;
928
1469
  }
929
- const { excludeMatcher, extensionsSet } = this.resolveScanConfig();
1470
+ const { excludeMatcher, extensionsSet } = this.scanConfig;
930
1471
  const priorCacheEntries = /* @__PURE__ */ new Map();
931
1472
  const priorExternalFileOwners = { ...this.externalFileOwners };
1473
+ const priorHostSuppliedKeys = new Set(this.hostSuppliedKeys);
932
1474
  const recordPrior = (key) => {
933
1475
  if (!priorCacheEntries.has(key)) priorCacheEntries.set(key, this.fileCache.get(key) ?? null);
934
1476
  };
935
1477
  const inventoryUpdates = [];
936
1478
  const ownerRemovals = [];
1479
+ const removedProjectFiles = /* @__PURE__ */ new Map();
937
1480
  let removedAny = false;
938
1481
  if (changes.removedFiles) for (const removedPath of changes.removedFiles) {
939
1482
  let key;
@@ -947,8 +1490,10 @@ var ExtractionSession = class {
947
1490
  }
948
1491
  recordPrior(key);
949
1492
  this.clearIngestionFailure(key);
1493
+ this.hostSuppliedKeys.delete(key);
950
1494
  if (this.fileCache.delete(key)) {
951
1495
  removedAny = true;
1496
+ if (!owningRoot) removedProjectFiles.set(key, removedPath);
952
1497
  if (owningRoot) {
953
1498
  inventoryUpdates.push({
954
1499
  root: owningRoot,
@@ -979,15 +1524,29 @@ var ExtractionSession = class {
979
1524
  owningRoot: null
980
1525
  }));
981
1526
  const changedPaths = [];
1527
+ const inOutput = require_pipeline.outputSourceGuard(rootDir, this.structuralExclude);
1528
+ let readFailuresChanged = false;
982
1529
  for (const target of targets) {
983
1530
  const relPath = target.key;
1531
+ const outputDir = inOutput(target.abs);
1532
+ if (outputDir !== null) {
1533
+ this.warn(require_pipeline.outputSourceMessage(relPath, outputDir));
1534
+ continue;
1535
+ }
984
1536
  let source;
985
1537
  try {
986
- source = (0, fs.readFileSync)(target.abs, "utf-8");
987
- } catch {
1538
+ const supplied = changes.sources?.get(target.abs);
1539
+ source = supplied ?? (0, fs.readFileSync)(target.abs, "utf-8");
1540
+ if (supplied === void 0) this.hostSuppliedKeys.delete(relPath);
1541
+ else this.hostSuppliedKeys.add(relPath);
1542
+ } catch (err) {
1543
+ if (require_pipeline.isDeletedSource(err)) continue;
1544
+ this.clearIngestionFailure(relPath);
1545
+ this.ingestionFailureDiagnostics = [...this.ingestionFailureDiagnostics, require_pipeline.unreadableSourceDiagnostic(relPath, err)];
1546
+ readFailuresChanged = true;
988
1547
  continue;
989
1548
  }
990
- this.clearIngestionFailure(relPath);
1549
+ if (this.clearIngestionFailure(relPath)) readFailuresChanged = true;
991
1550
  const rawHash = require_pipeline.contentHash(source);
992
1551
  const cached = this.fileCache.get(relPath);
993
1552
  const hash = rawHash;
@@ -1010,7 +1569,7 @@ var ExtractionSession = class {
1010
1569
  }
1011
1570
  }
1012
1571
  }
1013
- if (changedPaths.length > 0 || removedAny || assetChanged) {
1572
+ if (changedPaths.length > 0 || removedAny || readFailuresChanged || assetChanged) {
1014
1573
  const pending = changedPaths.map((rel) => [rel, this.fileCache.get(rel).hash]);
1015
1574
  const commitObservedBatch = () => {
1016
1575
  for (const update of inventoryUpdates) {
@@ -1036,24 +1595,32 @@ var ExtractionSession = class {
1036
1595
  });
1037
1596
  this.throwOnAbortedParse(ingested);
1038
1597
  this.skippedSources = ingested.skippedOriginals ?? [];
1598
+ this.ingestionOpenings = ingested.openings ?? [];
1599
+ this.ingestionAdapted = require_pipeline.adaptedOriginals(ingested.ownership);
1039
1600
  if (this.systemReloadOwed) {
1040
1601
  this.log("system reload: publishing the reload a parse hold deferred");
1602
+ const stale = this.staleHostSuppliedSources(changes.sources);
1603
+ 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`);
1041
1604
  this.resetForHmr();
1042
- await this.runFullPipeline(pending);
1043
- this.systemReloadOwed = false;
1605
+ await this.runFullPipeline(pending, changes.sources);
1044
1606
  return;
1045
1607
  }
1046
1608
  this.externalFileOwners = require_pipeline.projectExternalFileOwners(ingested, this.externalFileOwners);
1047
- resetAnalysisStartedPromise();
1609
+ resetAnalysisStartedPromise(this.key);
1048
1610
  const promise = this.runIncrementalPipeline(ingested.analysisEntries, pending);
1049
- setAnalysisStartedPromise(promise);
1611
+ setAnalysisStartedPromise(this.key, promise);
1050
1612
  await promise;
1051
1613
  } catch (err) {
1052
- if (err instanceof AbortedParseRejection) commitObservedBatch();
1614
+ if (err instanceof AbortedParseRejection || err instanceof DeferredReloadRefusal) commitObservedBatch();
1053
1615
  else {
1054
- for (const [key, prior] of priorCacheEntries) if (prior === null) this.fileCache.delete(key);
1055
- else this.fileCache.set(key, prior);
1616
+ for (const [key, prior] of priorCacheEntries) {
1617
+ const removed = removedProjectFiles.get(key);
1618
+ if (removed !== void 0 && !(0, fs.existsSync)(removed)) continue;
1619
+ if (prior === null) this.fileCache.delete(key);
1620
+ else this.fileCache.set(key, prior);
1621
+ }
1056
1622
  this.externalFileOwners = priorExternalFileOwners;
1623
+ this.hostSuppliedKeys = priorHostSuppliedKeys;
1057
1624
  }
1058
1625
  if (this.statusAttemptOpen) {
1059
1626
  this.debouncePending.clear();
@@ -1061,7 +1628,7 @@ var ExtractionSession = class {
1061
1628
  }
1062
1629
  throw err;
1063
1630
  }
1064
- this.publishSourceIngestion(ingested);
1631
+ this.publishSourceIngestion(ingested, "incremental", (path$19) => this.fileCache.get(path$19));
1065
1632
  commitObservedBatch();
1066
1633
  } else if (this.statusAttemptOpen) {
1067
1634
  this.debouncePending.clear();
@@ -1076,88 +1643,251 @@ var ExtractionSession = class {
1076
1643
  const rejection = this.corpus.rejection(prepared);
1077
1644
  if (!rejection) return;
1078
1645
  this.heldRejection = rejection.message;
1646
+ this.onOutcome?.({
1647
+ kind: "held",
1648
+ abortedOriginals: rejection.originals
1649
+ });
1079
1650
  throw new AbortedParseRejection(rejection.message);
1080
1651
  }
1081
- publishSourceIngestion(result) {
1652
+ /** Holds a held full read's entries: what it read stays the held bytes,
1653
+ * never overwriting held host bytes with the disk's (a key the host
1654
+ * supplied before that this read took from disk keeps its bytes). */
1655
+ holdObservedEntries(entries, supplied) {
1656
+ for (const entry of entries) {
1657
+ const fromHost = supplied.has(entry.path);
1658
+ if (this.hostSuppliedKeys.has(entry.path) && !fromHost) continue;
1659
+ this.fileCache.set(entry.path, {
1660
+ hash: entry.hash,
1661
+ source: entry.source
1662
+ });
1663
+ if (fromHost) this.hostSuppliedKeys.add(entry.path);
1664
+ }
1665
+ }
1666
+ /** Held sources whose bytes the host supplied and which the disk no
1667
+ * longer matches: a full read not carrying them (`carried`) would
1668
+ * publish the disk's bytes in their place. */
1669
+ staleHostSuppliedSources(carried) {
1670
+ const stale = [];
1671
+ for (const key of this.hostSuppliedKeys) {
1672
+ const held = this.fileCache.get(key);
1673
+ const abs = (0, path.resolve)(this.rootDir, key);
1674
+ if (held === void 0 || carried?.has(abs)) continue;
1675
+ let onDisk;
1676
+ try {
1677
+ onDisk = (0, fs.readFileSync)(abs, "utf-8");
1678
+ } catch {
1679
+ onDisk = null;
1680
+ }
1681
+ if (onDisk === null || require_pipeline.contentHash(onDisk) !== held.hash) stale.push(key);
1682
+ }
1683
+ return stale.sort();
1684
+ }
1685
+ /** `raw` is the attempt's input by original path, from which a
1686
+ * quarantined original stays held: every later pass re-ingests it, so it
1687
+ * keeps counting as skipped (nothing it renders is pruned) and its fix
1688
+ * edit compares against its bytes. */
1689
+ publishSourceIngestion(result, pass, raw) {
1690
+ const endedHold = this.heldRejection !== null;
1691
+ const previousOwnership = this.corpus.published.ownership;
1082
1692
  this.heldRejection = null;
1083
- this.fileCache = new Map(result.originalEntries.map((entry) => [entry.path, {
1693
+ const held = new Map(result.originalEntries.map((entry) => [entry.path, {
1084
1694
  hash: entry.hash,
1085
1695
  source: entry.source
1086
1696
  }]));
1697
+ for (const path$20 of result.skippedOriginals ?? []) {
1698
+ const entry = raw(path$20);
1699
+ if (entry) held.set(path$20, {
1700
+ hash: entry.hash ?? require_pipeline.contentHash(entry.source),
1701
+ source: entry.source
1702
+ });
1703
+ }
1704
+ this.fileCache = held;
1087
1705
  this.corpus.publish(result);
1706
+ const staged = this.stagedPublication;
1707
+ if (staged === null) return;
1708
+ this.stagedPublication = null;
1709
+ const previous = this.publication;
1710
+ const publication = {
1711
+ ...staged,
1712
+ ownership: result.ownership,
1713
+ externalPackageDirs: this.externalPackageDirs,
1714
+ externalSourceEntries: this.externalSourceEntries,
1715
+ externalSourceSideEffects: this.externalSourceSideEffects
1716
+ };
1717
+ this.publication = publication;
1718
+ setPublication(this.key, publication);
1719
+ this.onOutcome?.({
1720
+ kind: "published",
1721
+ publication,
1722
+ pass,
1723
+ planChanged: require_pipeline.diffFilePlans(require_pipeline.snapshotFilePlans(previous?.manifest ?? null), require_pipeline.snapshotFilePlans(publication.manifest)),
1724
+ endedHold,
1725
+ redeliver: endedHold ? Object.values(result.ownership).filter((owner) => previousOwnership[owner.originalPath]?.originalHash !== owner.originalHash).map((owner) => owner.originalPath) : []
1726
+ });
1088
1727
  }
1089
1728
  /** Full analysis + publication, registered in the one in-flight
1090
- * transaction slot; a nested reload leaves the enclosing registration. */
1091
- async runFullPipeline(pending = []) {
1729
+ * transaction slot; a nested reload leaves the enclosing registration.
1730
+ * `sources` are bytes a host already read (see WatchChanges.sources):
1731
+ * the full read uses them, and those of the observations it takes, in
1732
+ * place of the disk. */
1733
+ async runFullPipeline(pending = [], sources = /* @__PURE__ */ new Map()) {
1092
1734
  if (this.releasePublicationClaim === null) {
1093
- const releaseOwner = claimExclusiveSessionOwner(`${this.driverLabel}:${this.sessionDir}`);
1094
- (0, fs.mkdirSync)(this.sessionDir, { recursive: true });
1095
- const releaseClaimRecord = holdDirectoryClaim(this.sessionDir);
1096
- this.releasePublicationClaim = () => {
1097
- releaseOwner();
1098
- releaseClaimRecord();
1099
- };
1735
+ const releaseOwner = claimExclusiveSessionOwner(this.key, `${this.driverLabel}:${this.sessionDir}`);
1736
+ if (this.persistArtifacts) {
1737
+ (0, fs.mkdirSync)(this.sessionDir, { recursive: true });
1738
+ holdBuildClaimRecord(this.key, () => holdDirectoryClaim(this.sessionDir, { build: this.sessionId }));
1739
+ }
1740
+ this.releasePublicationClaim = releaseOwner;
1100
1741
  }
1101
1742
  const priorSystem = this.system;
1102
- const nested = getWatchTransaction() !== null;
1103
- const transaction = this.runPipelineTransaction(pending);
1104
- if (!nested) setWatchTransaction(transaction);
1743
+ const priorRegistration = this.snapshotWatchRegistration();
1744
+ this.attemptRecordedSystemDependencies = false;
1745
+ const nested = getWatchTransaction(this.key) !== null;
1746
+ const observations = nested ? null : this.takeObservations();
1747
+ const taken = observations?.taken ?? null;
1748
+ const transaction = this.runPipelineTransaction(pending, new Map([...observations?.sources ?? [], ...sources]));
1749
+ if (taken !== null) {
1750
+ setWatchTransaction(this.key, transaction);
1751
+ this.settleTaken(taken, transaction);
1752
+ }
1105
1753
  try {
1106
1754
  await transaction;
1107
1755
  } catch (error) {
1108
1756
  this.system = priorSystem;
1757
+ this.restoreWatchRegistration(priorRegistration);
1758
+ if (priorSystem !== null && (this.development || error instanceof AbortedParseRejection)) this.systemReloadOwed = true;
1109
1759
  throw error;
1110
1760
  } finally {
1111
- if (!nested) setWatchTransaction(null);
1761
+ if (!nested) setWatchTransaction(this.key, null);
1762
+ }
1763
+ this.systemReloadOwed = false;
1764
+ this.lastGoodSystemDependencies = {
1765
+ keys: this.systemDependencyKeys,
1766
+ paths: this.systemDependencyPaths,
1767
+ missing: this.systemMissingPaths,
1768
+ hashes: this.systemDependencyHashes
1769
+ };
1770
+ this.commitSystemAuthority();
1771
+ }
1772
+ /** This attempt's system dependencies: the files its load read, and the
1773
+ * paths it found absent, whose creation is a system change. */
1774
+ recordSystemDependencies(systemPath, dependencies, missing) {
1775
+ const keys = /* @__PURE__ */ new Set();
1776
+ for (const path$21 of [
1777
+ systemPath,
1778
+ ...dependencies,
1779
+ ...missing
1780
+ ]) for (const key of require_pipeline.toWatchKeys(path$21)) keys.add(key);
1781
+ const hashes = /* @__PURE__ */ new Map();
1782
+ for (const dep of dependencies.length > 0 ? dependencies : [systemPath]) try {
1783
+ hashes.set(dep, require_pipeline.contentHash((0, fs.readFileSync)(dep, "utf-8")));
1784
+ } catch {
1785
+ hashes.set(dep, "");
1786
+ }
1787
+ for (const path$22 of missing) hashes.set(path$22, "");
1788
+ this.systemDependencyKeys = keys;
1789
+ this.systemDependencyPaths = dependencies;
1790
+ this.systemMissingPaths = missing;
1791
+ this.systemDependencyHashes = hashes;
1792
+ this.attemptRecordedSystemDependencies = true;
1793
+ }
1794
+ /** Records the configuration inputs and, for a tsconfig-aliased host,
1795
+ * re-reads its aliases. */
1796
+ readConfigInputs(rootDir) {
1797
+ this.configInputPaths = [(0, path.join)(rootDir, "package.json"), ...require_pipeline.tsconfigChainPaths(rootDir)];
1798
+ this.configInputKeys = new Set(this.configInputPaths.flatMap((path$23) => require_pipeline.toWatchKeys(path$23)));
1799
+ this.configInputHashes = new Map(this.configInputPaths.map((path$24) => [path$24, fileHashOrEmpty(path$24)]));
1800
+ if (this.aliasesFromTsconfig) this.pathAliasesJson = require_pipeline.buildPathAliasesJson(require_pipeline.readTsconfigAliasPairs(rootDir), rootDir)?.json ?? null;
1801
+ }
1802
+ /** What hosts watch for assets, and what the last-good generation
1803
+ * reported about its kits and packages, which a failed full pass must
1804
+ * leave in place. */
1805
+ snapshotWatchRegistration() {
1806
+ return {
1807
+ assetDependencyPaths: new Set(this.assetDependencyPaths),
1808
+ assetDependencyKeys: new Set(this.assetDependencyKeys),
1809
+ kitDescriptors: this.kitDescriptors,
1810
+ lastExternalOutcomes: this.lastExternalOutcomes
1811
+ };
1812
+ }
1813
+ /** Restores the last-good registration. The system dependencies are the
1814
+ * last committed pass's and, when its load got that far, this failed
1815
+ * attempt's (spec §2.3): a fix to any of them must reach the owed reload,
1816
+ * and an earlier failure's are replaced, never accumulated. */
1817
+ restoreWatchRegistration(prior) {
1818
+ Object.assign(this, prior);
1819
+ const lastGood = this.lastGoodSystemDependencies;
1820
+ if (!this.attemptRecordedSystemDependencies) {
1821
+ this.systemDependencyKeys = lastGood.keys;
1822
+ this.systemDependencyPaths = lastGood.paths;
1823
+ this.systemMissingPaths = lastGood.missing;
1824
+ this.systemDependencyHashes = lastGood.hashes;
1825
+ return;
1112
1826
  }
1827
+ this.systemMissingPaths = [.../* @__PURE__ */ new Set([...lastGood.missing, ...this.systemMissingPaths])];
1828
+ this.systemDependencyKeys = /* @__PURE__ */ new Set([...lastGood.keys, ...this.systemDependencyKeys]);
1829
+ this.systemDependencyPaths = [.../* @__PURE__ */ new Set([...lastGood.paths, ...this.systemDependencyPaths])];
1830
+ this.systemDependencyHashes = new Map([...this.systemDependencyHashes, ...lastGood.hashes]);
1831
+ }
1832
+ /** The build's authority becomes the system this session committed, with
1833
+ * it and never before: a failed reload leaves the committed one, and the
1834
+ * context proofs made against it. A committed reload discards every
1835
+ * other context's proof, even for an unchanged system: the reload may
1836
+ * have changed what another context resolves, so each loads again. */
1837
+ commitSystemAuthority() {
1838
+ setSystemAuthority(this.key, {
1839
+ ...systemLoad(this.system),
1840
+ context: this.resolutionContextText(),
1841
+ label: this.resolutionContextLabel(),
1842
+ others: /* @__PURE__ */ new Map()
1843
+ });
1113
1844
  }
1114
- async runPipelineTransaction(pending) {
1845
+ async runPipelineTransaction(pending, hostSources) {
1115
1846
  const pipelineStart = this.now();
1116
1847
  const bt = {};
1117
1848
  this.runSessionStartHygiene();
1118
- setSessionArtifactDir(this.sessionDir);
1849
+ setSessionArtifactDir(this.key, this.sessionDir);
1119
1850
  const rootDir = this.rootDir;
1120
1851
  const priorSourceState = this.snapshotSourceState();
1121
1852
  const resolvedSystemPath = (0, path.resolve)(rootDir, this.options.system);
1853
+ this.readConfigInputs(rootDir);
1854
+ const context = {
1855
+ conditions: this.conditions,
1856
+ workspaces: require_pipeline.declaredWorkspacePackages(rootDir),
1857
+ extensions: this.extensionPolicy
1858
+ };
1859
+ this.log(`extension order: ${context.extensions.resolverVersion}`);
1122
1860
  let t = this.now();
1123
- this.system = require_pipeline.loadSystemConfig(engineApi, {
1124
- systemPath: resolvedSystemPath,
1125
- rootDir,
1126
- prefix: this.options.prefix,
1127
- prefixContextualVars: this.options.prefixContextualVars,
1128
- conditions: this.conditions
1129
- });
1861
+ try {
1862
+ this.system = this.loadSystemUnder(context);
1863
+ } catch (error) {
1864
+ const reads = require_pipeline.systemLoadReads(error);
1865
+ this.recordSystemDependencies(resolvedSystemPath, reads.dependencies, reads.missing);
1866
+ throw error;
1867
+ }
1130
1868
  this.assetCopyCache.clear();
1131
1869
  this.assetDependencyPaths.clear();
1132
1870
  this.assetDependencyKeys.clear();
1133
- {
1134
- const deps = this.system.dependencies ?? [];
1135
- const keys = /* @__PURE__ */ new Set();
1136
- for (const key of require_pipeline.toWatchKeys(resolvedSystemPath)) keys.add(key);
1137
- for (const dep of deps) for (const key of require_pipeline.toWatchKeys(dep)) keys.add(key);
1138
- this.systemDependencyKeys = keys;
1139
- this.systemDependencyPaths = deps;
1140
- const hashes = /* @__PURE__ */ new Map();
1141
- for (const dep of deps.length > 0 ? deps : [resolvedSystemPath]) try {
1142
- hashes.set(dep, require_pipeline.contentHash((0, fs.readFileSync)(dep, "utf-8")));
1143
- } catch {
1144
- hashes.set(dep, "");
1145
- }
1146
- this.systemDependencyHashes = hashes;
1147
- }
1871
+ this.recordSystemDependencies(resolvedSystemPath, this.system.dependencies ?? [], this.system.missing ?? []);
1148
1872
  bt.systemLoad = this.elapsed(t);
1149
1873
  t = this.now();
1150
- const { excludeMatcher, extensionsSet } = this.resolveScanConfig();
1874
+ const { excludeMatcher, extensionsSet } = this.scanConfig;
1151
1875
  const files = require_pipeline.discoverFiles(rootDir, rootDir, excludeMatcher, extensionsSet);
1876
+ if (claimUnmatchedExcludeReport(this.key)) for (const line of require_pipeline.unmatchedExcludeLines(excludeMatcher)) this.logger.error(line);
1152
1877
  bt.fileDiscovery = this.elapsed(t);
1153
1878
  t = this.now();
1154
1879
  const rawEntries = [];
1155
1880
  const ingestionFailures = [];
1881
+ const readPaths = /* @__PURE__ */ new Set();
1882
+ const supplied = /* @__PURE__ */ new Set();
1156
1883
  for (const filePath of files) {
1157
1884
  const relPath = (0, path.relative)(rootDir, filePath);
1885
+ readPaths.add(filePath);
1158
1886
  let source;
1159
1887
  try {
1160
- source = (0, fs.readFileSync)(filePath, "utf-8");
1888
+ const hostSource = hostSources.get(filePath);
1889
+ source = hostSource ?? (0, fs.readFileSync)(filePath, "utf-8");
1890
+ if (hostSource !== void 0) supplied.add(relPath);
1161
1891
  } catch (err) {
1162
1892
  if (!require_pipeline.isDeletedSource(err)) ingestionFailures.push(require_pipeline.unreadableSourceDiagnostic(relPath, err));
1163
1893
  continue;
@@ -1169,21 +1899,36 @@ var ExtractionSession = class {
1169
1899
  });
1170
1900
  }
1171
1901
  bt.fileRead = this.elapsed(t);
1902
+ const rawPaths = new Set(rawEntries.map((entry) => entry.path));
1172
1903
  t = this.now();
1173
- const discoveryDiagnostics = [];
1174
- const packageNames = await require_pipeline.extractSystemFilePackages(resolvedSystemPath, require_pipeline.engineModuleParser(engineApi()), (diagnostic) => discoveryDiagnostics.push(diagnostic), (name) => {
1175
- const entry = resolvePackagesByName(rootDir, [name])[name];
1176
- return entry ? (0, path.resolve)(rootDir, entry) : null;
1904
+ const resolver = require_pipeline.discoveryResolver(this.hostResolve, this.engine(), {
1905
+ rootDir,
1906
+ conditions: context.conditions,
1907
+ workspaces: context.workspaces,
1908
+ extensions: context.extensions
1177
1909
  });
1178
- discoveryDiagnostics.push(...require_pipeline.importedKitDiagnostics(rawEntries, engineApi(), rootDir, packageNames));
1179
- const preResolved = resolvePackagesByName(rootDir, packageNames);
1910
+ const discoveryDiagnostics = [];
1911
+ const systemPackageRoot = require_pipeline.findPackageRoot(resolvedSystemPath);
1912
+ const kitMembers = /* @__PURE__ */ new Map();
1913
+ const packageNames = await require_pipeline.extractSystemFilePackages(resolvedSystemPath, { systemMembership: this.engine().systemMembership ?? requireEngine().systemMembership }, (diagnostic) => discoveryDiagnostics.push(diagnostic), (specifier, importer) => require_pipeline.findPackageRoot(importer) === systemPackageRoot ? resolver.resolveSpecifier(specifier) : resolver.loaderResolve(specifier, (0, path.dirname)(importer)), context.extensions, kitMembers);
1914
+ discoveryDiagnostics.push(...require_pipeline.importedKitDiagnostics(rawEntries, this.engine(), rootDir, packageNames, (entries) => this.corpus.facts(entries)));
1180
1915
  const rawExternalFiles = /* @__PURE__ */ new Map();
1181
1916
  const collected = await require_pipeline.collectExternalPackageSources({
1917
+ extensions: context.extensions,
1918
+ kitSourceRoot: require_pipeline.nativeKitSourceRoot(this.engine(), rootDir, context.conditions, context.workspaces, require_pipeline.extensionPolicyJson(context.extensions)),
1919
+ locatePackage: require_pipeline.nativePackageLocator(this.engine(), context.workspaces),
1182
1920
  specifiers: packageNames,
1183
- resolveSpecifier: (name) => preResolved[name] ? (0, path.resolve)(rootDir, preResolved[name]) : null,
1921
+ resolveSpecifier: async (specifier) => kitMembers.get(specifier) ?? await resolver.resolveSpecifier(specifier),
1922
+ locateFrom: (specifier) => {
1923
+ const file = kitMembers.get(specifier);
1924
+ return file === void 0 ? rootDir : (0, path.dirname)(file);
1925
+ },
1926
+ unresolvedReason: resolver.unresolvedReason,
1927
+ requireSourceEntry: resolver.requireSourceEntry,
1184
1928
  rootDir,
1185
1929
  extensionsSet,
1186
- hasEntry: (relPath) => rawEntries.some((e) => e.path === relPath),
1930
+ hasEntry: (relPath) => rawPaths.has(relPath),
1931
+ hostOutputDirs: this.structuralExclude.map((dir) => (0, path.resolve)(rootDir, dir)),
1187
1932
  onSourceRead: (source, _relPath, absPath) => {
1188
1933
  rawExternalFiles.set(absPath, require_pipeline.contentHash(source));
1189
1934
  },
@@ -1202,21 +1947,22 @@ var ExtractionSession = class {
1202
1947
  });
1203
1948
  this.lastExternalOutcomes = collected.outcomes;
1204
1949
  this.ingestionFailureDiagnostics = ingestionFailures;
1950
+ if (resolver.hostFirst) discoveryDiagnostics.push(...systemInstanceMismatches({
1951
+ rootDir,
1952
+ specifiers: packageNames,
1953
+ instances: collected.packageInstances,
1954
+ evaluated: this.systemDependencyPaths,
1955
+ loaderResolve: (specifier) => resolver.loaderResolve(specifier, (0, path.dirname)(resolvedSystemPath))
1956
+ }));
1205
1957
  require_pipeline.surfaceManifestDiagnostics({ diagnostics: [...discoveryDiagnostics, ...collected.diagnostics] }, (message) => this.warn(message), {
1206
1958
  levels: this.options.diagnostics,
1207
1959
  strict: this.options.strict,
1208
1960
  reportErrors: this.reportErrors()
1209
1961
  });
1210
- const unresolvableMessage = require_pipeline.unresolvableIncludesMessage(collected.outcomes);
1211
- if (unresolvableMessage !== null) {
1212
- if (this.options.strict) this.strictFailure(unresolvableMessage);
1213
- else this.warn(unresolvableMessage);
1214
- }
1962
+ const unresolvableMessage = require_pipeline.unresolvableIncludesMessage(collected.outcomes, (0, path.relative)(rootDir, resolvedSystemPath));
1963
+ if (unresolvableMessage !== null) this.strictOrWarn(unresolvableMessage);
1215
1964
  const staleDistMessage = require_pipeline.staleDistIncludesMessage(collected.outcomes);
1216
- if (staleDistMessage !== null) {
1217
- if (this.options.strict) this.strictFailure(staleDistMessage);
1218
- else this.warn(staleDistMessage);
1219
- }
1965
+ if (staleDistMessage !== null) this.strictOrWarn(staleDistMessage);
1220
1966
  const rejectedSpecifiers = /* @__PURE__ */ new Set();
1221
1967
  const crossVolumeDetails = [];
1222
1968
  for (const [dir, specifiers] of Object.entries(collected.dirOwnerSets)) {
@@ -1233,6 +1979,7 @@ var ExtractionSession = class {
1233
1979
  for (const message of this.stickyDiagnostics.values()) this.warn(message);
1234
1980
  const admitted = require_pipeline.excludeCollectedPackages(collected, rejectedSpecifiers, rootDir);
1235
1981
  this.kitDescriptors = admitted.kitDescriptors;
1982
+ this.packageInstances = admitted.packageInstances;
1236
1983
  try {
1237
1984
  const identity = require_pipeline.createSourceIdentity(rootDir);
1238
1985
  this.externalRootOwners = /* @__PURE__ */ new Map();
@@ -1268,43 +2015,62 @@ var ExtractionSession = class {
1268
2015
  this.externalSourceEntries = admitted.sourceEntries;
1269
2016
  this.externalSourceSideEffects = admitted.sourceEntrySideEffects;
1270
2017
  for (const entry of admitted.entries) {
1271
- const hash = require_pipeline.contentHash(entry.source);
2018
+ const entryPath = (0, path.resolve)(rootDir, entry.path);
2019
+ readPaths.add(entryPath);
2020
+ const hostSource = hostSources.get(entryPath);
2021
+ if (hostSource !== void 0) supplied.add(entry.path);
2022
+ const source = hostSource ?? entry.source;
1272
2023
  rawEntries.push({
1273
2024
  path: entry.path,
1274
- source: entry.source,
1275
- hash
2025
+ source,
2026
+ hash: require_pipeline.contentHash(source)
1276
2027
  });
1277
2028
  }
1278
2029
  this.externalPackageDirs = admitted.packageDirs;
1279
2030
  this.externalLinkedDirs = admitted.linkedDirs;
1280
2031
  this.externalOutputDirs = admitted.outputDirs;
1281
2032
  bt.packageResolve = this.elapsed(t);
2033
+ const misspelled = misspelledHostSources(hostSources, readPaths).map(({ path: path$25, read }) => `${(0, path.relative)(rootDir, path$25)} (read as ${read.map((readPath) => (0, path.relative)(rootDir, readPath)).join(", ")})`);
2034
+ 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`);
1282
2035
  this.beginStatusAttempt();
1283
2036
  let accepted;
2037
+ const readEntries = this.withoutOutputSources(rawEntries);
1284
2038
  try {
1285
- accepted = await this.corpus.prepare(rawEntries);
2039
+ accepted = await this.corpus.prepare(readEntries);
1286
2040
  this.throwOnAbortedParse(accepted);
1287
2041
  this.skippedSources = accepted.skippedOriginals ?? [];
2042
+ this.ingestionOpenings = accepted.openings ?? [];
2043
+ this.ingestionAdapted = require_pipeline.adaptedOriginals(accepted.ownership);
1288
2044
  } catch (err) {
2045
+ if (err instanceof AbortedParseRejection) this.holdObservedEntries(readEntries, supplied);
1289
2046
  this.debouncePending.clear();
1290
2047
  this.writeAnalysisStatus("failed", pending, String(err));
1291
2048
  throw err;
1292
2049
  }
1293
2050
  this.externalFileOwners = require_pipeline.projectExternalFileOwners(accepted, this.externalFileOwners);
1294
2051
  bt.fileCount = accepted.analysisEntries.length;
1295
- require_pipeline.clearEngineCache(engineApi);
2052
+ require_pipeline.clearEngineCache(() => this.engine());
1296
2053
  await this.analyzeAndEmit(accepted.analysisEntries, packageMap, false, bt, pipelineStart, pending);
1297
- this.publishSourceIngestion(accepted);
1298
- setSharedExternalDirs(admitted.packageDirs);
1299
- setSharedExternalEntries(admitted.sourceEntries);
1300
- setSharedExternalSideEffects(admitted.sourceEntrySideEffects);
2054
+ this.publishSourceIngestion(accepted, "full", (path$26) => rawEntries.find((entry) => entry.path === path$26));
2055
+ this.hostSuppliedKeys = supplied;
1301
2056
  this.onExternalRootsCommitted?.(this.externalWatchRoots);
1302
- setOwningWatchSession(this);
2057
+ setOwningWatchSession(this.key, this);
1303
2058
  } catch (err) {
1304
2059
  this.restoreSourceState(priorSourceState);
1305
2060
  throw err;
1306
2061
  }
1307
2062
  }
2063
+ /** The full pass's analysis inputs, less any file whose real path lies in
2064
+ * an output directory, each reported. */
2065
+ withoutOutputSources(entries) {
2066
+ const inOutput = require_pipeline.outputSourceGuard(this.rootDir, this.structuralExclude);
2067
+ return entries.filter((entry) => {
2068
+ const outputDir = inOutput((0, path.resolve)(this.rootDir, entry.path));
2069
+ if (outputDir === null) return true;
2070
+ this.warn(require_pipeline.outputSourceMessage(entry.path, outputDir));
2071
+ return false;
2072
+ });
2073
+ }
1308
2074
  /** Per-generation source state captured as ONE object so the restore
1309
2075
  * cannot omit a member; deep-copied fields are mutated in place. */
1310
2076
  snapshotSourceState() {
@@ -1326,16 +2092,93 @@ var ExtractionSession = class {
1326
2092
  restoreSourceState(snapshot) {
1327
2093
  Object.assign(this, snapshot);
1328
2094
  }
1329
- /** Release this session's publication claim so a successor may publish.
1330
- * Idempotent, and scoped to this session's own claim. */
2095
+ /** The system under `context`: the one load path for the build's
2096
+ * authority and for a participant checking its own context. */
2097
+ loadSystemUnder(context) {
2098
+ return require_pipeline.loadSystemConfig(() => this.engine(), {
2099
+ systemPath: (0, path.resolve)(this.rootDir, this.options.system),
2100
+ rootDir: this.rootDir,
2101
+ prefix: this.options.prefix,
2102
+ prefixContextualVars: this.options.prefixContextualVars,
2103
+ conditions: context.conditions,
2104
+ workspaces: context.workspaces,
2105
+ extensions: context.extensions
2106
+ });
2107
+ }
2108
+ /** What the system loader reads of this participant besides the root, as
2109
+ * stable text: its conditions and extension order. */
2110
+ resolutionContextText() {
2111
+ return JSON.stringify([this.conditions, require_pipeline.extensionPolicyJson(this.extensionPolicy)]);
2112
+ }
2113
+ resolutionContextLabel() {
2114
+ return `${this.driverLabel} with conditions [${this.conditions.join(", ")}] and extension order ${this.extensionPolicy.resolverVersion}`;
2115
+ }
2116
+ /** One agreed system authority (SA5): a participant whose resolution
2117
+ * context differs from the one the build's system was loaded under loads
2118
+ * it once under its own, and proceeds only on the same system. Otherwise
2119
+ * it throws, naming both contexts, whichever participant came first. */
2120
+ agreeWithSystemAuthority() {
2121
+ const authority = getSystemAuthority(this.key);
2122
+ if (authority === null) return;
2123
+ const context = this.resolutionContextText();
2124
+ if (context === authority.context) return;
2125
+ let own = authority.others.get(context);
2126
+ if (own === void 0) {
2127
+ own = systemLoad(this.loadSystemUnder({
2128
+ conditions: this.conditions,
2129
+ workspaces: require_pipeline.declaredWorkspacePackages(this.rootDir),
2130
+ extensions: this.extensionPolicy
2131
+ }));
2132
+ authority.others.set(context, own);
2133
+ }
2134
+ if (own.identity === authority.identity) return;
2135
+ const shared = new Set(authority.dependencies);
2136
+ const mine = new Set(own.dependencies);
2137
+ const differing = [...authority.dependencies.filter((path$27) => !mine.has(path$27)), ...own.dependencies.filter((path$28) => !shared.has(path$28))].map((path$29) => (0, path.relative)(this.rootDir, path$29));
2138
+ 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.");
2139
+ }
2140
+ /** Release this session's publication claim so a successor may publish,
2141
+ * and leave the build, which retires with its last participant.
2142
+ * Idempotent; `reattach` joins the build again. */
1331
2143
  close() {
1332
2144
  this.releasePublicationClaim?.();
1333
2145
  this.releasePublicationClaim = null;
2146
+ this.hostSuppliedKeys.clear();
2147
+ if (this.joinedKey === null || this.participant === null) return;
2148
+ if (getOwningWatchSession(this.joinedKey) === this) setOwningWatchSession(this.joinedKey, null);
2149
+ leaveSession(this.joinedKey, this.participant);
2150
+ this.participant = null;
2151
+ this.detached = true;
2152
+ }
2153
+ /** A closed participant attaching again: its private analysis is stale,
2154
+ * so it is discarded, and it joins its build again. Idempotent. */
2155
+ reattach() {
2156
+ if (!this.detached) return;
2157
+ this.detached = false;
2158
+ this.system = null;
2159
+ this.joinedKey = null;
2160
+ }
2161
+ /** Brings a reattached participant up to date with a full rescan of a
2162
+ * live build, which reconciles deletions too; true then. A retired build
2163
+ * is false: the host starts it as at first. */
2164
+ async catchUp() {
2165
+ if (getManifestJson(this.key) === null) return false;
2166
+ await this.awaitObservation(this.recordRescan());
2167
+ return true;
2168
+ }
2169
+ /** Queues a full rescan under a new revision. It is never equivalent to
2170
+ * an active attempt: only a full read covers it. */
2171
+ recordRescan() {
2172
+ const queue = observationQueue(this.key);
2173
+ queue.rescan = true;
2174
+ const revision = ++queue.revision;
2175
+ queue.revisions.set(revision, null);
2176
+ return revision;
1334
2177
  }
1335
2178
  /** Reset analysis state for a system reload: write guards go back to null
1336
2179
  * and reseed from the disk envelopes on the next publication. */
1337
2180
  resetForHmr() {
1338
- resetAnalysisStartedPromise();
2181
+ resetAnalysisStartedPromise(this.key);
1339
2182
  this.artifactRecords = {
1340
2183
  manifest: null,
1341
2184
  inputs: null,
@@ -1346,16 +2189,16 @@ var ExtractionSession = class {
1346
2189
  /** The (sourceKey, observed hash) pairs of a watch batch — the status
1347
2190
  * file's pending set; loaders wait only on observed inputs. */
1348
2191
  pendingFromBatch(changes) {
1349
- const { excludeMatcher, extensionsSet } = this.resolveScanConfig();
2192
+ const { excludeMatcher, extensionsSet } = this.scanConfig;
1350
2193
  const pending = [];
1351
- for (const path$4 of changes.modifiedFiles ?? []) {
1352
- const classified = this.classifyWatchPath(path$4, {
2194
+ for (const path$30 of changes.modifiedFiles ?? []) {
2195
+ const classified = this.classifyWatchPath(path$30, {
1353
2196
  excludeMatcher,
1354
2197
  extensionsSet
1355
2198
  });
1356
2199
  if (!classified) continue;
1357
2200
  try {
1358
- pending.push([classified.key, require_pipeline.contentHash((0, fs.readFileSync)(path$4, "utf-8"))]);
2201
+ pending.push([classified.key, require_pipeline.contentHash(changes.sources?.get(path$30) ?? (0, fs.readFileSync)(path$30, "utf-8"))]);
1359
2202
  } catch {}
1360
2203
  }
1361
2204
  return pending;
@@ -1367,28 +2210,28 @@ var ExtractionSession = class {
1367
2210
  if (!identity) return changes;
1368
2211
  if (!changes.modifiedFiles && !changes.removedFiles) return changes;
1369
2212
  if (this.externalWatchRoots.length === 0) return changes;
1370
- const { extensionsSet } = this.resolveScanConfig();
2213
+ const { extensionsSet } = this.scanConfig;
1371
2214
  const dirtyRoots = /* @__PURE__ */ new Set();
1372
2215
  const modified = /* @__PURE__ */ new Set();
1373
2216
  const removed = /* @__PURE__ */ new Set();
1374
- const rootHitFor = (path$5) => {
1375
- const containing = identity.containingExternalRoot(path$5);
2217
+ const rootHitFor = (path$31) => {
2218
+ const containing = identity.containingExternalRoot(path$31);
1376
2219
  if (!containing) return null;
1377
2220
  try {
1378
- return (0, fs.statSync)(path$5).isDirectory() ? containing : null;
2221
+ return (0, fs.statSync)(path$31).isDirectory() ? containing : null;
1379
2222
  } catch {
1380
- return identity.resolveDeletedSourceId(path$5) ? null : containing;
2223
+ return identity.resolveDeletedSourceId(path$31) ? null : containing;
1381
2224
  }
1382
2225
  };
1383
- for (const path$6 of changes.modifiedFiles ?? []) {
1384
- const root = rootHitFor(path$6);
2226
+ for (const path$32 of changes.modifiedFiles ?? []) {
2227
+ const root = rootHitFor(path$32);
1385
2228
  if (root) dirtyRoots.add(root);
1386
- else modified.add(path$6);
2229
+ else modified.add(path$32);
1387
2230
  }
1388
- for (const path$7 of changes.removedFiles ?? []) {
1389
- const root = rootHitFor(path$7);
2231
+ for (const path$33 of changes.removedFiles ?? []) {
2232
+ const root = rootHitFor(path$33);
1390
2233
  if (root) dirtyRoots.add(root);
1391
- else removed.add(path$7);
2234
+ else removed.add(path$33);
1392
2235
  }
1393
2236
  for (const root of dirtyRoots) {
1394
2237
  const previous = this.externalInventory.get(root) ?? /* @__PURE__ */ new Map();
@@ -1405,13 +2248,14 @@ var ExtractionSession = class {
1405
2248
  } catch {
1406
2249
  continue;
1407
2250
  }
1408
- if (previous.get(resolved.sourceKey)?.hash !== rawHash) modified.add(abs);
2251
+ if (previous.get(resolved.sourceKey)?.hash !== rawHash || this.ingestionFailureDiagnostics.some((diagnostic) => diagnostic.file === resolved.sourceKey)) modified.add(abs);
1409
2252
  }
1410
2253
  for (const [key, entry] of previous) if (!seen.has(key)) removed.add(entry.abs);
1411
2254
  }
1412
2255
  return {
1413
2256
  modifiedFiles: modified,
1414
- removedFiles: removed
2257
+ removedFiles: removed,
2258
+ sources: changes.sources
1415
2259
  };
1416
2260
  }
1417
2261
  /** Route one watcher path: a project-root member (consumer excludes apply)
@@ -1478,33 +2322,40 @@ var ExtractionSession = class {
1478
2322
  packageMap,
1479
2323
  system,
1480
2324
  emitter: {
1481
- runtimeImport: "@animus-ui/system/runtime",
1482
- cssModuleId: ANIMUS_CSS_MODULE_ID,
1483
- systemPropsModuleId: this.systemPropsModuleId ?? systemPropsPath(this.sessionDir)
2325
+ runtimeImport: this.runtimeImport,
2326
+ cssModuleId: this.delivery.cssModuleId,
2327
+ systemPropsModuleId: this.delivery.systemPropsModuleId === SESSION_ARTIFACT ? systemPropsPath(this.sessionDir) : this.delivery.systemPropsModuleId
1484
2328
  },
1485
2329
  pathAliasesJson: this.pathAliasesJson,
1486
2330
  staticCssJson: this.staticCssJson,
1487
2331
  externalDirs: this.externalPackageDirs.map((dir) => (0, path.relative)(this.rootDir, dir)),
2332
+ rootDir: this.rootDir,
2333
+ assets: this.assetClassification,
2334
+ adapted: this.ingestionAdapted,
2335
+ loadExpander: this.loadExpander,
1488
2336
  devMode: this.engineDevMode(devMode),
1489
2337
  analysisContext: {
1490
2338
  skippedSources: [...this.skippedSources, ...this.ingestionFailureDiagnostics.map((d) => d.file)].map((file) => (0, path.isAbsolute)(file) ? (0, path.relative)(this.rootDir, file) : file),
1491
2339
  unbundledComputedImports: this.host.unbundledComputedImports,
2340
+ openings: [...this.ingestionOpenings],
1492
2341
  packageDirs: this.externalPackageDirs.map((dir) => (0, path.relative)(this.rootDir, dir)),
1493
2342
  linkedDirs: this.externalLinkedDirs.map((dir) => (0, path.relative)(this.rootDir, dir)),
1494
- outputDirs: this.externalOutputDirs.map((dir) => (0, path.relative)(this.rootDir, dir))
2343
+ outputDirs: this.externalOutputDirs.map((dir) => (0, path.relative)(this.rootDir, dir)),
2344
+ extensions: this.extensionPolicy
1495
2345
  }
1496
2346
  };
1497
2347
  this.writeAnalysisStatus("analyzing", pending);
1498
- const result = require_pipeline.runProjectAnalysis(engineApi, {
2348
+ const result = require_pipeline.runProjectAnalysis(() => this.engine(), {
1499
2349
  ...analysisOptions,
1500
2350
  warn: (message) => this.warn(message),
1501
2351
  info: (message) => this.log(message),
1502
2352
  strict: this.options.strict,
1503
2353
  diagnostics: this.options.diagnostics,
1504
2354
  reportErrors: this.reportErrors(),
1505
- extraDiagnostics: this.ingestionFailureDiagnostics,
2355
+ extraDiagnostics: [...this.ingestionFailureDiagnostics, ...await this.compareAliasesWithHost(fileEntries)],
1506
2356
  kitDescriptors: this.kitDescriptors
1507
2357
  });
2358
+ if (result.manifest) require_pipeline.assertRuntimeImportSuppliesTerminals(this.runtimeImport, result.manifest);
1508
2359
  require_pipeline.enforceExternalTokenContracts({
1509
2360
  diagnostics: result.manifest?.diagnostics,
1510
2361
  fileOwners: this.externalFileOwners,
@@ -1539,12 +2390,12 @@ var ExtractionSession = class {
1539
2390
  declaration,
1540
2391
  variables,
1541
2392
  require_pipeline.postProcessCss(body, {
1542
- minify: this.options.minify ?? require_pipeline.resolveMode(this.options.mode, () => process.env.NODE_ENV === "production" ? "production" : "development").mode === "production",
2393
+ minify: this.options.minify ?? this.startupMode === "production",
1543
2394
  targets: this.lcssTargets,
1544
2395
  warnFn: (msg) => this.warn(msg)
1545
2396
  })
1546
2397
  ].filter(Boolean).join("\n");
1547
- require_pipeline.reportSurvivingAssetPlaceholders(fullCss, {
2398
+ if (!this.hostSubstitutesAssets) require_pipeline.reportSurvivingAssetPlaceholders(fullCss, {
1548
2399
  strict: this.options.strict,
1549
2400
  warn: (message) => this.warn(message),
1550
2401
  reportErrors: this.reportErrors(),
@@ -1557,7 +2408,6 @@ var ExtractionSession = class {
1557
2408
  prefix: `[${this.driverLabel}]`,
1558
2409
  surface: "generated runtime modules"
1559
2410
  });
1560
- setSharedCss(fullCss);
1561
2411
  const systemPropsContent = require_pipeline.buildSystemPropsModule({
1562
2412
  systemPropMapJson: JSON.stringify(manifest?.system_prop_map ?? {}),
1563
2413
  groupRegistryJson: system.groupRegistryJson,
@@ -1565,9 +2415,6 @@ var ExtractionSession = class {
1565
2415
  admittedTransforms: manifest?.admitted_transforms ?? {},
1566
2416
  typedSystemProps: manifest?.typed_system_props ?? []
1567
2417
  });
1568
- setSharedSystemProps(systemPropsContent);
1569
- setManifestJson(result.manifestJson);
1570
- setAnalyzedHashes(new Map(fileEntries.map((entry) => [entry.path, entry.hash])));
1571
2418
  this.writeAnalysisStatus("committing", pending);
1572
2419
  const epoch = require_pipeline.hashReplacementPlans(require_pipeline.snapshotFilePlans(manifest), systemPropsContent);
1573
2420
  if (this.lastCommit === null) this.lastCommit = this.seedCommitFromDisk();
@@ -1587,6 +2434,19 @@ var ExtractionSession = class {
1587
2434
  } else this.writeSessionArtifact(SYSTEM_PROPS_ARTIFACT, systemPropsContent);
1588
2435
  this.publishAnalysisCommit(epoch, generation);
1589
2436
  this.publishReplacementEpoch(epoch);
2437
+ this.stagedPublication = {
2438
+ manifest,
2439
+ manifestJson: result.manifestJson,
2440
+ analysisGlobalCss: sheets.globalCss,
2441
+ analysisComponentCss: sheets.componentCss,
2442
+ styles: fullCss,
2443
+ systemPropsModule: systemPropsContent,
2444
+ system,
2445
+ packageMap,
2446
+ analyzedHashes: new Map(fileEntries.map((entry) => [entry.path, entry.hash])),
2447
+ packageReadRecord: this.packageReadRecord,
2448
+ replacementEpoch: epoch
2449
+ };
1590
2450
  bt.total = this.elapsed(pipelineStart);
1591
2451
  logBuildTimings(bt, manifest?.timing, (msg) => this.log(msg), this.verbose);
1592
2452
  }
@@ -1600,6 +2460,86 @@ var ExtractionSession = class {
1600
2460
  }
1601
2461
  /** Write the session's analysis-status artifact. Terminal states
1602
2462
  * (idle/failed) close the attempt. */
2463
+ /** Where the host resolves a path alias the analysis reads to another
2464
+ * file (spec §2.2 A7, report only), and how the two were compared. */
2465
+ async compareAliasesWithHost(fileEntries) {
2466
+ const record = require_pipeline.packageReadRecord({
2467
+ rootDir: this.rootDir,
2468
+ roots: this.packageInstances,
2469
+ redirected: new Set(this.externalSourceEntries.keys()),
2470
+ analysed: fileEntries.map((entry) => entry.path),
2471
+ rendersBuilder: (file) => this.rendersBuilder(file)
2472
+ });
2473
+ this.packageReadRecord = record;
2474
+ const { moduleSpecifiers } = this.engine();
2475
+ if (this.host.noBundler) return [];
2476
+ if (this.hostResolve === null || moduleSpecifiers === void 0) {
2477
+ this.log("path aliases: no host resolver, comparison unavailable");
2478
+ return [];
2479
+ }
2480
+ try {
2481
+ const { findings, unavailable } = await require_pipeline.compareAliases({
2482
+ rootDir: this.rootDir,
2483
+ fileEntries,
2484
+ pathAliasesJson: this.pathAliasesJson,
2485
+ order: this.extensionPolicy,
2486
+ readings: {
2487
+ isAsset: this.assetClassification.isAsset,
2488
+ adapted: this.ingestionAdapted
2489
+ },
2490
+ moduleSpecifiers,
2491
+ resolve: this.hostResolve,
2492
+ cache: this.aliasCache,
2493
+ packages: record
2494
+ });
2495
+ for (const finding of findings) record.reported.add(`${finding.file}\0${finding.component}`);
2496
+ if (unavailable > 0) this.log(`path aliases: ${unavailable} aliased import(s) the host's resolver failed on are unavailable, not agreeing`);
2497
+ return findings;
2498
+ } catch (error) {
2499
+ this.log(`path aliases: comparison unavailable (${String(error)})`);
2500
+ return [];
2501
+ }
2502
+ }
2503
+ /** Whether the file at a real path declares builder chains, by the
2504
+ * engine's own parse: a module that does, served unread, renders with
2505
+ * empty classes. One the engine cannot read counts as one that does. */
2506
+ rendersBuilder(file) {
2507
+ if (!require_pipeline.isEngineTransformExtension(file)) return false;
2508
+ const { extractFacts } = this.engine();
2509
+ if (extractFacts === void 0) return true;
2510
+ const path$34 = (0, path.relative)(this.rootDir, file);
2511
+ try {
2512
+ const facts = require_pipeline.parseInternalWire(extractFacts(JSON.stringify([{
2513
+ path: path$34,
2514
+ source: (0, fs.readFileSync)(file, "utf-8")
2515
+ }])), "extractFacts").files[path$34];
2516
+ return facts === void 0 || facts.parsePanicked || facts.chains.length > 0;
2517
+ } catch {
2518
+ return true;
2519
+ }
2520
+ }
2521
+ /**
2522
+ * An import the host resolved while bundling (spec §2.2 A4), reported as
2523
+ * the comparison reports it: webpack resolves each module's requests with
2524
+ * the resolve options of the rules that module matches, which its
2525
+ * resolver, asked outside a compilation, does not apply. `importer` and
2526
+ * `resolved` are absolute. A finding the comparison already reported is
2527
+ * not repeated; an error-level one throws in a build.
2528
+ */
2529
+ reportHostResolution(importer, request, resolved) {
2530
+ const record = getSharedPackageReadRecord(this.key);
2531
+ if (record === null) return;
2532
+ const finding = require_pipeline.packageInstanceFinding(record, (0, path.relative)(record.rootDir, importer).split(path.sep).join(path.posix.sep), request, resolved);
2533
+ if (finding === null) return;
2534
+ const key = `${finding.file}\0${finding.component}`;
2535
+ if (record.reported.has(key)) return;
2536
+ record.reported.add(key);
2537
+ require_pipeline.surfaceManifestDiagnostics({ diagnostics: [finding] }, (message) => this.warn(message), {
2538
+ levels: this.options.diagnostics,
2539
+ strict: this.options.strict,
2540
+ reportErrors: this.reportErrors()
2541
+ });
2542
+ }
1603
2543
  writeAnalysisStatus(state, pending, diagnostic) {
1604
2544
  const status = {
1605
2545
  schema: 2,
@@ -1709,6 +2649,10 @@ var ExtractionSession = class {
1709
2649
  /** Maintain the session's epoch witness, rewritten ONLY when the value
1710
2650
  * changes, then expose it through the singleton. */
1711
2651
  publishReplacementEpoch(epoch) {
2652
+ if (!this.persistArtifacts) {
2653
+ this.lastEpochValue = epoch;
2654
+ return;
2655
+ }
1712
2656
  const onDisk = this.diskEpochValue();
1713
2657
  if (this.lastEpochValue === null) this.lastEpochValue = onDisk;
1714
2658
  if (epoch !== onDisk) this.writeSessionArtifact(REPLACEMENT_EPOCH_ARTIFACT, JSON.stringify({
@@ -1720,7 +2664,6 @@ var ExtractionSession = class {
1720
2664
  this.lastEpochValue = epoch;
1721
2665
  this.reconcileSiblingEpochs(epoch);
1722
2666
  }
1723
- setReplacementEpoch(epoch);
1724
2667
  }
1725
2668
  /** sessions-root listing memo, keyed by the root dir's mtime — a new or
1726
2669
  * pruned sibling DIRECTORY moves it; agreeing siblings stay listed. */
@@ -1765,10 +2708,14 @@ var ExtractionSession = class {
1765
2708
  }
1766
2709
  }
1767
2710
  /** The pid of `siblingDir`'s live owner, or null when nothing proves one.
1768
- * Our own pid is never one — publication is process-exclusive. A missing,
1769
- * undecodable or unreadable claim all mean no owner: the opposite default
1770
- * from the CLI lock check on purpose, so do not unify the two. */
2711
+ * A tree whose build is registered in this process is ours and live,
2712
+ * claim or no claim: a publisher's close() releases the claim while the
2713
+ * build's other participants still serve from it. Otherwise a missing,
2714
+ * undecodable or unreadable claim, or one naming our own pid, means no
2715
+ * owner: the opposite default from the CLI lock check on purpose, so do
2716
+ * not unify the two. */
1771
2717
  liveSiblingOwnerPid(siblingDir) {
2718
+ if (findSession(sessionKey((0, path.basename)(siblingDir), this.rootDir)) !== void 0) return process.pid;
1772
2719
  let claim;
1773
2720
  try {
1774
2721
  claim = readCliLockRecord(siblingDir);
@@ -1799,7 +2746,7 @@ var ExtractionSession = class {
1799
2746
  /** Delete legacy flat artifacts and prune sibling session directories past
1800
2747
  * the retention window — never this session's own dir. */
1801
2748
  runSessionStartHygiene() {
1802
- if (this.sessionStartHygieneDone) return;
2749
+ if (this.sessionStartHygieneDone || !this.persistArtifacts) return;
1803
2750
  this.sessionStartHygieneDone = true;
1804
2751
  const animusDir = (0, path.join)(this.rootDir, ANIMUS_ARTIFACT_DIR);
1805
2752
  const isCliPublishedSet = this.cliWriterHoldsLock(animusDir) || verifyCommitRecord(animusDir).length === 0;
@@ -1840,13 +2787,29 @@ var ExtractionSession = class {
1840
2787
  /** Write one artifact write-then-rename, so a cross-process reader never
1841
2788
  * sees a torn file; the tmp name carries the pid to keep writers apart. */
1842
2789
  writeSessionArtifact(name, content) {
1843
- const dir = this.sessionDir;
1844
- (0, fs.mkdirSync)(dir, { recursive: true });
1845
- const tmpPath = (0, path.join)(dir, `.${name}.${process.pid}.tmp`);
1846
- (0, fs.writeFileSync)(tmpPath, content);
1847
- (0, fs.renameSync)(tmpPath, (0, path.join)(dir, name));
2790
+ if (this.persistArtifacts) {
2791
+ const dir = this.sessionDir;
2792
+ (0, fs.mkdirSync)(dir, { recursive: true });
2793
+ const tmpPath = (0, path.join)(dir, `.${name}.${process.pid}.tmp`);
2794
+ (0, fs.writeFileSync)(tmpPath, content);
2795
+ (0, fs.renameSync)(tmpPath, (0, path.join)(dir, name));
2796
+ }
1848
2797
  this.onArtifactWrite?.(name, content);
1849
2798
  }
2799
+ /** How an asset's bytes are fingerprinted, of a file or of bytes read:
2800
+ * the engine's fast fingerprint where it has both, the content hash
2801
+ * otherwise, so the two always agree. */
2802
+ assetFingerprints() {
2803
+ const { fingerprintFile, fingerprintBytes } = this.engine();
2804
+ if (fingerprintFile && fingerprintBytes) return {
2805
+ ofFile: (path$35) => fingerprintFile(path$35) ?? require_pipeline.contentHash((0, fs.readFileSync)(path$35)),
2806
+ ofBytes: fingerprintBytes
2807
+ };
2808
+ return {
2809
+ ofFile: (path$36) => require_pipeline.contentHash((0, fs.readFileSync)(path$36)),
2810
+ ofBytes: require_pipeline.contentHash
2811
+ };
2812
+ }
1850
2813
  /** Copy each asset() specifier's bytes, from every sheet, into `assets/`
1851
2814
  * under a content-hashed name and substitute a url relative to
1852
2815
  * styles.css. */
@@ -1856,12 +2819,12 @@ var ExtractionSession = class {
1856
2819
  const expected = /* @__PURE__ */ new Set();
1857
2820
  this.assetDependencyPaths.clear();
1858
2821
  this.assetDependencyKeys.clear();
2822
+ if (this.hostSubstitutesAssets) return sheets;
1859
2823
  const urlBySpecifier = /* @__PURE__ */ new Map();
1860
2824
  for (const specifier of specifiers) {
1861
2825
  const cached = this.assetCopyCache.get(specifier);
1862
2826
  if (cached && (0, fs.existsSync)((0, path.join)(assetsDir, cached.fileName))) try {
1863
- const current = (0, fs.statSync)(cached.sourcePath);
1864
- if (current.mtimeMs === cached.mtimeMs && current.size === cached.size) {
2827
+ if (this.assetFingerprints().ofFile(cached.sourcePath) === cached.fingerprint) {
1865
2828
  this.trackAssetDependency(cached.sourcePath);
1866
2829
  expected.add(cached.fileName);
1867
2830
  urlBySpecifier.set(specifier, cached.url);
@@ -1870,37 +2833,32 @@ var ExtractionSession = class {
1870
2833
  } catch {}
1871
2834
  const resolvedPath = this.resolveAssetSpecifier(specifier);
1872
2835
  if (!resolvedPath) {
1873
- const message = `unresolvable asset() specifier: ${specifier}`;
1874
- if (this.options.strict) this.strictFailure(`[${this.driverLabel}] ${message}`);
1875
- else this.warn(message);
2836
+ this.reportUnresolvableAsset(specifier);
1876
2837
  urlBySpecifier.set(specifier, specifier);
1877
2838
  continue;
1878
2839
  }
1879
- const bytes = (0, fs.readFileSync)(resolvedPath);
1880
- const sourceStat = (0, fs.statSync)(resolvedPath);
2840
+ const { fileName, bytes } = require_pipeline.writeAssetCopy(resolvedPath, assetsDir);
1881
2841
  this.trackAssetDependency(resolvedPath);
1882
- const ext = (0, path.extname)(resolvedPath);
1883
- const fileName = `${(0, path.basename)(resolvedPath, ext)}.${require_pipeline.contentHash(bytes).slice(0, 8)}${ext}`;
1884
2842
  expected.add(fileName);
1885
- if (!(0, fs.existsSync)(assetsDir)) (0, fs.mkdirSync)(assetsDir, { recursive: true });
1886
- const assetPath = (0, path.join)(assetsDir, fileName);
1887
- if (!(0, fs.existsSync)(assetPath)) (0, fs.writeFileSync)(assetPath, bytes);
1888
2843
  const url = `./${SESSION_ASSETS_DIR}/${fileName}`;
1889
2844
  urlBySpecifier.set(specifier, url);
1890
2845
  this.assetCopyCache.set(specifier, {
1891
2846
  sourcePath: resolvedPath,
1892
- mtimeMs: sourceStat.mtimeMs,
1893
- size: sourceStat.size,
2847
+ fingerprint: this.assetFingerprints().ofBytes(bytes),
1894
2848
  fileName,
1895
2849
  url
1896
2850
  });
1897
2851
  }
1898
- if (pruneSuperseded) pruneStaleAssets(assetsDir, expected);
2852
+ if (pruneSuperseded) require_pipeline.pruneStaleAssets(assetsDir, expected);
1899
2853
  return require_pipeline.substituteSheetAssets(sheets, urlBySpecifier);
1900
2854
  }
1901
- trackAssetDependency(path$8) {
1902
- this.assetDependencyPaths.add(path$8);
1903
- for (const key of require_pipeline.toWatchKeys(path$8)) this.assetDependencyKeys.add(key);
2855
+ reportUnresolvableAsset(specifier) {
2856
+ const message = `unresolvable asset() specifier: ${specifier}`;
2857
+ this.strictOrWarn(message, `[${this.driverLabel}] ${message}`);
2858
+ }
2859
+ trackAssetDependency(path$37) {
2860
+ this.assetDependencyPaths.add(path$37);
2861
+ for (const key of require_pipeline.toWatchKeys(path$37)) this.assetDependencyKeys.add(key);
1904
2862
  }
1905
2863
  /** Resolve an asset specifier through the shared resolver, with one
1906
2864
  * last resort: a discovered source entry's package root. */
@@ -1919,30 +2877,17 @@ var ExtractionSession = class {
1919
2877
  return null;
1920
2878
  }
1921
2879
  };
1922
- /** Sync `assets/` to the current build's file set; per-entry failures are
1923
- * tolerated — a concurrent session may have removed the same file. */
1924
- function pruneStaleAssets(assetsDir, expected) {
1925
- if (!(0, fs.existsSync)(assetsDir)) return;
1926
- let entries;
1927
- try {
1928
- entries = (0, fs.readdirSync)(assetsDir);
1929
- } catch {
1930
- return;
1931
- }
1932
- for (const entry of entries) {
1933
- if (expected.has(entry)) continue;
1934
- try {
1935
- (0, fs.unlinkSync)((0, path.join)(assetsDir, entry));
1936
- } catch {}
1937
- }
1938
- }
1939
2880
  //#endregion
1940
2881
  //#region session/turbopack-orchestrator.ts
1941
2882
  async function runSessionPipeline(session) {
1942
- session.systemPropsModuleId = TURBOPACK_SYSTEM_PROPS_ID;
2883
+ session.delivery = {
2884
+ ...session.delivery,
2885
+ systemPropsModuleId: TURBOPACK_SYSTEM_PROPS_ID
2886
+ };
1943
2887
  session.persistAnalysisInputs = true;
1944
2888
  await session.runFullPipeline();
1945
2889
  }
2890
+ /** Each build's watched root: another build of the root watches it too. */
1946
2891
  const activeWatcherRoots = /* @__PURE__ */ new Set();
1947
2892
  const IGNORED_SEGMENTS = /* @__PURE__ */ new Set([
1948
2893
  ANIMUS_ARTIFACT_DIR,
@@ -1969,17 +2914,18 @@ function isIntrinsicString(value) {
1969
2914
  }
1970
2915
  function startTurbopackWatcher(session, rootDir, options = {}) {
1971
2916
  const { debounceMs = 75, watchFn = fs.watch, holdEvents = false } = options;
1972
- if (activeWatcherRoots.has(rootDir)) return { kind: "already-watched" };
1973
- activeWatcherRoots.add(rootDir);
2917
+ const watched = JSON.stringify([session.sessionId, rootDir]);
2918
+ if (activeWatcherRoots.has(watched)) return { kind: "already-watched" };
2919
+ activeWatcherRoots.add(watched);
1974
2920
  session.debounceCeilingMs = debounceMs;
1975
2921
  const pendingPaths = /* @__PURE__ */ new Set();
1976
2922
  const watchers = /* @__PURE__ */ new Map();
1977
- let timer = null;
1978
2923
  let updateChain = Promise.resolve();
1979
2924
  let closed = false;
1980
2925
  let died = false;
1981
2926
  let holdingEvents = holdEvents;
1982
2927
  const externalWatchers = /* @__PURE__ */ new Map();
2928
+ const dependencyWatchers = /* @__PURE__ */ new Map();
1983
2929
  let pendingOpened = /* @__PURE__ */ new Map();
1984
2930
  let capturedDuringSnapshot = [];
1985
2931
  const externalDiagnosticKey = (root) => `external-watch:${root}`;
@@ -1998,24 +2944,28 @@ function startTurbopackWatcher(session, rootDir, options = {}) {
1998
2944
  session.stickyDiagnostics?.set(externalDiagnosticKey(root), message);
1999
2945
  console.warn(`[animus-extract] ${message}`);
2000
2946
  };
2947
+ let cyclesInFlight = 0;
2001
2948
  const flush = () => {
2002
- timer = null;
2003
2949
  const modifiedFiles = /* @__PURE__ */ new Set();
2004
2950
  const removedFiles = /* @__PURE__ */ new Set();
2005
2951
  for (const path$1 of pendingPaths) ((0, fs.existsSync)(path$1) ? modifiedFiles : removedFiles).add(path$1);
2006
2952
  pendingPaths.clear();
2007
- updateChain = updateChain.then(() => {
2008
- if (closed) return;
2009
- return session.handleWatchUpdate({
2010
- modifiedFiles,
2011
- removedFiles
2012
- }).catch((err) => {
2013
- console.warn(`[animus-extract] watch update failed: ${String(err)}`);
2014
- }).then(() => {
2015
- if (pendingOpened.size > 0 || capturedDuringSnapshot.length > 0) rollbackPendingExternal();
2016
- });
2953
+ if (closed) return;
2954
+ cyclesInFlight++;
2955
+ const cycle = session.handleWatchUpdate({
2956
+ modifiedFiles,
2957
+ removedFiles
2958
+ }).catch((err) => {
2959
+ console.warn(`[animus-extract] watch update failed: ${String(err)}`);
2960
+ }).finally(() => {
2961
+ cyclesInFlight--;
2962
+ if (cyclesInFlight > 0) return;
2963
+ if (pendingOpened.size > 0 || capturedDuringSnapshot.length > 0) rollbackPendingExternal();
2964
+ syncDependencyWatchers();
2017
2965
  });
2966
+ updateChain = updateChain.then(() => cycle);
2018
2967
  };
2968
+ const coalescing = new CoalescingWindow(flush, debounceMs);
2019
2969
  const rollbackPendingExternal = () => {
2020
2970
  for (const watcher of pendingOpened.values()) watcher.close();
2021
2971
  pendingOpened = /* @__PURE__ */ new Map();
@@ -2024,16 +2974,17 @@ function startTurbopackWatcher(session, rootDir, options = {}) {
2024
2974
  const closeAll = () => {
2025
2975
  if (closed) return;
2026
2976
  closed = true;
2027
- if (timer) clearTimeout(timer);
2028
- timer = null;
2977
+ coalescing.cancel();
2029
2978
  for (const watcher of watchers.values()) watcher.close();
2030
2979
  watchers.clear();
2980
+ for (const { watcher } of dependencyWatchers.values()) watcher.close();
2981
+ dependencyWatchers.clear();
2031
2982
  for (const watcher of externalWatchers.values()) watcher.close();
2032
2983
  externalWatchers.clear();
2033
2984
  rollbackPendingExternal();
2034
2985
  session.onExternalRootResolved = null;
2035
2986
  session.onExternalRootsCommitted = null;
2036
- activeWatcherRoots.delete(rootDir);
2987
+ activeWatcherRoots.delete(watched);
2037
2988
  };
2038
2989
  const onWatcherError = (err) => {
2039
2990
  died = true;
@@ -2049,10 +3000,7 @@ function startTurbopackWatcher(session, rootDir, options = {}) {
2049
3000
  } catch (err) {
2050
3001
  console.warn(`[animus-extract] Turbopack debounce status update failed: ${String(err)}`);
2051
3002
  }
2052
- if (!timer) {
2053
- timer = setTimeout(flush, debounceMs);
2054
- timer.unref?.();
2055
- }
3003
+ coalescing.note();
2056
3004
  };
2057
3005
  const onEvent = (baseDir, filename) => {
2058
3006
  if (closed || !filename) return;
@@ -2096,6 +3044,57 @@ function startTurbopackWatcher(session, rootDir, options = {}) {
2096
3044
  degradeExternalRoot(root, err);
2097
3045
  }
2098
3046
  };
3047
+ /** Whether the root or an external root watcher already reports `path`. */
3048
+ const watchedByRoot = (path$2) => {
3049
+ if (require_pipeline.isPathWithinRoot(rootDir, path$2) && !(0, path.relative)(rootDir, path$2).split(/[\\/]/).some((segment) => IGNORED_SEGMENTS.has(segment))) return true;
3050
+ return [...externalWatchers.keys()].some((root) => require_pipeline.isPathWithinRoot(root, path$2));
3051
+ };
3052
+ /** Watches the Session's plan (its files, and the absent paths whose
3053
+ * creation reloads the system) where no root watcher reports them, by
3054
+ * their directories, for those paths only. */
3055
+ const syncDependencyWatchers = () => {
3056
+ if (closed) return;
3057
+ const wanted = /* @__PURE__ */ new Map();
3058
+ const plan = session.watchPlan();
3059
+ for (const path$3 of [...plan.files, ...plan.missing]) {
3060
+ if (watchedByRoot(path$3)) continue;
3061
+ const files = wanted.get((0, path.dirname)(path$3)) ?? /* @__PURE__ */ new Set();
3062
+ files.add(path$3);
3063
+ wanted.set((0, path.dirname)(path$3), files);
3064
+ }
3065
+ for (const [dir, { watcher }] of [...dependencyWatchers]) {
3066
+ if (wanted.has(dir)) continue;
3067
+ watcher.close();
3068
+ dependencyWatchers.delete(dir);
3069
+ }
3070
+ for (const [dir, files] of wanted) {
3071
+ const existing = dependencyWatchers.get(dir);
3072
+ if (existing) {
3073
+ existing.files = files;
3074
+ continue;
3075
+ }
3076
+ try {
3077
+ const watcher = watchFn(dir, { recursive: false }, (_event, name) => {
3078
+ if (closed || !name) return;
3079
+ const abs = (0, path.join)(dir, name.toString());
3080
+ if (dependencyWatchers.get(dir)?.files.has(abs)) enqueuePath(abs);
3081
+ });
3082
+ watcher.on("error", (err) => {
3083
+ watcher.close();
3084
+ dependencyWatchers.delete(dir);
3085
+ console.warn(`[animus-extract] dependency watch on ${dir} failed (${String(err)}); its changes require a dev-server restart`);
3086
+ });
3087
+ watcher.unref?.();
3088
+ dependencyWatchers.set(dir, {
3089
+ watcher,
3090
+ files
3091
+ });
3092
+ } catch (err) {
3093
+ if (errnoCode(err) === "ENOENT") continue;
3094
+ console.warn(`[animus-extract] dependency watch on ${dir} unavailable (${String(err)}); its changes require a dev-server restart`);
3095
+ }
3096
+ }
3097
+ };
2099
3098
  const addWatcher = (dir, recursive) => {
2100
3099
  const watcher = watchFn(dir, { recursive }, (_event, filename) => onEvent(dir, filename));
2101
3100
  watcher.on("error", onWatcherError);
@@ -2114,6 +3113,7 @@ function startTurbopackWatcher(session, rootDir, options = {}) {
2114
3113
  return { kind: "unavailable" };
2115
3114
  }
2116
3115
  for (const root of session.externalWatchRoots ?? []) openExternalWatcher(root, externalWatchers);
3116
+ syncDependencyWatchers();
2117
3117
  session.onExternalRootResolved = (root) => {
2118
3118
  if (closed) return;
2119
3119
  if (externalWatchers.has(root) || pendingOpened.has(root)) return;
@@ -2138,7 +3138,8 @@ function startTurbopackWatcher(session, rootDir, options = {}) {
2138
3138
  onDied: null,
2139
3139
  deliverHeldEvents: () => {
2140
3140
  holdingEvents = false;
2141
- if (pendingPaths.size > 0) flush();
3141
+ syncDependencyWatchers();
3142
+ if (pendingPaths.size > 0) coalescing.flush();
2142
3143
  return updateChain;
2143
3144
  },
2144
3145
  settle: () => updateChain
@@ -2155,19 +3156,29 @@ exports.CLI_COMMIT_ARTIFACT = CLI_COMMIT_ARTIFACT;
2155
3156
  exports.CLI_LOCK_ARTIFACT = CLI_LOCK_ARTIFACT;
2156
3157
  exports.CLI_LOCK_HEARTBEAT_INTERVAL_MS = CLI_LOCK_HEARTBEAT_INTERVAL_MS;
2157
3158
  exports.CLI_LOCK_STALE_AFTER_MS = CLI_LOCK_STALE_AFTER_MS;
3159
+ exports.COALESCING_WINDOW_MS = COALESCING_WINDOW_MS;
3160
+ exports.CoalescingWindow = CoalescingWindow;
3161
+ exports.EnvironmentBuilds = EnvironmentBuilds;
3162
+ exports.EnvironmentFailure = EnvironmentFailure;
2158
3163
  exports.ExtractionSession = ExtractionSession;
2159
3164
  exports.MANIFEST_ARTIFACT = MANIFEST_ARTIFACT;
3165
+ exports.SESSION_ARTIFACT = SESSION_ARTIFACT;
2160
3166
  exports.SESSION_ASSETS_DIR = SESSION_ASSETS_DIR;
2161
3167
  exports.STYLES_ARTIFACT = STYLES_ARTIFACT;
2162
3168
  exports.SYSTEM_PROPS_ARTIFACT = SYSTEM_PROPS_ARTIFACT;
3169
+ exports.SessionConfigurationConflict = SessionConfigurationConflict;
2163
3170
  exports.TURBOPACK_SYSTEM_PROPS_ID = TURBOPACK_SYSTEM_PROPS_ID;
2164
3171
  exports.analysisCommitPath = analysisCommitPath;
2165
3172
  exports.analysisInputsPath = analysisInputsPath;
2166
3173
  exports.analysisStatusPath = analysisStatusPath;
2167
3174
  exports.checkLockLiveness = checkLockLiveness;
2168
3175
  exports.collectSessionAssets = collectSessionAssets;
3176
+ exports.createBuildId = createBuildId;
2169
3177
  exports.decodeCommitRecord = decodeCommitRecord;
2170
3178
  exports.engineApi = engineApi;
3179
+ exports.engineFactory = engineFactory;
3180
+ exports.engineHandle = engineHandle;
3181
+ exports.findSession = findSession;
2171
3182
  exports.getAnalysisStartedPromise = getAnalysisStartedPromise;
2172
3183
  exports.getAnalyzedHashes = getAnalyzedHashes;
2173
3184
  exports.getManifestJson = getManifestJson;
@@ -2180,17 +3191,20 @@ exports.getSharedExternalSideEffects = getSharedExternalSideEffects;
2180
3191
  exports.getSharedSystemProps = getSharedSystemProps;
2181
3192
  exports.holdDirectoryClaim = holdDirectoryClaim;
2182
3193
  exports.isLockHolderAlive = isLockHolderAlive;
3194
+ exports.joinSession = joinSession;
2183
3195
  exports.lockRecordBytes = lockRecordBytes;
2184
3196
  exports.manifestPath = manifestPath;
2185
- exports.pruneStaleAssets = pruneStaleAssets;
3197
+ exports.pruneStaleAssets = require_pipeline.pruneStaleAssets;
2186
3198
  exports.readCliLockRecord = readCliLockRecord;
2187
3199
  exports.readJsonEnvelope = readJsonEnvelope;
3200
+ exports.releaseEngineHandle = releaseEngineHandle;
2188
3201
  exports.replacementEpochPath = replacementEpochPath;
3202
+ exports.requireEngine = requireEngine;
2189
3203
  exports.resetAnalysisStartedPromise = resetAnalysisStartedPromise;
2190
3204
  exports.runSessionPipeline = runSessionPipeline;
2191
3205
  exports.sessionArtifactDir = sessionArtifactDir;
3206
+ exports.sessionKey = sessionKey;
2192
3207
  exports.setAnalysisStartedPromise = setAnalysisStartedPromise;
2193
- exports.setSharedEngine = setSharedEngine;
2194
3208
  exports.startTurbopackWatcher = startTurbopackWatcher;
2195
3209
  exports.stylesPath = stylesPath;
2196
3210
  exports.systemPropsPath = systemPropsPath;