@animus-ui/extract 0.1.9 → 0.1.12

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 (158) hide show
  1. package/README.md +48 -1
  2. package/crates/extract-v2/animus-extract-v2.darwin-arm64.node +0 -0
  3. package/crates/extract-v2/animus-extract-v2.linux-arm64-gnu.node +0 -0
  4. package/crates/extract-v2/animus-extract-v2.linux-x64-gnu.node +0 -0
  5. package/crates/extract-v2/index.d.ts +17 -98
  6. package/dist/index.cjs +78 -2101
  7. package/dist/index.mjs +2 -2026
  8. package/dist/{analyze-project-args.d.ts → pipeline/analyze-project-args.d.ts} +0 -12
  9. package/dist/pipeline/analyze-project-args.d.ts.map +1 -0
  10. package/dist/pipeline/assemble-stylesheet.d.ts +23 -0
  11. package/dist/pipeline/assemble-stylesheet.d.ts.map +1 -0
  12. package/dist/pipeline/asset-placeholders.d.ts +4 -0
  13. package/dist/pipeline/asset-placeholders.d.ts.map +1 -0
  14. package/dist/pipeline/content-hash.d.ts +2 -0
  15. package/dist/pipeline/content-hash.d.ts.map +1 -0
  16. package/dist/pipeline/core-options.d.ts +96 -0
  17. package/dist/pipeline/core-options.d.ts.map +1 -0
  18. package/dist/pipeline/correlate-external-tokens.d.ts +38 -0
  19. package/dist/pipeline/correlate-external-tokens.d.ts.map +1 -0
  20. package/dist/pipeline/discover-files.d.ts +4 -0
  21. package/dist/pipeline/discover-files.d.ts.map +1 -0
  22. package/dist/pipeline/discover-packages.d.ts +55 -0
  23. package/dist/pipeline/discover-packages.d.ts.map +1 -0
  24. package/dist/pipeline/dynamic-prop-config.d.ts +22 -0
  25. package/dist/pipeline/dynamic-prop-config.d.ts.map +1 -0
  26. package/dist/pipeline/engine-adapter.d.ts +63 -0
  27. package/dist/pipeline/engine-adapter.d.ts.map +1 -0
  28. package/dist/pipeline/engine-retirement.d.ts +3 -0
  29. package/dist/pipeline/engine-retirement.d.ts.map +1 -0
  30. package/dist/pipeline/error-diagnostics.d.ts +8 -0
  31. package/dist/pipeline/error-diagnostics.d.ts.map +1 -0
  32. package/dist/{index.d.ts → pipeline/index.d.ts} +16 -6
  33. package/dist/pipeline/index.d.ts.map +1 -0
  34. package/dist/pipeline/internal-wire.d.ts +2 -0
  35. package/dist/pipeline/internal-wire.d.ts.map +1 -0
  36. package/dist/pipeline/manifest-diagnostics.d.ts +57 -0
  37. package/dist/pipeline/manifest-diagnostics.d.ts.map +1 -0
  38. package/dist/pipeline/manifest-schema.d.ts +117 -0
  39. package/dist/pipeline/manifest-schema.d.ts.map +1 -0
  40. package/dist/pipeline/mdx-preprocessor.d.ts +25 -0
  41. package/dist/pipeline/mdx-preprocessor.d.ts.map +1 -0
  42. package/dist/pipeline/path-aliases.d.ts +15 -0
  43. package/dist/pipeline/path-aliases.d.ts.map +1 -0
  44. package/dist/{post-process-css.d.ts → pipeline/post-process-css.d.ts} +0 -8
  45. package/dist/pipeline/post-process-css.d.ts.map +1 -0
  46. package/dist/pipeline/prefix.d.ts +8 -0
  47. package/dist/pipeline/prefix.d.ts.map +1 -0
  48. package/dist/pipeline/replacement-plans.d.ts +22 -0
  49. package/dist/pipeline/replacement-plans.d.ts.map +1 -0
  50. package/dist/pipeline/resolve-asset.d.ts +11 -0
  51. package/dist/pipeline/resolve-asset.d.ts.map +1 -0
  52. package/dist/pipeline/run-analysis.d.ts +62 -0
  53. package/dist/pipeline/run-analysis.d.ts.map +1 -0
  54. package/dist/pipeline/source-corpus.d.ts +25 -0
  55. package/dist/pipeline/source-corpus.d.ts.map +1 -0
  56. package/dist/pipeline/source-identity.d.ts +63 -0
  57. package/dist/pipeline/source-identity.d.ts.map +1 -0
  58. package/dist/pipeline/source-ingestion.d.ts +168 -0
  59. package/dist/pipeline/source-ingestion.d.ts.map +1 -0
  60. package/dist/pipeline/static-css.d.ts +16 -0
  61. package/dist/pipeline/static-css.d.ts.map +1 -0
  62. package/dist/pipeline/structural-self-check.d.ts +12 -0
  63. package/dist/pipeline/structural-self-check.d.ts.map +1 -0
  64. package/dist/pipeline/svelte-source-adapter.d.ts +81 -0
  65. package/dist/pipeline/svelte-source-adapter.d.ts.map +1 -0
  66. package/dist/pipeline/system-config.d.ts +40 -0
  67. package/dist/pipeline/system-config.d.ts.map +1 -0
  68. package/dist/pipeline/system-props-module.d.ts +8 -0
  69. package/dist/pipeline/system-props-module.d.ts.map +1 -0
  70. package/dist/pipeline/timing-waterfall.d.ts +6 -0
  71. package/dist/pipeline/timing-waterfall.d.ts.map +1 -0
  72. package/dist/pipeline/tsconfig-paths.d.ts +7 -0
  73. package/dist/pipeline/tsconfig-paths.d.ts.map +1 -0
  74. package/dist/pipeline/unit-fallback.d.ts +2 -0
  75. package/dist/pipeline/unit-fallback.d.ts.map +1 -0
  76. package/dist/pipeline/utils.d.ts +3 -0
  77. package/dist/pipeline/utils.d.ts.map +1 -0
  78. package/dist/pipeline/watch-keys.d.ts +2 -0
  79. package/dist/pipeline/watch-keys.d.ts.map +1 -0
  80. package/dist/pipeline-DeEENy5C.mjs +2905 -0
  81. package/dist/pipeline-x1yBZ2UP.cjs +3386 -0
  82. package/dist/session/extraction-session.d.ts +240 -0
  83. package/dist/session/extraction-session.d.ts.map +1 -0
  84. package/dist/session/index.d.ts +10 -0
  85. package/dist/session/index.d.ts.map +1 -0
  86. package/dist/session/published-set.d.ts +52 -0
  87. package/dist/session/published-set.d.ts.map +1 -0
  88. package/dist/session/resolve-packages.d.ts +7 -0
  89. package/dist/session/resolve-packages.d.ts.map +1 -0
  90. package/dist/session/session-paths.d.ts +97 -0
  91. package/dist/session/session-paths.d.ts.map +1 -0
  92. package/dist/session/singleton.d.ts +85 -0
  93. package/dist/session/singleton.d.ts.map +1 -0
  94. package/dist/session/timing.d.ts +2 -0
  95. package/dist/session/timing.d.ts.map +1 -0
  96. package/dist/session/turbopack-orchestrator.d.ts +27 -0
  97. package/dist/session/turbopack-orchestrator.d.ts.map +1 -0
  98. package/dist/session.cjs +1999 -0
  99. package/dist/session.mjs +1955 -0
  100. package/index-v2.js +0 -9
  101. package/package.json +22 -9
  102. package/dist/analyze-project-args.d.ts.map +0 -1
  103. package/dist/assemble-stylesheet.d.ts +0 -58
  104. package/dist/assemble-stylesheet.d.ts.map +0 -1
  105. package/dist/asset-placeholders.d.ts +0 -29
  106. package/dist/asset-placeholders.d.ts.map +0 -1
  107. package/dist/content-hash.d.ts +0 -9
  108. package/dist/content-hash.d.ts.map +0 -1
  109. package/dist/correlate-external-tokens.d.ts +0 -46
  110. package/dist/correlate-external-tokens.d.ts.map +0 -1
  111. package/dist/discover-files.d.ts +0 -10
  112. package/dist/discover-files.d.ts.map +0 -1
  113. package/dist/discover-packages.d.ts +0 -192
  114. package/dist/discover-packages.d.ts.map +0 -1
  115. package/dist/dynamic-prop-config.d.ts +0 -37
  116. package/dist/dynamic-prop-config.d.ts.map +0 -1
  117. package/dist/engine-adapter.d.ts +0 -97
  118. package/dist/engine-adapter.d.ts.map +0 -1
  119. package/dist/engine-retirement.d.ts +0 -22
  120. package/dist/engine-retirement.d.ts.map +0 -1
  121. package/dist/error-diagnostics.d.ts +0 -32
  122. package/dist/error-diagnostics.d.ts.map +0 -1
  123. package/dist/external-keyframes.d.ts +0 -23
  124. package/dist/external-keyframes.d.ts.map +0 -1
  125. package/dist/index.d.ts.map +0 -1
  126. package/dist/manifest-diagnostics.d.ts +0 -62
  127. package/dist/manifest-diagnostics.d.ts.map +0 -1
  128. package/dist/mdx-preprocessor.d.ts +0 -39
  129. package/dist/mdx-preprocessor.d.ts.map +0 -1
  130. package/dist/path-aliases.d.ts +0 -34
  131. package/dist/path-aliases.d.ts.map +0 -1
  132. package/dist/post-process-css.d.ts.map +0 -1
  133. package/dist/prefix.d.ts +0 -15
  134. package/dist/prefix.d.ts.map +0 -1
  135. package/dist/replacement-plans.d.ts +0 -46
  136. package/dist/replacement-plans.d.ts.map +0 -1
  137. package/dist/resolve-asset.d.ts +0 -14
  138. package/dist/resolve-asset.d.ts.map +0 -1
  139. package/dist/run-analysis.d.ts +0 -74
  140. package/dist/run-analysis.d.ts.map +0 -1
  141. package/dist/source-identity.d.ts +0 -93
  142. package/dist/source-identity.d.ts.map +0 -1
  143. package/dist/static-css.d.ts +0 -25
  144. package/dist/static-css.d.ts.map +0 -1
  145. package/dist/system-config.d.ts +0 -57
  146. package/dist/system-config.d.ts.map +0 -1
  147. package/dist/system-props-module.d.ts +0 -22
  148. package/dist/system-props-module.d.ts.map +0 -1
  149. package/dist/timing-waterfall.d.ts +0 -17
  150. package/dist/timing-waterfall.d.ts.map +0 -1
  151. package/dist/tsconfig-paths.d.ts +0 -8
  152. package/dist/tsconfig-paths.d.ts.map +0 -1
  153. package/dist/unit-fallback.d.ts +0 -7
  154. package/dist/unit-fallback.d.ts.map +0 -1
  155. package/dist/utils.d.ts +0 -3
  156. package/dist/utils.d.ts.map +0 -1
  157. package/dist/watch-keys.d.ts +0 -12
  158. package/dist/watch-keys.d.ts.map +0 -1
@@ -0,0 +1,1955 @@
1
+ import { A as enforceExternalTokenContracts, B as firstOwners, D as postProcessCss, E as vocabularyWitnessDiagnostics, F as createSourceCorpus, G as walkPackageSources, I as collectExternalPackageSources, J as sharesVolumeRoot, K as createSourceIdentity, L as excludeCollectedPackages, N as findAssetSpecifiers, O as resolveLightningTargets, P as substituteAssetPlaceholders, R as extractSystemFilePackages, U as staleDistIncludesMessage, V as isExcludedPackageRelativePath, W as unresolvableIncludesMessage, X as discoverFiles, _ as clearEngineCache, a as assertNoErrorDiagnostics, at as resolveMode, c as buildSystemPropsModule, ct as createV2EngineApi, d as loadSystemConfig, ft as projectExternalFileOwners, i as snapshotFilePlans, it as createExcludeMatcher, j as resolveAssetFile, mt as DEFAULT_EXTENSIONS, p as serializeStaticCss, q as isPathWithinRoot, r as hashReplacementPlans, s as formatRustTimingWaterfall, t as toWatchKeys, v as runProjectAnalysis, vt as contentHash, w as unreadableSourceDiagnostic, wt as __require, xt as assembleStylesheet, z as findPackageRoot } from "./pipeline-DeEENy5C.mjs";
2
+ import { randomUUID } from "crypto";
3
+ import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, watch, writeFileSync } from "fs";
4
+ import { basename, extname, join, relative, resolve } from "path";
5
+ //#region session/session-paths.ts
6
+ /**
7
+ * Session artifact names, path derivations, and artifact shapes. Imports no
8
+ * session or singleton module: the Turbopack loader graph stays worker-local.
9
+ */
10
+ /** Session-dir-relative name of the replacement-epoch disk witness. */
11
+ const REPLACEMENT_EPOCH_ARTIFACT = "replacements-epoch";
12
+ /** Virtual module id of the emitted system-props module. */
13
+ const TURBOPACK_SYSTEM_PROPS_ID = "virtual:animus/system-props";
14
+ /** Session-dir-relative name of the analysis-commit transaction artifact. */
15
+ const ANALYSIS_COMMIT_ARTIFACT = "analysis-commit";
16
+ /** Session-dir-relative name of the analysis-status artifact. */
17
+ const ANALYSIS_STATUS_ARTIFACT = "analysis-status.json";
18
+ /** Session-dir-relative name of the manifest payload artifact. */
19
+ const MANIFEST_ARTIFACT = "manifest.json";
20
+ /** Session-dir-relative name of the analysis-inputs payload artifact — the
21
+ * Turbopack loader-worker hydration corpus. */
22
+ const ANALYSIS_INPUTS_ARTIFACT = "analysis-inputs.json";
23
+ /** Session-dir-relative name of the emitted stylesheet artifact. */
24
+ const STYLES_ARTIFACT = "styles.css";
25
+ /** Session-dir-relative name of the emitted system-props module. */
26
+ const SYSTEM_PROPS_ARTIFACT = "system-props.js";
27
+ /** Session-dir-relative directory holding copied asset() files. */
28
+ const SESSION_ASSETS_DIR = "assets";
29
+ /** The standalone CLI's published-set commit record in the flat `.animus/`
30
+ * tree; the session's start hygiene keys on this name before deleting. */
31
+ const CLI_COMMIT_ARTIFACT = "commit.json";
32
+ /** Advisory claim record (pid plus heartbeat) of whichever directory holds
33
+ * it; sessions write it into their own tree too, despite the `CLI_` name. */
34
+ const CLI_LOCK_ARTIFACT = "lock.json";
35
+ /** Project-relative artifact directory: the CLI's default output tree and
36
+ * the parent of every session tree. One spelling, or the tree splits. */
37
+ const ANIMUS_ARTIFACT_DIR = ".animus";
38
+ /** Module id the Rust emitter injects, and the alias key both bundlers
39
+ * register. A literal on purpose: a rename breaks built consumer packages.
40
+ * The Next plugin's webpack alias harvest skips this exact key, so a change
41
+ * here is a change in the emitter, both bundler arms and that harvest. */
42
+ const ANIMUS_CSS_MODULE_ID = ".animus/styles.css";
43
+ function sessionsRootDir(rootDir) {
44
+ return join(rootDir, ANIMUS_ARTIFACT_DIR, "sessions");
45
+ }
46
+ function sessionArtifactDir(rootDir, sessionId) {
47
+ return join(sessionsRootDir(rootDir), sessionId);
48
+ }
49
+ function replacementEpochPath(sessionDir) {
50
+ return join(sessionDir, REPLACEMENT_EPOCH_ARTIFACT);
51
+ }
52
+ function analysisCommitPath(sessionDir) {
53
+ return join(sessionDir, ANALYSIS_COMMIT_ARTIFACT);
54
+ }
55
+ function analysisStatusPath(sessionDir) {
56
+ return join(sessionDir, ANALYSIS_STATUS_ARTIFACT);
57
+ }
58
+ function manifestPath(sessionDir) {
59
+ return join(sessionDir, MANIFEST_ARTIFACT);
60
+ }
61
+ function analysisInputsPath(sessionDir) {
62
+ return join(sessionDir, ANALYSIS_INPUTS_ARTIFACT);
63
+ }
64
+ function stylesPath(sessionDir) {
65
+ return join(sessionDir, STYLES_ARTIFACT);
66
+ }
67
+ function systemPropsPath(sessionDir) {
68
+ return join(sessionDir, SYSTEM_PROPS_ARTIFACT);
69
+ }
70
+ /** Splices the envelope in as a leading `__animusSession` field without
71
+ * re-serializing: parse/re-stringify would reformat engine-emitted JSON. */
72
+ function envelopeJsonArtifact(payload, envelopeJson) {
73
+ const trimmed = payload.trimStart();
74
+ if (!trimmed.startsWith("{")) return payload;
75
+ if (trimmed.slice(1).trimStart().startsWith("}")) return `{"__animusSession":${envelopeJson}}`;
76
+ return `{"__animusSession":${envelopeJson},${trimmed.slice(1)}`;
77
+ }
78
+ /** Trailing-comment side-band: a leading comment would displace the `@layer`
79
+ * declaration consumers pin at offset 0. */
80
+ function envelopeCssArtifact(payload, envelopeJson) {
81
+ return `${payload}\n/* __animusSession ${envelopeJson} */\n`;
82
+ }
83
+ const ENVELOPE_CSS_COMMENT_RE = /\/\* __animusSession (\{.*\}) \*\//;
84
+ /** Envelope of a JSON artifact's bytes, or undefined when it carries none.
85
+ * Throws on unparseable bytes; callers decide torn-artifact policy. */
86
+ function readJsonEnvelope(bytes) {
87
+ return JSON.parse(bytes).__animusSession;
88
+ }
89
+ /** Envelope of the CSS side-band comment, or undefined when it is absent.
90
+ * Throws on an unparseable envelope body. */
91
+ function readCssEnvelope(bytes) {
92
+ const match = bytes.match(ENVELOPE_CSS_COMMENT_RE);
93
+ return match ? JSON.parse(match[1]) : void 0;
94
+ }
95
+ //#endregion
96
+ //#region session/published-set.ts
97
+ function isPublishedSetRecord(value) {
98
+ return Object.prototype.toString.call(value) === "[object Object]";
99
+ }
100
+ function isPublishedSetString(value) {
101
+ return Object.prototype.toString.call(value) === "[object String]";
102
+ }
103
+ function isPublishedSetNumber(value) {
104
+ return Object.prototype.toString.call(value) === "[object Number]";
105
+ }
106
+ function decodeCommitRecord(bytes) {
107
+ let candidate;
108
+ try {
109
+ candidate = JSON.parse(bytes);
110
+ } catch {
111
+ return null;
112
+ }
113
+ if (!isPublishedSetRecord(candidate) || candidate.schema !== 1) return null;
114
+ const declared = candidate.payloads;
115
+ if (!isPublishedSetRecord(declared)) return null;
116
+ const payloads = {};
117
+ for (const [name, entry] of Object.entries(declared)) {
118
+ if (!isPublishedSetRecord(entry) || !isPublishedSetString(entry.hash)) return null;
119
+ payloads[name] = { hash: entry.hash };
120
+ }
121
+ return {
122
+ schema: 1,
123
+ payloads
124
+ };
125
+ }
126
+ /** Recompute every recorded payload hash over raw bytes — the writer's own
127
+ * hashing domain — and return the mismatches; empty means the set verifies. */
128
+ function verifyCommitRecord(dir) {
129
+ let bytes;
130
+ try {
131
+ bytes = readFileSync(join(dir, CLI_COMMIT_ARTIFACT), "utf-8");
132
+ } catch (error) {
133
+ return [`commit record unreadable: ${String(error)}`];
134
+ }
135
+ const record = decodeCommitRecord(bytes);
136
+ if (record === null) return ["commit record is not a schema-1 payload record"];
137
+ const failures = [];
138
+ for (const [name, entry] of Object.entries(record.payloads)) try {
139
+ if (contentHash(readFileSync(join(dir, name))) !== entry.hash) failures.push(`${name}: bytes do not match the commit record`);
140
+ } catch (error) {
141
+ failures.push(`${name}: unreadable (${String(error)})`);
142
+ }
143
+ return failures;
144
+ }
145
+ const CLI_LOCK_HEARTBEAT_INTERVAL_MS = 6e4;
146
+ /** A claim whose heartbeat is older than this has no live holder. Five missed
147
+ * refreshes: synchronous extraction can block the holder's refresh timer. */
148
+ const CLI_LOCK_STALE_AFTER_MS = 5 * CLI_LOCK_HEARTBEAT_INTERVAL_MS;
149
+ /** Absence of the file — the only read failure that means "no holder".
150
+ * ENOTDIR joins ENOENT: the path cannot exist under a non-directory. */
151
+ function isMissingFile(error) {
152
+ return error instanceof Object && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR");
153
+ }
154
+ function readCliLockRecord(dir) {
155
+ let bytes;
156
+ try {
157
+ bytes = readFileSync(join(dir, CLI_LOCK_ARTIFACT), "utf-8");
158
+ } catch (error) {
159
+ if (isMissingFile(error)) return { kind: "none" };
160
+ throw error;
161
+ }
162
+ let candidate;
163
+ try {
164
+ candidate = JSON.parse(bytes);
165
+ } catch {
166
+ return { kind: "indeterminate" };
167
+ }
168
+ if (!isPublishedSetRecord(candidate) || !isPublishedSetNumber(candidate.pid)) return { kind: "indeterminate" };
169
+ const startedAt = candidate.startedAt;
170
+ const heartbeatAt = candidate.heartbeatAt;
171
+ const record = { pid: candidate.pid };
172
+ if (isPublishedSetString(startedAt)) record.startedAt = startedAt;
173
+ if (isPublishedSetString(heartbeatAt)) record.heartbeatAt = heartbeatAt;
174
+ return {
175
+ kind: "held",
176
+ record
177
+ };
178
+ }
179
+ function isPermissionDenied(error) {
180
+ return error instanceof Object && "code" in error && error.code === "EPERM";
181
+ }
182
+ function isLockHolderAlive(pid) {
183
+ try {
184
+ process.kill(pid, 0);
185
+ return true;
186
+ } catch (error) {
187
+ return isPermissionDenied(error);
188
+ }
189
+ }
190
+ function checkLockLiveness(record) {
191
+ if (!isLockHolderAlive(record.pid)) return {
192
+ live: false,
193
+ reason: `holder pid ${record.pid} is not running`
194
+ };
195
+ const beatAt = record.heartbeatAt === void 0 ? NaN : Date.parse(record.heartbeatAt);
196
+ if (Number.isNaN(beatAt)) return { live: true };
197
+ const ageMs = Date.now() - beatAt;
198
+ if (ageMs <= 3e5) return { live: true };
199
+ return {
200
+ live: false,
201
+ reason: `holder pid ${record.pid} is running but its lock heartbeat (${record.heartbeatAt}) is ${Math.round(ageMs / 1e3)}s old, past the ${CLI_LOCK_STALE_AFTER_MS / 1e3}s staleness window — the pid names a different process now`
202
+ };
203
+ }
204
+ function lockRecordBytes(startedAt) {
205
+ return JSON.stringify({
206
+ pid: process.pid,
207
+ startedAt,
208
+ heartbeatAt: (/* @__PURE__ */ new Date()).toISOString()
209
+ });
210
+ }
211
+ const HEARTBEAT_TEMP_PREFIX = ".lock-heartbeat-";
212
+ function holdDirectoryClaim(dir, options = {}) {
213
+ const startedAt = options.startedAt ?? (/* @__PURE__ */ new Date()).toISOString();
214
+ const path = join(dir, CLI_LOCK_ARTIFACT);
215
+ const tempPath = join(dir, `${HEARTBEAT_TEMP_PREFIX}${process.pid}`);
216
+ const rename = options.write === "rename";
217
+ let refresh = null;
218
+ let stopped = false;
219
+ const stop = () => {
220
+ stopped = true;
221
+ if (refresh !== null) clearInterval(refresh);
222
+ refresh = null;
223
+ };
224
+ const stamp = () => {
225
+ try {
226
+ if (rename) {
227
+ writeFileSync(tempPath, lockRecordBytes(startedAt));
228
+ renameSync(tempPath, path);
229
+ } else writeFileSync(path, lockRecordBytes(startedAt));
230
+ } catch (error) {
231
+ stop();
232
+ if (isMissingFile(error)) return;
233
+ console.error(`[animus] Could not write the directory claim ${path}: ${String(error)}`);
234
+ }
235
+ };
236
+ stamp();
237
+ if (!stopped) {
238
+ refresh = setInterval(stamp, CLI_LOCK_HEARTBEAT_INTERVAL_MS);
239
+ refresh.unref?.();
240
+ }
241
+ return () => {
242
+ stop();
243
+ if (rename) rmSync(tempPath, { force: true });
244
+ rmSync(path, { force: true });
245
+ };
246
+ }
247
+ /** Snapshot the session's copied asset() files, sorted, BEFORE the session
248
+ * tree is disposed. Drivers publish them beside the emitted stylesheet. */
249
+ function collectSessionAssets(sessionDir) {
250
+ if (!sessionDir) return [];
251
+ const dir = join(sessionDir, SESSION_ASSETS_DIR);
252
+ if (!existsSync(dir)) return [];
253
+ return readdirSync(dir).sort().map((name) => ({
254
+ name,
255
+ bytes: readFileSync(join(dir, name))
256
+ }));
257
+ }
258
+ //#endregion
259
+ //#region session/resolve-packages.ts
260
+ function resolvePackagesByName(rootDir, names) {
261
+ if (names.length === 0) return {};
262
+ const nameSet = new Set(names);
263
+ const resolved = /* @__PURE__ */ new Set();
264
+ const packageMap = {};
265
+ try {
266
+ const rootPkg = JSON.parse(readFileSync(join(rootDir, "package.json"), "utf-8"));
267
+ const workspaces = Array.isArray(rootPkg.workspaces) ? rootPkg.workspaces : rootPkg.workspaces?.packages ?? [];
268
+ for (const ws of workspaces) {
269
+ const wsDir = resolve(rootDir, ws);
270
+ if (!existsSync(wsDir)) continue;
271
+ try {
272
+ const pkg = JSON.parse(readFileSync(join(wsDir, "package.json"), "utf-8"));
273
+ const name = pkg.name || "";
274
+ if (nameSet.has(name)) {
275
+ const main = pkg.main || pkg.module || "index.ts";
276
+ const entryPath = resolve(wsDir, main);
277
+ if (existsSync(entryPath)) {
278
+ packageMap[name] = relative(rootDir, entryPath);
279
+ resolved.add(name);
280
+ }
281
+ }
282
+ } catch {}
283
+ }
284
+ } catch {}
285
+ for (const name of nameSet) {
286
+ if (resolved.has(name)) continue;
287
+ try {
288
+ const entryPath = __require.resolve(name, { paths: [rootDir] });
289
+ packageMap[name] = relative(rootDir, entryPath);
290
+ } catch {}
291
+ }
292
+ return packageMap;
293
+ }
294
+ //#endregion
295
+ //#region session/singleton.ts
296
+ /**
297
+ * globalThis-backed so the ESM plugin copy and the CJS webpack-loader copy
298
+ * share one store; module scope alone gives each copy its own instance.
299
+ */
300
+ const MANIFEST_KEY = "__animus_manifest_json__";
301
+ const ANALYSIS_STARTED_KEY = "__animus_analysis_started__";
302
+ const SHARED_CSS_KEY = "__animus_shared_css__";
303
+ const SHARED_SYSTEM_PROPS_KEY = "__animus_shared_system_props__";
304
+ const SHARED_EXTERNAL_DIRS_KEY = "__animus_external_pkg_dirs__";
305
+ const SHARED_EXTERNAL_ENTRIES_KEY = "__animus_external_source_entries__";
306
+ const ANALYZED_HASHES_KEY = "__animus_analyzed_hashes__";
307
+ const REPLACEMENT_EPOCH_KEY = "__animus_replacement_epoch__";
308
+ const WATCH_TRANSACTION_KEY = "__animus_watch_transaction__";
309
+ const PROCESS_SESSION_ID_KEY = "__animus_process_session_id__";
310
+ const SESSION_ARTIFACT_DIR_KEY = "__animus_session_artifact_dir__";
311
+ const OWNING_WATCH_SESSION_KEY = "__animus_owning_watch_session__";
312
+ const EXCLUSIVE_SESSION_OWNER_KEY = "__animus_exclusive_session_owner__";
313
+ const ENGINE_KEY = "__animus_engine__";
314
+ const V2_ENGINE_KEY = "__animus_v2_engine__";
315
+ const V2_SENT_SOURCES_KEY = "__animus_v2_sent_sources__";
316
+ const V2_DRIFT_WARNED_KEY = "__animus_v2_drift_warned__";
317
+ const ENGINE_API_OVERRIDE_KEY = "__animus_engine_api_override__";
318
+ const singletonStore = globalThis;
319
+ const isSingletonString = (value) => {
320
+ if (Object(value) === value) return false;
321
+ try {
322
+ String.prototype.valueOf.call(value);
323
+ return true;
324
+ } catch {
325
+ return false;
326
+ }
327
+ };
328
+ function getManifestJson() {
329
+ return singletonStore[MANIFEST_KEY];
330
+ }
331
+ function setManifestJson(json) {
332
+ singletonStore[MANIFEST_KEY] = json;
333
+ }
334
+ /**
335
+ * A latch over the most recently STARTED analysis, never cleared on settle:
336
+ * non-null means one was scheduled. In-flight lives in getWatchTransaction.
337
+ */
338
+ function getAnalysisStartedPromise() {
339
+ return singletonStore[ANALYSIS_STARTED_KEY];
340
+ }
341
+ function setAnalysisStartedPromise(promise) {
342
+ singletonStore[ANALYSIS_STARTED_KEY] = promise;
343
+ }
344
+ function resetAnalysisStartedPromise() {
345
+ singletonStore[ANALYSIS_STARTED_KEY] = null;
346
+ }
347
+ function getSharedCss() {
348
+ return singletonStore[SHARED_CSS_KEY] || "";
349
+ }
350
+ function setSharedCss(css) {
351
+ singletonStore[SHARED_CSS_KEY] = css;
352
+ }
353
+ function getSharedSystemProps() {
354
+ return singletonStore[SHARED_SYSTEM_PROPS_KEY] || "";
355
+ }
356
+ function setSharedSystemProps(content) {
357
+ singletonStore[SHARED_SYSTEM_PROPS_KEY] = content;
358
+ }
359
+ function getSharedExternalDirs() {
360
+ return singletonStore[SHARED_EXTERNAL_DIRS_KEY] || [];
361
+ }
362
+ function setSharedExternalDirs(dirs) {
363
+ singletonStore[SHARED_EXTERNAL_DIRS_KEY] = dirs;
364
+ }
365
+ function getSharedExternalEntries() {
366
+ return singletonStore[SHARED_EXTERNAL_ENTRIES_KEY] || /* @__PURE__ */ new Map();
367
+ }
368
+ function setSharedExternalEntries(entries) {
369
+ singletonStore[SHARED_EXTERNAL_ENTRIES_KEY] = entries;
370
+ }
371
+ function globalSlot(key) {
372
+ return {
373
+ get: () => singletonStore[key] ?? null,
374
+ set: (value) => {
375
+ singletonStore[key] = value;
376
+ }
377
+ };
378
+ }
379
+ const analyzedHashesSlot = globalSlot(ANALYZED_HASHES_KEY);
380
+ const replacementEpochSlot = globalSlot(REPLACEMENT_EPOCH_KEY);
381
+ const watchTransactionSlot = globalSlot(WATCH_TRANSACTION_KEY);
382
+ const sessionArtifactDirSlot = globalSlot(SESSION_ARTIFACT_DIR_KEY);
383
+ const owningWatchSessionSlot = globalSlot(OWNING_WATCH_SESSION_KEY);
384
+ /**
385
+ * The only session that can run watch analysis. A non-owning session forwards
386
+ * its batch here; dropping it loses files only that watcher sees.
387
+ */
388
+ function getOwningWatchSession() {
389
+ return owningWatchSessionSlot.get();
390
+ }
391
+ function setOwningWatchSession(session) {
392
+ owningWatchSessionSlot.set(session);
393
+ }
394
+ /**
395
+ * Per-file content hashes of the last published analysis (relPath → hash of
396
+ * the exact bytes analyzed), including files that produced zero entries.
397
+ */
398
+ function getAnalyzedHashes() {
399
+ return analyzedHashesSlot.get();
400
+ }
401
+ function setAnalyzedHashes(hashes) {
402
+ analyzedHashesSlot.set(hashes);
403
+ }
404
+ /**
405
+ * Session identity is per process: the first caller claims a fresh id and
406
+ * every later one adopts it, so all compilers share one artifact tree.
407
+ */
408
+ function claimProcessSessionId() {
409
+ const existing = singletonStore[PROCESS_SESSION_ID_KEY];
410
+ if (isSingletonString(existing) && existing.length > 0) return existing;
411
+ const fresh = randomUUID();
412
+ singletonStore[PROCESS_SESSION_ID_KEY] = fresh;
413
+ return fresh;
414
+ }
415
+ /**
416
+ * Two CONCURRENT drive loops in one process would share one session tree and
417
+ * clobber each other; sequential claim/release cycles are legal.
418
+ */
419
+ function claimExclusiveSessionOwner(label) {
420
+ const active = singletonStore[EXCLUSIVE_SESSION_OWNER_KEY];
421
+ 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.`);
422
+ singletonStore[EXCLUSIVE_SESSION_OWNER_KEY] = label;
423
+ return () => {
424
+ if (singletonStore[EXCLUSIVE_SESSION_OWNER_KEY] === label) delete singletonStore[EXCLUSIVE_SESSION_OWNER_KEY];
425
+ };
426
+ }
427
+ /**
428
+ * Absolute artifact directory of the OWNING session's last publication. The
429
+ * Turbopack loader runs out of process and receives it via options instead.
430
+ */
431
+ function getSessionArtifactDir() {
432
+ return sessionArtifactDirSlot.get();
433
+ }
434
+ function setSessionArtifactDir(dir) {
435
+ sessionArtifactDirSlot.set(dir);
436
+ }
437
+ /**
438
+ * Replacement epoch of the last published analysis, written AFTER the
439
+ * manifest so an epoch reader always sees at least that generation's manifest.
440
+ */
441
+ function getReplacementEpoch() {
442
+ return replacementEpochSlot.get();
443
+ }
444
+ function setReplacementEpoch(epoch) {
445
+ replacementEpochSlot.set(epoch);
446
+ }
447
+ /**
448
+ * The one in-flight watch-analysis transaction: a compiler that did not start
449
+ * it joins this promise rather than read a pre-transaction generation.
450
+ */
451
+ function getWatchTransaction() {
452
+ return watchTransactionSlot.get();
453
+ }
454
+ function setWatchTransaction(transaction) {
455
+ watchTransactionSlot.set(transaction);
456
+ }
457
+ /** Engine selection travels through the singleton so non-owning compilers
458
+ * and the webpack loader honor the owner's choice. */
459
+ function setSharedEngine(engine) {
460
+ singletonStore[ENGINE_KEY] = engine;
461
+ }
462
+ function getSharedEngine() {
463
+ return singletonStore[ENGINE_KEY] || "v2";
464
+ }
465
+ /** Single choke-point for every native extraction call; the NAPI module's
466
+ * own `.d.ts` is the authoritative surface for what it returns. */
467
+ function requireEngine() {
468
+ return __require("@animus-ui/extract");
469
+ }
470
+ /**
471
+ * The one engine API for this process: the instance, its analyze-time
472
+ * sources, and the drift flag live on globalThis so both copies share them.
473
+ */
474
+ const v2EngineApi = createV2EngineApi({
475
+ label: "animus",
476
+ isV2: () => getSharedEngine() === "v2",
477
+ loadNativeEngine: requireEngine,
478
+ passThroughUnknownPaths: true,
479
+ store: {
480
+ getEngine: () => singletonStore[V2_ENGINE_KEY],
481
+ setEngine: (engine) => {
482
+ singletonStore[V2_ENGINE_KEY] = engine;
483
+ },
484
+ getSentSources: () => singletonStore[V2_SENT_SOURCES_KEY] ?? null,
485
+ setSentSources: (sources) => {
486
+ singletonStore[V2_SENT_SOURCES_KEY] = sources;
487
+ },
488
+ getDriftWarned: () => Boolean(singletonStore[V2_DRIFT_WARNED_KEY]),
489
+ setDriftWarned: (value) => {
490
+ singletonStore[V2_DRIFT_WARNED_KEY] = value;
491
+ }
492
+ }
493
+ });
494
+ function engineApi() {
495
+ const override = singletonStore[ENGINE_API_OVERRIDE_KEY];
496
+ return override ? override() : v2EngineApi();
497
+ }
498
+ //#endregion
499
+ //#region session/timing.ts
500
+ function logBuildTimings(bt, rustTiming, log, verbose) {
501
+ if (!verbose) return;
502
+ const pad = (label, width) => label + " ".repeat(Math.max(0, width - label.length));
503
+ for (const [key, label] of [
504
+ ["systemLoad", "system-load"],
505
+ ["fileDiscovery", "file-discovery"],
506
+ ["fileRead", "file-read+hash"],
507
+ ["packageResolve", "pkg-resolve"],
508
+ ["analysis", "analysis"]
509
+ ]) {
510
+ const ms = bt[key] ?? 0;
511
+ if (ms === 0 && !Object.hasOwn(bt, key)) continue;
512
+ log(` ${pad(label, 17)}${String(ms).padStart(5)}ms`);
513
+ if (key === "analysis") for (const [sk, sl] of [
514
+ ["jsonSerialize", "json-serialize"],
515
+ ["rustExtract", "rust-extract"],
516
+ ["jsonParse", "json-parse"]
517
+ ]) {
518
+ const sms = bt[sk] ?? 0;
519
+ log(` ${pad(sl, 15)}${String(sms).padStart(5)}ms`);
520
+ if (sk === "rustExtract" && rustTiming) for (const line of formatRustTimingWaterfall(rustTiming, {
521
+ indent: " ",
522
+ labelWidth: 13
523
+ })) log(line);
524
+ }
525
+ }
526
+ log(` total ${String(bt.total).padStart(5)}ms`);
527
+ if (process.env.ANIMUS_TIMING_JSON === "1") {
528
+ const merged = {};
529
+ for (const [k, v] of Object.entries(bt)) merged[`buildStart.${k}`] = v;
530
+ if (rustTiming) for (const [k, v] of Object.entries(rustTiming)) merged[`rust.${k}`] = v;
531
+ console.error(`[animus:timing] ${JSON.stringify(merged)}`);
532
+ }
533
+ }
534
+ //#endregion
535
+ //#region session/extraction-session.ts
536
+ const SESSION_DIR_MAX_AGE_MS = 864e5;
537
+ const STATUS_WATCHDOG_MS = 2e3;
538
+ const LEGACY_FLAT_ARTIFACTS = [
539
+ MANIFEST_ARTIFACT,
540
+ ANALYSIS_INPUTS_ARTIFACT,
541
+ STYLES_ARTIFACT,
542
+ SYSTEM_PROPS_ARTIFACT,
543
+ REPLACEMENT_EPOCH_ARTIFACT,
544
+ ANALYSIS_COMMIT_ARTIFACT,
545
+ ANALYSIS_STATUS_ARTIFACT
546
+ ];
547
+ /** Brands values read back off DISK. `Object(value) !== value` rejects
548
+ * boxed primitives; the tag is unspoofable by `Symbol.toStringTag`. */
549
+ function isDiskString(value) {
550
+ return Object(value) !== value && Object.prototype.toString.call(value) === "[object String]";
551
+ }
552
+ function isDiskNumber(value) {
553
+ return Object(value) !== value && Object.prototype.toString.call(value) === "[object Number]";
554
+ }
555
+ /** Bundler-agnostic extraction pipeline: system load, source ingestion,
556
+ * analysis, emission, and watch-cycle diffing. Carries no bundler types. */
557
+ var ExtractionSession = class {
558
+ /** Set by the adapter before any pipeline run. */
559
+ rootDir = null;
560
+ /** Serialized path aliases, harvested by the adapter from bundler config. */
561
+ pathAliasesJson = null;
562
+ assetCopyCache = /* @__PURE__ */ new Map();
563
+ /** Physical asset files registered with the host watcher. */
564
+ assetDependencyPaths = /* @__PURE__ */ new Set();
565
+ assetDependencyKeys = /* @__PURE__ */ new Set();
566
+ /** System-props module id override. Turbopack rejects absolute-path
567
+ * imports, so its driver sets a virtual id; null injects the abs path. */
568
+ systemPropsModuleId = null;
569
+ /** Whether the analysis-inputs hydration corpus is written to disk. Only
570
+ * isolated loader workers replay it; an in-process loader reads memory. */
571
+ persistAnalysisInputs = false;
572
+ /** Absolute directory prefixes for external packages (loader allowlist). */
573
+ externalPackageDirs = [];
574
+ /** Absolute package dir → owning specifier (cross-source correlation). */
575
+ externalDirOwners = {};
576
+ /** rootDir-relative external file → owning specifier (correlation join). */
577
+ externalFileOwners = {};
578
+ /** External package specifier → absolute source entry path. */
579
+ externalSourceEntries = /* @__PURE__ */ new Map();
580
+ /** SourceId authority for this generation — roots realpath'd once per
581
+ * pipeline; aliases recorded while files exist so deletions resolve. */
582
+ sourceIdentity = null;
583
+ /** Canonical admitted external root → declared specifiers; duplicate
584
+ * specifiers resolving to one canonical root share an entry. */
585
+ externalRootOwners = /* @__PURE__ */ new Map();
586
+ /** Canonical external root → the extension set its collection walk used
587
+ * (widened for dist-only roots); rewalks and classification share it. */
588
+ externalRootExtensions = /* @__PURE__ */ new Map();
589
+ /** Canonical admitted external watch roots — the engine-side watch
590
+ * surface (webpack contextDependencies / Turbopack watchers). */
591
+ externalWatchRoots = [];
592
+ /** Canonical external root → sourceKey → {raw content hash, recorded abs}:
593
+ * the inventory a dirty-root rewalk diffs against, hashed before MDX. */
594
+ externalInventory = /* @__PURE__ */ new Map();
595
+ /** Stable key → message, re-surfaced on every full pipeline until the
596
+ * underlying condition clears. */
597
+ stickyDiagnostics = /* @__PURE__ */ new Map();
598
+ /** Volume-membership predicate for the cross-volume gate. Injected seam:
599
+ * the real predicate only discriminates on Windows, so tests replace it. */
600
+ sharesProjectVolume = sharesVolumeRoot;
601
+ /** `onExternalRootResolved` fires per admitted root DURING collection,
602
+ * before its walk; `onExternalRootsCommitted` only after publication. */
603
+ onExternalRootResolved = null;
604
+ onExternalRootsCommitted = null;
605
+ /** Fires exactly once per settled `handleWatchUpdate` — the failure cause,
606
+ * or null when clean. Observation never alters delivery. */
607
+ onCycleSettled = null;
608
+ options;
609
+ staticCssJson;
610
+ system = null;
611
+ /** Vocabulary witness diagnostics from the sealed system's registration
612
+ * record, awaiting the shared surfacing pass. */
613
+ systemVocabularyDiagnostics = [];
614
+ /** Configured external files that could not be read. Incremental passes
615
+ * re-collect nothing, so they replay these instead of passing strict. */
616
+ ingestionFailureDiagnostics = [];
617
+ /** Full package-resolution map from the last full pipeline — replayed by
618
+ * incremental passes (sourceEntries alone omits dist-resolved packages). */
619
+ lastPackageMap = {};
620
+ fileCache = /* @__PURE__ */ new Map();
621
+ corpus = createSourceCorpus({
622
+ engineApi: () => engineApi(),
623
+ prefix: "[animus-next]",
624
+ strict: () => !!this.options.strict,
625
+ warn: (message) => this.warn(message)
626
+ });
627
+ systemDependencyKeys = /* @__PURE__ */ new Set();
628
+ /** Loader-reported dependency paths for bundler watch registration. */
629
+ systemDependencyPaths = [];
630
+ systemDependencyHashes = /* @__PURE__ */ new Map();
631
+ lastSystemPropsHash = null;
632
+ artifactRecords = {
633
+ manifest: null,
634
+ inputs: null,
635
+ styles: null
636
+ };
637
+ /** Last written analysis-commit; null seeds from disk on first use. Its
638
+ * generation counter is correct only because one session owns the dir. */
639
+ lastCommit = null;
640
+ /** Release handle for the in-process owner slot AND the directory's
641
+ * on-disk owner record, taken and released as one; null when unheld. */
642
+ releasePublicationClaim = null;
643
+ /** Session identity, claimed once per PROCESS and adopted by every later
644
+ * session instance so all compilers share one artifact tree. */
645
+ sessionId = claimProcessSessionId();
646
+ /** Epoch of the last published analysis; null seeds from disk so a
647
+ * same-session restart with unchanged plans rewrites no bytes. */
648
+ lastEpochValue = null;
649
+ /** Watcher debounce ceiling feeding status deadlines. */
650
+ debounceCeilingMs = 75;
651
+ /** Test seam: observes every session-artifact write (name, content)
652
+ * post-rename — write ORDER is part of the transaction contract. */
653
+ onArtifactWrite = null;
654
+ statusAttemptId = 0;
655
+ statusAttemptOpen = false;
656
+ /** Monotonic readiness witness: flips true on the first complete
657
+ * publication and never regresses; every later status write carries it. */
658
+ firstEmissionComplete = false;
659
+ /** Debounce-window observations pending analysis (sourceKey → hash). */
660
+ debouncePending = /* @__PURE__ */ new Map();
661
+ sessionStartHygieneDone = false;
662
+ lcssTargets = null;
663
+ constructor(options) {
664
+ this.options = options;
665
+ this.staticCssJson = serializeStaticCss(options.staticCss);
666
+ }
667
+ get sessionDir() {
668
+ return sessionArtifactDir(this.rootDir, this.sessionId);
669
+ }
670
+ get verbose() {
671
+ return this.options.verbose === true || process.env.ANIMUS_DEBUG === "1" || process.env.ANIMUS_DEBUG === "true";
672
+ }
673
+ log(msg) {
674
+ if (this.verbose) console.error(`[animus] ${msg}`);
675
+ }
676
+ warn(msg) {
677
+ console.warn(`[animus] ${msg}`);
678
+ }
679
+ now() {
680
+ return this.verbose ? performance.now() : 0;
681
+ }
682
+ elapsed(t) {
683
+ return this.verbose ? Math.round(performance.now() - t) : 0;
684
+ }
685
+ /** Diagnostic prefix label — drivers set their own so one host's errors
686
+ * never masquerade as another driver's. */
687
+ driverLabel = "animus-next";
688
+ /** Per-specifier discovery outcomes from the last full collection —
689
+ * driver-consumed reporting surface (the CLI's summary). */
690
+ lastExternalOutcomes = [];
691
+ /** Component count of the last published analysis, null before the first —
692
+ * read by drivers instead of re-parsing the manifest JSON every cycle. */
693
+ lastComponentCount = null;
694
+ /** When superseded asset copies leave `assets/`: `'full-pipeline'` suits a
695
+ * driver serving the dir in place, `'every-cycle'` one republishing it. */
696
+ staleAssetPruning = "full-pipeline";
697
+ /** Driver-owned STRUCTURAL exclusions, joined to the never-replaceable set
698
+ * — never the user list, whose presence flips the replace semantics. */
699
+ structuralExclude = [];
700
+ scanConfigMemo = null;
701
+ /** Per-pattern exclusion hit counts from this session's matcher —
702
+ * driver-consumed reporting surface (dead-pattern visibility). */
703
+ getExcludeStats() {
704
+ return this.resolveScanConfig().excludeMatcher.stats();
705
+ }
706
+ /** Resolve the scan configuration from options — the single source of the
707
+ * exclude/extension policy shared by the full and incremental pipelines. */
708
+ resolveScanConfig() {
709
+ if (this.scanConfigMemo === null) {
710
+ const extensionsSet = new Set(this.options.extensions ?? DEFAULT_EXTENSIONS);
711
+ this.scanConfigMemo = {
712
+ excludeMatcher: createExcludeMatcher(this.options.exclude, this.structuralExclude),
713
+ extensionsSet
714
+ };
715
+ }
716
+ return this.scanConfigMemo;
717
+ }
718
+ /** One single-flight analysis transaction per event batch: concurrent
719
+ * entries join the in-flight promise; each settled entry reports outcome. */
720
+ async handleWatchUpdate(changes) {
721
+ try {
722
+ await this.routeWatchUpdate(changes);
723
+ } catch (error) {
724
+ this.onCycleSettled?.(error);
725
+ throw error;
726
+ }
727
+ this.onCycleSettled?.(null);
728
+ }
729
+ async routeWatchUpdate(changes) {
730
+ const inflight = getWatchTransaction();
731
+ if (inflight) {
732
+ await inflight;
733
+ return;
734
+ }
735
+ if (!this.system) {
736
+ const owner = getOwningWatchSession();
737
+ const hasBatch = (changes.modifiedFiles?.size ?? 0) > 0 || (changes.removedFiles?.size ?? 0) > 0;
738
+ if (owner && owner !== this && hasBatch) await owner.ingestForwardedBatch(changes);
739
+ return;
740
+ }
741
+ const transaction = this.processWatchUpdate(changes);
742
+ setWatchTransaction(transaction);
743
+ try {
744
+ await transaction;
745
+ } finally {
746
+ setWatchTransaction(null);
747
+ }
748
+ }
749
+ /** Owner-side entry for a batch a non-owning compiler observed. Serializes
750
+ * behind any in-flight transaction — it has no other delivery. */
751
+ async ingestForwardedBatch(changes) {
752
+ if (!this.system) return;
753
+ for (;;) {
754
+ const inflight = getWatchTransaction();
755
+ if (!inflight) break;
756
+ await inflight;
757
+ }
758
+ const transaction = this.processWatchUpdate(changes);
759
+ setWatchTransaction(transaction);
760
+ try {
761
+ await transaction;
762
+ } finally {
763
+ setWatchTransaction(null);
764
+ }
765
+ }
766
+ async processWatchUpdate(changes) {
767
+ const rootDir = this.rootDir;
768
+ try {
769
+ changes = this.reconcileExternalRoots(changes);
770
+ } catch (err) {
771
+ this.warn(`external root reconciliation failed: ${String(err)}`);
772
+ }
773
+ const changed = [...changes.modifiedFiles ?? [], ...changes.removedFiles ?? []];
774
+ let assetChanged = false;
775
+ let systemHit;
776
+ try {
777
+ if (!changes.modifiedFiles && !changes.removedFiles) for (const [dep, hash] of this.systemDependencyHashes) {
778
+ let current = "";
779
+ try {
780
+ current = contentHash(readFileSync(dep, "utf-8"));
781
+ } catch {}
782
+ if (current !== hash) {
783
+ systemHit = dep;
784
+ break;
785
+ }
786
+ }
787
+ else systemHit = changed.find((path) => toWatchKeys(path).some((key) => this.systemDependencyKeys.has(key)));
788
+ } catch (err) {
789
+ this.warn(`HMR system-reload check failed: ${String(err)}`);
790
+ }
791
+ if (systemHit) {
792
+ this.log(`system reload: system dependency changed (${relative(rootDir, systemHit)})`);
793
+ this.resetForHmr();
794
+ try {
795
+ const promise = this.runFullPipeline(this.pendingFromBatch(changes));
796
+ setAnalysisStartedPromise(promise);
797
+ await promise;
798
+ } catch (err) {
799
+ try {
800
+ this.beginStatusAttempt();
801
+ this.writeAnalysisStatus("failed", this.pendingFromBatch(changes), String(err));
802
+ } catch (statusErr) {
803
+ this.warn(`failed-status write failed: ${String(statusErr)}`);
804
+ }
805
+ throw err;
806
+ }
807
+ return;
808
+ }
809
+ if (changed.some((path) => toWatchKeys(path).some((key) => this.assetDependencyKeys.has(key)))) {
810
+ this.assetCopyCache.clear();
811
+ assetChanged = true;
812
+ }
813
+ const { excludeMatcher, extensionsSet } = this.resolveScanConfig();
814
+ const priorCacheEntries = /* @__PURE__ */ new Map();
815
+ const priorExternalFileOwners = { ...this.externalFileOwners };
816
+ const recordPrior = (key) => {
817
+ if (!priorCacheEntries.has(key)) priorCacheEntries.set(key, this.fileCache.get(key) ?? null);
818
+ };
819
+ const inventoryUpdates = [];
820
+ const ownerRemovals = [];
821
+ let removedAny = false;
822
+ if (changes.removedFiles) for (const removedPath of changes.removedFiles) {
823
+ let key;
824
+ let owningRoot = null;
825
+ if (isPathWithinRoot(rootDir, removedPath)) key = relative(rootDir, removedPath);
826
+ else {
827
+ const resolved = this.sourceIdentity?.resolveDeletedSourceId(removedPath);
828
+ if (!resolved) continue;
829
+ key = resolved.sourceKey;
830
+ owningRoot = resolved.owningRoot;
831
+ }
832
+ recordPrior(key);
833
+ if (this.fileCache.delete(key)) {
834
+ removedAny = true;
835
+ if (owningRoot) {
836
+ inventoryUpdates.push({
837
+ root: owningRoot,
838
+ key,
839
+ hash: null,
840
+ abs: removedPath
841
+ });
842
+ ownerRemovals.push(key, key + ".tsx");
843
+ }
844
+ }
845
+ }
846
+ let targets;
847
+ if (changes.modifiedFiles) {
848
+ targets = [];
849
+ for (const modifiedPath of changes.modifiedFiles) {
850
+ const classified = this.classifyWatchPath(modifiedPath, {
851
+ excludeMatcher,
852
+ extensionsSet
853
+ });
854
+ if (classified) targets.push({
855
+ abs: modifiedPath,
856
+ ...classified
857
+ });
858
+ }
859
+ } else targets = discoverFiles(rootDir, rootDir, excludeMatcher, extensionsSet).map((abs) => ({
860
+ abs,
861
+ key: relative(rootDir, abs),
862
+ owningRoot: null
863
+ }));
864
+ const changedPaths = [];
865
+ for (const target of targets) {
866
+ const relPath = target.key;
867
+ let source;
868
+ try {
869
+ source = readFileSync(target.abs, "utf-8");
870
+ } catch {
871
+ continue;
872
+ }
873
+ const rawHash = contentHash(source);
874
+ const cached = this.fileCache.get(relPath);
875
+ const hash = rawHash;
876
+ if (!cached || cached.hash !== hash) {
877
+ changedPaths.push(relPath);
878
+ recordPrior(relPath);
879
+ this.fileCache.set(relPath, {
880
+ hash,
881
+ source
882
+ });
883
+ if (target.owningRoot) {
884
+ const owner = this.externalRootOwners.get(target.owningRoot)?.values().next().value;
885
+ if (owner !== void 0) this.externalFileOwners[relPath] ??= owner;
886
+ inventoryUpdates.push({
887
+ root: target.owningRoot,
888
+ key: target.key,
889
+ hash: rawHash,
890
+ abs: target.abs
891
+ });
892
+ }
893
+ }
894
+ }
895
+ if (changedPaths.length > 0 || removedAny || assetChanged) {
896
+ const pending = changedPaths.map((rel) => [rel, this.fileCache.get(rel).hash]);
897
+ let ingested;
898
+ try {
899
+ this.beginStatusAttempt();
900
+ ingested = await this.corpus.prepare({
901
+ fileCache: this.fileCache,
902
+ externalFileOwners: this.externalFileOwners
903
+ });
904
+ this.externalFileOwners = projectExternalFileOwners(ingested, this.externalFileOwners);
905
+ resetAnalysisStartedPromise();
906
+ const promise = this.runIncrementalPipeline(ingested.analysisEntries, pending);
907
+ setAnalysisStartedPromise(promise);
908
+ await promise;
909
+ } catch (err) {
910
+ for (const [key, prior] of priorCacheEntries) if (prior === null) this.fileCache.delete(key);
911
+ else this.fileCache.set(key, prior);
912
+ this.externalFileOwners = priorExternalFileOwners;
913
+ if (this.statusAttemptOpen) {
914
+ this.debouncePending.clear();
915
+ this.writeAnalysisStatus("failed", pending, String(err));
916
+ }
917
+ throw err;
918
+ }
919
+ this.publishSourceIngestion(ingested);
920
+ for (const update of inventoryUpdates) {
921
+ let inventory = this.externalInventory.get(update.root);
922
+ if (!inventory) {
923
+ inventory = /* @__PURE__ */ new Map();
924
+ this.externalInventory.set(update.root, inventory);
925
+ }
926
+ if (update.hash === null) inventory.delete(update.key);
927
+ else inventory.set(update.key, {
928
+ hash: update.hash,
929
+ abs: update.abs
930
+ });
931
+ }
932
+ for (const key of ownerRemovals) delete this.externalFileOwners[key];
933
+ } else if (this.statusAttemptOpen) {
934
+ this.debouncePending.clear();
935
+ this.writeAnalysisStatus("idle", []);
936
+ }
937
+ }
938
+ publishSourceIngestion(result) {
939
+ this.fileCache = new Map(result.originalEntries.map((entry) => [entry.path, {
940
+ hash: entry.hash,
941
+ source: entry.source
942
+ }]));
943
+ this.corpus.publish(result);
944
+ }
945
+ /** Full analysis + publication, registered in the one in-flight
946
+ * transaction slot; a nested reload leaves the enclosing registration. */
947
+ async runFullPipeline(pending = []) {
948
+ if (this.releasePublicationClaim === null) {
949
+ const releaseOwner = claimExclusiveSessionOwner(`${this.driverLabel}:${this.sessionDir}`);
950
+ mkdirSync(this.sessionDir, { recursive: true });
951
+ const releaseClaimRecord = holdDirectoryClaim(this.sessionDir);
952
+ this.releasePublicationClaim = () => {
953
+ releaseOwner();
954
+ releaseClaimRecord();
955
+ };
956
+ }
957
+ const priorSystem = this.system;
958
+ const nested = getWatchTransaction() !== null;
959
+ const transaction = this.runPipelineTransaction(pending);
960
+ if (!nested) setWatchTransaction(transaction);
961
+ try {
962
+ await transaction;
963
+ } catch (error) {
964
+ this.system = priorSystem;
965
+ throw error;
966
+ } finally {
967
+ if (!nested) setWatchTransaction(null);
968
+ }
969
+ }
970
+ async runPipelineTransaction(pending) {
971
+ const pipelineStart = this.now();
972
+ const bt = {};
973
+ this.runSessionStartHygiene();
974
+ setSessionArtifactDir(this.sessionDir);
975
+ const rootDir = this.rootDir;
976
+ const priorSourceState = this.snapshotSourceState();
977
+ const resolvedSystemPath = resolve(rootDir, this.options.system);
978
+ let t = this.now();
979
+ this.system = loadSystemConfig(engineApi, {
980
+ systemPath: resolvedSystemPath,
981
+ rootDir,
982
+ prefix: this.options.prefix
983
+ });
984
+ this.systemVocabularyDiagnostics = vocabularyWitnessDiagnostics(this.system.vocabularyWitnessesJson);
985
+ this.assetCopyCache.clear();
986
+ this.assetDependencyPaths.clear();
987
+ this.assetDependencyKeys.clear();
988
+ {
989
+ const deps = this.system.dependencies ?? [];
990
+ const keys = /* @__PURE__ */ new Set();
991
+ for (const key of toWatchKeys(resolvedSystemPath)) keys.add(key);
992
+ for (const dep of deps) for (const key of toWatchKeys(dep)) keys.add(key);
993
+ this.systemDependencyKeys = keys;
994
+ this.systemDependencyPaths = deps;
995
+ const hashes = /* @__PURE__ */ new Map();
996
+ for (const dep of deps.length > 0 ? deps : [resolvedSystemPath]) try {
997
+ hashes.set(dep, contentHash(readFileSync(dep, "utf-8")));
998
+ } catch {
999
+ hashes.set(dep, "");
1000
+ }
1001
+ this.systemDependencyHashes = hashes;
1002
+ }
1003
+ bt.systemLoad = this.elapsed(t);
1004
+ t = this.now();
1005
+ const { excludeMatcher, extensionsSet } = this.resolveScanConfig();
1006
+ const files = discoverFiles(rootDir, rootDir, excludeMatcher, extensionsSet);
1007
+ bt.fileDiscovery = this.elapsed(t);
1008
+ t = this.now();
1009
+ const rawEntries = [];
1010
+ for (const filePath of files) {
1011
+ const source = readFileSync(filePath, "utf-8");
1012
+ const relPath = relative(rootDir, filePath);
1013
+ const hash = contentHash(source);
1014
+ rawEntries.push({
1015
+ path: relPath,
1016
+ source,
1017
+ hash
1018
+ });
1019
+ }
1020
+ bt.fileRead = this.elapsed(t);
1021
+ t = this.now();
1022
+ const packageNames = extractSystemFilePackages(resolvedSystemPath);
1023
+ const preResolved = resolvePackagesByName(rootDir, packageNames);
1024
+ const rawExternalFiles = /* @__PURE__ */ new Map();
1025
+ const ingestionFailures = [];
1026
+ const collected = await collectExternalPackageSources({
1027
+ specifiers: packageNames,
1028
+ resolveSpecifier: (name) => preResolved[name] ? resolve(rootDir, preResolved[name]) : null,
1029
+ rootDir,
1030
+ extensionsSet,
1031
+ hasEntry: (relPath) => rawEntries.some((e) => e.path === relPath),
1032
+ onSourceRead: (source, _relPath, absPath) => {
1033
+ rawExternalFiles.set(absPath, contentHash(source));
1034
+ },
1035
+ onUnreadable: (relPath, err) => ingestionFailures.push(unreadableSourceDiagnostic(relPath, err)),
1036
+ onPackageResolved: (_specifier, packageDir) => {
1037
+ if (!this.onExternalRootResolved) return;
1038
+ if (!this.sharesProjectVolume(rootDir, packageDir)) return;
1039
+ let canonical = packageDir;
1040
+ try {
1041
+ canonical = realpathSync(packageDir);
1042
+ } catch {}
1043
+ this.onExternalRootResolved(canonical);
1044
+ }
1045
+ });
1046
+ this.lastExternalOutcomes = collected.outcomes;
1047
+ this.ingestionFailureDiagnostics = ingestionFailures;
1048
+ for (const record of collected.outcomes) if (record.outcome === "empty") this.warn(`include '${record.specifier}' resolved but discovered no component sources`);
1049
+ const unresolvableMessage = unresolvableIncludesMessage(collected.outcomes);
1050
+ if (unresolvableMessage !== null) {
1051
+ if (this.options.strict) throw new Error(unresolvableMessage);
1052
+ this.warn(unresolvableMessage);
1053
+ }
1054
+ const staleDistMessage = staleDistIncludesMessage(collected.outcomes);
1055
+ if (staleDistMessage !== null) {
1056
+ if (this.options.strict) throw new Error(staleDistMessage);
1057
+ this.warn(staleDistMessage);
1058
+ }
1059
+ const rejectedSpecifiers = /* @__PURE__ */ new Set();
1060
+ const crossVolumeDetails = [];
1061
+ for (const [dir, specifiers] of Object.entries(collected.dirOwnerSets)) {
1062
+ if (this.sharesProjectVolume(rootDir, dir)) continue;
1063
+ for (const specifier of specifiers) rejectedSpecifiers.add(specifier);
1064
+ crossVolumeDetails.push(`${specifiers.join(", ")} → ${dir}`);
1065
+ }
1066
+ this.stickyDiagnostics.delete("cross-volume");
1067
+ if (rejectedSpecifiers.size > 0) {
1068
+ const message = `ANIMUS_EXTERNAL_CROSS_VOLUME_UNSUPPORTED: external package source root(s) on a different volume than the project root (${rootDir}): ${crossVolumeDetails.join("; ")} — cross-volume workspace sources are unsupported; the package(s) are excluded from extraction and watching`;
1069
+ if (this.options.strict) throw new Error(`[${this.driverLabel}] ${message}`);
1070
+ this.stickyDiagnostics.set("cross-volume", message);
1071
+ }
1072
+ for (const message of this.stickyDiagnostics.values()) this.warn(message);
1073
+ const admitted = excludeCollectedPackages(collected, rejectedSpecifiers, rootDir);
1074
+ try {
1075
+ const identity = createSourceIdentity(rootDir);
1076
+ this.externalRootOwners = /* @__PURE__ */ new Map();
1077
+ this.externalRootExtensions = /* @__PURE__ */ new Map();
1078
+ for (const [dir, specifiers] of Object.entries(admitted.dirOwnerSets)) {
1079
+ const canonical = identity.registerExternalRoot(dir);
1080
+ const owners = this.externalRootOwners.get(canonical) ?? /* @__PURE__ */ new Set();
1081
+ for (const specifier of specifiers) owners.add(specifier);
1082
+ this.externalRootOwners.set(canonical, owners);
1083
+ const exts = admitted.dirExtensions[dir];
1084
+ if (exts) this.externalRootExtensions.set(canonical, new Set(exts));
1085
+ }
1086
+ this.sourceIdentity = identity;
1087
+ this.externalWatchRoots = identity.externalRoots();
1088
+ this.externalInventory = /* @__PURE__ */ new Map();
1089
+ for (const [absPath, rawHash] of rawExternalFiles) {
1090
+ const resolved = identity.resolveSourceId(absPath);
1091
+ if (!resolved?.owningRoot) continue;
1092
+ let inventory = this.externalInventory.get(resolved.owningRoot);
1093
+ if (!inventory) {
1094
+ inventory = /* @__PURE__ */ new Map();
1095
+ this.externalInventory.set(resolved.owningRoot, inventory);
1096
+ }
1097
+ inventory.set(resolved.sourceKey, {
1098
+ hash: rawHash,
1099
+ abs: absPath
1100
+ });
1101
+ }
1102
+ const packageMap = admitted.packageMap;
1103
+ this.lastPackageMap = packageMap;
1104
+ this.externalDirOwners = firstOwners(admitted.dirOwnerSets);
1105
+ this.externalFileOwners = admitted.fileOwners;
1106
+ this.externalSourceEntries = admitted.sourceEntries;
1107
+ for (const entry of admitted.entries) {
1108
+ const hash = contentHash(entry.source);
1109
+ rawEntries.push({
1110
+ path: entry.path,
1111
+ source: entry.source,
1112
+ hash
1113
+ });
1114
+ }
1115
+ this.externalPackageDirs = admitted.packageDirs;
1116
+ bt.packageResolve = this.elapsed(t);
1117
+ this.beginStatusAttempt();
1118
+ let accepted;
1119
+ try {
1120
+ accepted = await this.corpus.prepare(rawEntries);
1121
+ } catch (err) {
1122
+ this.debouncePending.clear();
1123
+ this.writeAnalysisStatus("failed", pending, String(err));
1124
+ throw err;
1125
+ }
1126
+ this.externalFileOwners = projectExternalFileOwners(accepted, this.externalFileOwners);
1127
+ bt.fileCount = accepted.analysisEntries.length;
1128
+ clearEngineCache(engineApi);
1129
+ await this.analyzeAndEmit(accepted.analysisEntries, packageMap, false, bt, pipelineStart, pending);
1130
+ this.publishSourceIngestion(accepted);
1131
+ setSharedExternalDirs(admitted.packageDirs);
1132
+ setSharedExternalEntries(admitted.sourceEntries);
1133
+ this.onExternalRootsCommitted?.(this.externalWatchRoots);
1134
+ setOwningWatchSession(this);
1135
+ } catch (err) {
1136
+ this.restoreSourceState(priorSourceState);
1137
+ throw err;
1138
+ }
1139
+ }
1140
+ /** Per-generation source state captured as ONE object so the restore
1141
+ * cannot omit a member; deep-copied fields are mutated in place. */
1142
+ snapshotSourceState() {
1143
+ return {
1144
+ sourceIdentity: this.sourceIdentity,
1145
+ externalRootOwners: new Map([...this.externalRootOwners].map(([root, owners]) => [root, new Set(owners)])),
1146
+ externalRootExtensions: new Map(this.externalRootExtensions),
1147
+ externalWatchRoots: [...this.externalWatchRoots],
1148
+ externalInventory: new Map([...this.externalInventory].map(([root, inventory]) => [root, new Map(inventory)])),
1149
+ lastPackageMap: this.lastPackageMap,
1150
+ externalDirOwners: this.externalDirOwners,
1151
+ externalFileOwners: this.externalFileOwners,
1152
+ externalSourceEntries: this.externalSourceEntries,
1153
+ externalPackageDirs: this.externalPackageDirs,
1154
+ systemVocabularyDiagnostics: this.systemVocabularyDiagnostics,
1155
+ ingestionFailureDiagnostics: this.ingestionFailureDiagnostics
1156
+ };
1157
+ }
1158
+ restoreSourceState(snapshot) {
1159
+ Object.assign(this, snapshot);
1160
+ }
1161
+ /** Release this session's publication claim so a successor may publish.
1162
+ * Idempotent, and scoped to this session's own claim. */
1163
+ close() {
1164
+ this.releasePublicationClaim?.();
1165
+ this.releasePublicationClaim = null;
1166
+ }
1167
+ /** Reset analysis state for a system reload: write guards go back to null
1168
+ * and reseed from the disk envelopes on the next publication. */
1169
+ resetForHmr() {
1170
+ resetAnalysisStartedPromise();
1171
+ this.artifactRecords = {
1172
+ manifest: null,
1173
+ inputs: null,
1174
+ styles: null
1175
+ };
1176
+ this.lastSystemPropsHash = null;
1177
+ }
1178
+ /** The (sourceKey, observed hash) pairs of a watch batch — the status
1179
+ * file's pending set; loaders wait only on observed inputs. */
1180
+ pendingFromBatch(changes) {
1181
+ const { excludeMatcher, extensionsSet } = this.resolveScanConfig();
1182
+ const pending = [];
1183
+ for (const path of changes.modifiedFiles ?? []) {
1184
+ const classified = this.classifyWatchPath(path, {
1185
+ excludeMatcher,
1186
+ extensionsSet
1187
+ });
1188
+ if (!classified) continue;
1189
+ try {
1190
+ pending.push([classified.key, contentHash(readFileSync(path, "utf-8"))]);
1191
+ } catch {}
1192
+ }
1193
+ return pending;
1194
+ }
1195
+ /** Rewalk dirty external roots and diff the inventory into created,
1196
+ * edited and deleted deltas: a watcher may report only the directory. */
1197
+ reconcileExternalRoots(changes) {
1198
+ const identity = this.sourceIdentity;
1199
+ if (!identity) return changes;
1200
+ if (!changes.modifiedFiles && !changes.removedFiles) return changes;
1201
+ if (this.externalWatchRoots.length === 0) return changes;
1202
+ const { extensionsSet } = this.resolveScanConfig();
1203
+ const dirtyRoots = /* @__PURE__ */ new Set();
1204
+ const modified = /* @__PURE__ */ new Set();
1205
+ const removed = /* @__PURE__ */ new Set();
1206
+ const rootHitFor = (path) => {
1207
+ const containing = identity.containingExternalRoot(path);
1208
+ if (!containing) return null;
1209
+ try {
1210
+ return statSync(path).isDirectory() ? containing : null;
1211
+ } catch {
1212
+ return identity.resolveDeletedSourceId(path) ? null : containing;
1213
+ }
1214
+ };
1215
+ for (const path of changes.modifiedFiles ?? []) {
1216
+ const root = rootHitFor(path);
1217
+ if (root) dirtyRoots.add(root);
1218
+ else modified.add(path);
1219
+ }
1220
+ for (const path of changes.removedFiles ?? []) {
1221
+ const root = rootHitFor(path);
1222
+ if (root) dirtyRoots.add(root);
1223
+ else removed.add(path);
1224
+ }
1225
+ for (const root of dirtyRoots) {
1226
+ const previous = this.externalInventory.get(root) ?? /* @__PURE__ */ new Map();
1227
+ const seen = /* @__PURE__ */ new Set();
1228
+ const walked = walkPackageSources(root, this.externalRootExtensions.get(root) ?? extensionsSet);
1229
+ if (walked.length === 0 && previous.size > 0) this.warn(`external root rewalk found no files under ${root} while its inventory holds ${previous.size} — reconciling as full deletion`);
1230
+ for (const abs of walked) {
1231
+ const resolved = identity.resolveSourceId(abs);
1232
+ if (!resolved || resolved.owningRoot !== root) continue;
1233
+ seen.add(resolved.sourceKey);
1234
+ let rawHash;
1235
+ try {
1236
+ rawHash = contentHash(readFileSync(abs, "utf-8"));
1237
+ } catch {
1238
+ continue;
1239
+ }
1240
+ if (previous.get(resolved.sourceKey)?.hash !== rawHash) modified.add(abs);
1241
+ }
1242
+ for (const [key, entry] of previous) if (!seen.has(key)) removed.add(entry.abs);
1243
+ }
1244
+ return {
1245
+ modifiedFiles: modified,
1246
+ removedFiles: removed
1247
+ };
1248
+ }
1249
+ /** Route one watcher path: a project-root member (consumer excludes apply)
1250
+ * or an admitted external member (symlink escapes rejected). */
1251
+ classifyWatchPath(absPath, scan) {
1252
+ const ext = extname(absPath);
1253
+ const rootDir = this.rootDir;
1254
+ if (isPathWithinRoot(rootDir, absPath)) {
1255
+ if (!scan.extensionsSet.has(ext)) return null;
1256
+ const rel = relative(rootDir, absPath);
1257
+ if (scan.excludeMatcher.matches(absPath, rel)) return null;
1258
+ return {
1259
+ key: rel,
1260
+ owningRoot: null
1261
+ };
1262
+ }
1263
+ const resolved = this.sourceIdentity?.resolveSourceId(absPath);
1264
+ if (!resolved) return null;
1265
+ if (resolved.owningRoot === null) {
1266
+ if (!scan.extensionsSet.has(ext)) return null;
1267
+ if (scan.excludeMatcher.matches(resolved.sourceKey, resolved.sourceKey)) return null;
1268
+ return {
1269
+ key: resolved.sourceKey,
1270
+ owningRoot: null
1271
+ };
1272
+ }
1273
+ if (!(this.externalRootExtensions.get(resolved.owningRoot) ?? scan.extensionsSet).has(ext)) return null;
1274
+ if (isExcludedPackageRelativePath(resolved.pathInRoot)) return null;
1275
+ return {
1276
+ key: resolved.sourceKey,
1277
+ owningRoot: resolved.owningRoot
1278
+ };
1279
+ }
1280
+ async runIncrementalPipeline(fileEntries, pending = []) {
1281
+ const bt = {};
1282
+ const pipelineStart = this.now();
1283
+ await this.analyzeAndEmit(fileEntries, this.lastPackageMap, true, bt, pipelineStart, pending);
1284
+ }
1285
+ /** Shared analysis + emit core for both pipelines. `devMode` forks only
1286
+ * the report log and the system-props write guard. */
1287
+ async analyzeAndEmit(fileEntries, packageMap, devMode, bt, pipelineStart, pending = []) {
1288
+ this.beginStatusAttempt();
1289
+ this.debouncePending.clear();
1290
+ this.writeAnalysisStatus("starting", pending);
1291
+ try {
1292
+ await this.analyzeAndEmitAttempt(fileEntries, packageMap, devMode, bt, pipelineStart, pending);
1293
+ this.firstEmissionComplete = true;
1294
+ this.writeAnalysisStatus("idle", []);
1295
+ } catch (err) {
1296
+ this.writeAnalysisStatus("failed", pending, String(err));
1297
+ throw err;
1298
+ }
1299
+ }
1300
+ /** An explicit `mode` wins over the pipeline path: a pinned-production
1301
+ * watch must not emit unpruned CSS on its first incremental republish. */
1302
+ engineDevMode(pipelineDefault) {
1303
+ if (this.options.mode !== void 0) return this.options.mode === "development";
1304
+ return pipelineDefault;
1305
+ }
1306
+ async analyzeAndEmitAttempt(fileEntries, packageMap, devMode, bt, pipelineStart, pending) {
1307
+ const system = this.system;
1308
+ const analysisOptions = {
1309
+ fileEntries,
1310
+ packageMap,
1311
+ system,
1312
+ emitter: {
1313
+ runtimeImport: "@animus-ui/system/runtime",
1314
+ cssModuleId: ANIMUS_CSS_MODULE_ID,
1315
+ systemPropsModuleId: this.systemPropsModuleId ?? systemPropsPath(this.sessionDir)
1316
+ },
1317
+ pathAliasesJson: this.pathAliasesJson,
1318
+ staticCssJson: this.staticCssJson,
1319
+ externalDirs: this.externalPackageDirs.map((dir) => relative(this.rootDir, dir)),
1320
+ devMode: this.engineDevMode(devMode)
1321
+ };
1322
+ this.writeAnalysisStatus("analyzing", pending);
1323
+ const result = runProjectAnalysis(engineApi, {
1324
+ ...analysisOptions,
1325
+ warn: (message) => this.warn(message),
1326
+ strict: this.options.strict,
1327
+ extraDiagnostics: [...this.systemVocabularyDiagnostics, ...this.ingestionFailureDiagnostics]
1328
+ });
1329
+ assertNoErrorDiagnostics(result.manifest?.diagnostics);
1330
+ enforceExternalTokenContracts({
1331
+ diagnostics: result.manifest?.diagnostics,
1332
+ fileOwners: this.externalFileOwners,
1333
+ dirOwners: this.externalDirOwners,
1334
+ sourceThemeManifestsJson: system.sourceThemeManifestsJson,
1335
+ strict: this.options.strict,
1336
+ prefix: `[${this.driverLabel}]`,
1337
+ warn: (message) => this.warn(message)
1338
+ });
1339
+ bt.jsonSerialize = result.timings.serializeMs;
1340
+ bt.rustExtract = result.timings.extractMs;
1341
+ bt.jsonParse = result.timings.parseMs;
1342
+ const manifest = result.manifest;
1343
+ this.lastComponentCount = Object.keys(manifest?.components ?? {}).length;
1344
+ if (!devMode) {
1345
+ bt.analysis = (bt.jsonSerialize ?? 0) + (bt.rustExtract ?? 0) + (bt.jsonParse ?? 0);
1346
+ if (manifest?.report) this.log(`Extracted ${manifest.report.components_extracted ?? "?"}/${manifest.report.components_total ?? "?"} components (${bt.analysis}ms)`);
1347
+ }
1348
+ const globalCss = this.substituteAssetReferences(result.globalCss, !devMode || this.staleAssetPruning === "every-cycle");
1349
+ const { declaration, variables, body } = assembleStylesheet({
1350
+ layers: this.options.layers,
1351
+ variableCss: system.variableCss,
1352
+ globalCss,
1353
+ componentCss: result.componentCss,
1354
+ split: true
1355
+ });
1356
+ if (this.lcssTargets === null) this.lcssTargets = resolveLightningTargets(this.options.targets, this.rootDir);
1357
+ const fullCss = [
1358
+ declaration,
1359
+ variables,
1360
+ postProcessCss(body, {
1361
+ minify: this.options.minify ?? resolveMode(this.options.mode, () => process.env.NODE_ENV === "production" ? "production" : "development").mode === "production",
1362
+ targets: this.lcssTargets,
1363
+ warnFn: (msg) => this.warn(msg)
1364
+ })
1365
+ ].filter(Boolean).join("\n");
1366
+ setSharedCss(fullCss);
1367
+ const systemPropsContent = buildSystemPropsModule({
1368
+ systemPropMapJson: JSON.stringify(manifest?.system_prop_map ?? {}),
1369
+ groupRegistryJson: system.groupRegistryJson,
1370
+ dynamicProps: manifest?.dynamic_props ?? {}
1371
+ });
1372
+ setSharedSystemProps(systemPropsContent);
1373
+ setManifestJson(result.manifestJson);
1374
+ setAnalyzedHashes(new Map(fileEntries.map((entry) => [entry.path, entry.hash])));
1375
+ this.writeAnalysisStatus("committing", pending);
1376
+ const epoch = hashReplacementPlans(snapshotFilePlans(manifest), systemPropsContent);
1377
+ if (this.lastCommit === null) this.lastCommit = this.seedCommitFromDisk();
1378
+ const generation = (this.lastCommit?.generation ?? 0) + 1;
1379
+ this.publishPayloadArtifact("manifest", MANIFEST_ARTIFACT, result.manifestJson, epoch, generation, envelopeJsonArtifact);
1380
+ if (this.persistAnalysisInputs) this.publishPayloadArtifact("inputs", ANALYSIS_INPUTS_ARTIFACT, JSON.stringify({
1381
+ analyzedHashes: Object.fromEntries(fileEntries.map((entry) => [entry.path, entry.hash])),
1382
+ ...result.inputs
1383
+ }), epoch, generation, envelopeJsonArtifact);
1384
+ this.publishPayloadArtifact("styles", STYLES_ARTIFACT, fullCss, epoch, generation, envelopeCssArtifact);
1385
+ if (devMode) {
1386
+ const systemPropsHash = contentHash(systemPropsContent);
1387
+ if (systemPropsHash !== this.lastSystemPropsHash) {
1388
+ this.writeSessionArtifact(SYSTEM_PROPS_ARTIFACT, systemPropsContent);
1389
+ this.lastSystemPropsHash = systemPropsHash;
1390
+ }
1391
+ } else this.writeSessionArtifact(SYSTEM_PROPS_ARTIFACT, systemPropsContent);
1392
+ this.publishAnalysisCommit(epoch, generation);
1393
+ this.publishReplacementEpoch(epoch);
1394
+ bt.total = this.elapsed(pipelineStart);
1395
+ logBuildTimings(bt, manifest?.timing, (msg) => this.log(msg), this.verbose);
1396
+ }
1397
+ /** Open a status attempt (attemptId increments once per burst — a
1398
+ * debouncing pre-write and the analysis that follows share one id). */
1399
+ beginStatusAttempt() {
1400
+ if (!this.statusAttemptOpen) {
1401
+ this.statusAttemptId += 1;
1402
+ this.statusAttemptOpen = true;
1403
+ }
1404
+ }
1405
+ /** Write the session's analysis-status artifact. Terminal states
1406
+ * (idle/failed) close the attempt. */
1407
+ writeAnalysisStatus(state, pending, diagnostic) {
1408
+ const status = {
1409
+ schema: 2,
1410
+ sessionId: this.sessionId,
1411
+ attemptId: this.statusAttemptId,
1412
+ state,
1413
+ pending,
1414
+ deadlineAt: Date.now() + this.debounceCeilingMs + STATUS_WATCHDOG_MS
1415
+ };
1416
+ if (diagnostic !== void 0) status.diagnostic = diagnostic;
1417
+ status.ready = this.firstEmissionComplete;
1418
+ this.writeSessionArtifact(ANALYSIS_STATUS_ARTIFACT, JSON.stringify(status));
1419
+ if (state === "idle" || state === "failed") this.statusAttemptOpen = false;
1420
+ }
1421
+ /** Record watch events observed during the debounce window so a loader
1422
+ * running ahead of the analysis has evidence to wait on. */
1423
+ noteDebouncedWatchEvents(absPaths) {
1424
+ if (!this.rootDir) return;
1425
+ const additions = this.pendingFromBatch({ modifiedFiles: new Set(absPaths) });
1426
+ if (additions.length === 0) return;
1427
+ this.beginStatusAttempt();
1428
+ for (const [key, hash] of additions) this.debouncePending.set(key, hash);
1429
+ if (this.debounceStatusWriteScheduled) return;
1430
+ this.debounceStatusWriteScheduled = true;
1431
+ queueMicrotask(() => {
1432
+ this.debounceStatusWriteScheduled = false;
1433
+ if (this.debouncePending.size === 0) return;
1434
+ try {
1435
+ this.writeAnalysisStatus("debouncing", [...this.debouncePending.entries()]);
1436
+ } catch (err) {
1437
+ this.warn(`debounce status write failed: ${String(err)}`);
1438
+ }
1439
+ });
1440
+ }
1441
+ /** One pending microtask flushes a burst of debounce observations. */
1442
+ debounceStatusWriteScheduled = false;
1443
+ /** Write one enveloped payload artifact, rewritten only when the PAYLOAD
1444
+ * bytes changed; the recorded diskHash feeds the analysis-commit. */
1445
+ publishPayloadArtifact(key, name, payload, epoch, generation, wrap) {
1446
+ const payloadHash = contentHash(payload);
1447
+ if (this.artifactRecords[key] === null) this.artifactRecords[key] = this.seedPayloadRecord(key, name);
1448
+ const record = this.artifactRecords[key];
1449
+ if (record !== null && record.payloadHash === payloadHash) return;
1450
+ const envelope = {
1451
+ sessionId: this.sessionId,
1452
+ generation,
1453
+ replacementEpoch: epoch,
1454
+ payloadHash
1455
+ };
1456
+ const bytes = wrap(payload, JSON.stringify(envelope));
1457
+ this.writeSessionArtifact(name, bytes);
1458
+ this.artifactRecords[key] = {
1459
+ payloadHash,
1460
+ diskHash: contentHash(bytes)
1461
+ };
1462
+ }
1463
+ /** Reconstruct a payload write guard from the on-disk artifact's envelope
1464
+ * (same-session restart: a byte-identical payload must not rewrite). */
1465
+ seedPayloadRecord(key, name) {
1466
+ let bytes;
1467
+ try {
1468
+ bytes = readFileSync(join(this.sessionDir, name), "utf-8");
1469
+ } catch {
1470
+ return null;
1471
+ }
1472
+ let envelope;
1473
+ try {
1474
+ envelope = key === "styles" ? readCssEnvelope(bytes) : readJsonEnvelope(bytes);
1475
+ } catch {
1476
+ return null;
1477
+ }
1478
+ if (!envelope || !isDiskString(envelope.payloadHash)) return null;
1479
+ return {
1480
+ payloadHash: envelope.payloadHash,
1481
+ diskHash: contentHash(bytes)
1482
+ };
1483
+ }
1484
+ /** Last analysis-commit persisted in this session's directory, or null. */
1485
+ seedCommitFromDisk() {
1486
+ try {
1487
+ const parsed = JSON.parse(readFileSync(analysisCommitPath(this.sessionDir), "utf-8"));
1488
+ return parsed.schema === 1 && parsed.sessionId === this.sessionId && isDiskNumber(parsed.generation) ? parsed : null;
1489
+ } catch {
1490
+ return null;
1491
+ }
1492
+ }
1493
+ /** Publish the analysis-commit: written after every payload and before the
1494
+ * epoch, skipped when the payload hashes and epoch are unchanged. */
1495
+ publishAnalysisCommit(epoch, generation) {
1496
+ const manifestHash = this.artifactRecords.manifest?.diskHash ?? "";
1497
+ const inputsHash = this.persistAnalysisInputs ? this.artifactRecords.inputs?.diskHash ?? "" : void 0;
1498
+ const stylesHash = this.artifactRecords.styles?.diskHash ?? "";
1499
+ const prev = this.lastCommit;
1500
+ if (prev !== null && prev.manifestHash === manifestHash && prev.inputsHash === inputsHash && prev.stylesHash === stylesHash && prev.replacementEpoch === epoch) return;
1501
+ const commit = {
1502
+ schema: 1,
1503
+ sessionId: this.sessionId,
1504
+ generation,
1505
+ replacementEpoch: epoch,
1506
+ manifestHash,
1507
+ stylesHash
1508
+ };
1509
+ if (inputsHash !== void 0) commit.inputsHash = inputsHash;
1510
+ this.writeSessionArtifact(ANALYSIS_COMMIT_ARTIFACT, JSON.stringify(commit));
1511
+ this.lastCommit = commit;
1512
+ }
1513
+ /** Maintain the session's epoch witness, rewritten ONLY when the value
1514
+ * changes, then expose it through the singleton. */
1515
+ publishReplacementEpoch(epoch) {
1516
+ const onDisk = this.diskEpochValue();
1517
+ if (this.lastEpochValue === null) this.lastEpochValue = onDisk;
1518
+ if (epoch !== onDisk) this.writeSessionArtifact(REPLACEMENT_EPOCH_ARTIFACT, JSON.stringify({
1519
+ schema: 1,
1520
+ sessionId: this.sessionId,
1521
+ epoch
1522
+ }));
1523
+ if (epoch !== this.lastEpochValue) {
1524
+ this.lastEpochValue = epoch;
1525
+ this.reconcileSiblingEpochs(epoch);
1526
+ }
1527
+ setReplacementEpoch(epoch);
1528
+ }
1529
+ /** sessions-root listing memo, keyed by the root dir's mtime — a new or
1530
+ * pruned sibling DIRECTORY moves it; agreeing siblings stay listed. */
1531
+ siblingListing = null;
1532
+ /** Restored-module snapshots point at the epoch artifact of the session
1533
+ * that built them, so a moved epoch deletes every DISAGREEING sibling
1534
+ * artifact to invalidate them. Agreeing siblings stay byte-untouched so
1535
+ * warm restores keep working; a sibling with a live owner is exempt. */
1536
+ reconcileSiblingEpochs(epoch) {
1537
+ const rootPath = sessionsRootDir(this.rootDir);
1538
+ let entries;
1539
+ try {
1540
+ const mtimeMs = statSync(rootPath).mtimeMs;
1541
+ if (this.siblingListing?.mtimeMs === mtimeMs) entries = this.siblingListing.entries;
1542
+ else {
1543
+ entries = readdirSync(rootPath);
1544
+ this.siblingListing = {
1545
+ mtimeMs,
1546
+ entries
1547
+ };
1548
+ }
1549
+ } catch {
1550
+ return;
1551
+ }
1552
+ for (const entry of entries) {
1553
+ if (entry === this.sessionId) continue;
1554
+ const siblingDir = join(rootPath, entry);
1555
+ const siblingEpochPath = join(siblingDir, REPLACEMENT_EPOCH_ARTIFACT);
1556
+ try {
1557
+ if (JSON.parse(readFileSync(siblingEpochPath, "utf-8")).epoch === epoch) continue;
1558
+ } catch (err) {
1559
+ if (err?.code === "ENOENT") continue;
1560
+ }
1561
+ const ownerPid = this.liveSiblingOwnerPid(siblingDir);
1562
+ if (ownerPid !== null) {
1563
+ this.warn(`sibling session ${entry} still has a live owner (pid ${ownerPid}) and its replacement epoch disagrees with this session's (${epoch}) — leaving its epoch artifact in place`);
1564
+ continue;
1565
+ }
1566
+ try {
1567
+ unlinkSync(siblingEpochPath);
1568
+ } catch {}
1569
+ }
1570
+ }
1571
+ /** The pid of `siblingDir`'s live owner, or null when nothing proves one.
1572
+ * Our own pid is never one — publication is process-exclusive. A missing,
1573
+ * undecodable or unreadable claim all mean no owner: the opposite default
1574
+ * from the CLI lock check on purpose, so do not unify the two. */
1575
+ liveSiblingOwnerPid(siblingDir) {
1576
+ let claim;
1577
+ try {
1578
+ claim = readCliLockRecord(siblingDir);
1579
+ } catch {
1580
+ return null;
1581
+ }
1582
+ if (claim.kind !== "held") return null;
1583
+ if (claim.record.pid === process.pid) return null;
1584
+ return checkLockLiveness(claim.record).live ? claim.record.pid : null;
1585
+ }
1586
+ /** Epoch value held by this session's on-disk artifact, or null when
1587
+ * absent, unreadable, or not the expected schema. */
1588
+ diskEpochValue() {
1589
+ try {
1590
+ const parsed = JSON.parse(readFileSync(replacementEpochPath(this.sessionDir), "utf-8"));
1591
+ return parsed.schema === 1 && isDiskString(parsed.epoch) ? parsed.epoch : null;
1592
+ } catch {
1593
+ return null;
1594
+ }
1595
+ }
1596
+ /** True when the flat `.animus/` advisory lock claims the tree. A lock that
1597
+ * exists but cannot be decoded claims it too — unknown is never absent. */
1598
+ cliWriterHoldsLock(animusDir) {
1599
+ const lock = readCliLockRecord(animusDir);
1600
+ if (lock.kind === "none") return false;
1601
+ return lock.kind === "indeterminate" || checkLockLiveness(lock.record).live;
1602
+ }
1603
+ /** Delete legacy flat artifacts and prune sibling session directories past
1604
+ * the retention window — never this session's own dir. */
1605
+ runSessionStartHygiene() {
1606
+ if (this.sessionStartHygieneDone) return;
1607
+ this.sessionStartHygieneDone = true;
1608
+ const animusDir = join(this.rootDir, ANIMUS_ARTIFACT_DIR);
1609
+ const isCliPublishedSet = this.cliWriterHoldsLock(animusDir) || verifyCommitRecord(animusDir).length === 0;
1610
+ const cliPublishedNames = [
1611
+ MANIFEST_ARTIFACT,
1612
+ STYLES_ARTIFACT,
1613
+ SYSTEM_PROPS_ARTIFACT
1614
+ ];
1615
+ for (const name of LEGACY_FLAT_ARTIFACTS) {
1616
+ if (isCliPublishedSet && cliPublishedNames.includes(name)) continue;
1617
+ try {
1618
+ unlinkSync(join(animusDir, name));
1619
+ } catch {}
1620
+ }
1621
+ if (!isCliPublishedSet) try {
1622
+ unlinkSync(join(animusDir, CLI_COMMIT_ARTIFACT));
1623
+ } catch {}
1624
+ let entries;
1625
+ try {
1626
+ entries = readdirSync(sessionsRootDir(this.rootDir));
1627
+ } catch {
1628
+ return;
1629
+ }
1630
+ const cutoff = Date.now() - SESSION_DIR_MAX_AGE_MS;
1631
+ for (const entry of entries) {
1632
+ if (entry === this.sessionId) continue;
1633
+ const dir = join(sessionsRootDir(this.rootDir), entry);
1634
+ try {
1635
+ if (statSync(dir).mtimeMs >= cutoff) continue;
1636
+ if (this.liveSiblingOwnerPid(dir) !== null) continue;
1637
+ rmSync(dir, {
1638
+ recursive: true,
1639
+ force: true
1640
+ });
1641
+ } catch {}
1642
+ }
1643
+ }
1644
+ /** Write one artifact write-then-rename, so a cross-process reader never
1645
+ * sees a torn file; the tmp name carries the pid to keep writers apart. */
1646
+ writeSessionArtifact(name, content) {
1647
+ const dir = this.sessionDir;
1648
+ mkdirSync(dir, { recursive: true });
1649
+ const tmpPath = join(dir, `.${name}.${process.pid}.tmp`);
1650
+ writeFileSync(tmpPath, content);
1651
+ renameSync(tmpPath, join(dir, name));
1652
+ this.onArtifactWrite?.(name, content);
1653
+ }
1654
+ /** Copy each asset() specifier's bytes into `assets/` under a
1655
+ * content-hashed name and substitute a url relative to styles.css. */
1656
+ substituteAssetReferences(globalCss, pruneSuperseded) {
1657
+ const specifiers = findAssetSpecifiers(globalCss);
1658
+ const assetsDir = join(this.sessionDir, SESSION_ASSETS_DIR);
1659
+ const expected = /* @__PURE__ */ new Set();
1660
+ this.assetDependencyPaths.clear();
1661
+ this.assetDependencyKeys.clear();
1662
+ const urlBySpecifier = /* @__PURE__ */ new Map();
1663
+ for (const specifier of specifiers) {
1664
+ const cached = this.assetCopyCache.get(specifier);
1665
+ if (cached && existsSync(join(assetsDir, cached.fileName))) try {
1666
+ const current = statSync(cached.sourcePath);
1667
+ if (current.mtimeMs === cached.mtimeMs && current.size === cached.size) {
1668
+ this.trackAssetDependency(cached.sourcePath);
1669
+ expected.add(cached.fileName);
1670
+ urlBySpecifier.set(specifier, cached.url);
1671
+ continue;
1672
+ }
1673
+ } catch {}
1674
+ const resolvedPath = this.resolveAssetSpecifier(specifier);
1675
+ if (!resolvedPath) {
1676
+ const message = `unresolvable asset() specifier: ${specifier}`;
1677
+ if (this.options.strict) throw new Error(`[${this.driverLabel}] ${message}`);
1678
+ this.warn(message);
1679
+ urlBySpecifier.set(specifier, specifier);
1680
+ continue;
1681
+ }
1682
+ const bytes = readFileSync(resolvedPath);
1683
+ const sourceStat = statSync(resolvedPath);
1684
+ this.trackAssetDependency(resolvedPath);
1685
+ const ext = extname(resolvedPath);
1686
+ const fileName = `${basename(resolvedPath, ext)}.${contentHash(bytes).slice(0, 8)}${ext}`;
1687
+ expected.add(fileName);
1688
+ if (!existsSync(assetsDir)) mkdirSync(assetsDir, { recursive: true });
1689
+ const assetPath = join(assetsDir, fileName);
1690
+ if (!existsSync(assetPath)) writeFileSync(assetPath, bytes);
1691
+ const url = `./${SESSION_ASSETS_DIR}/${fileName}`;
1692
+ urlBySpecifier.set(specifier, url);
1693
+ this.assetCopyCache.set(specifier, {
1694
+ sourcePath: resolvedPath,
1695
+ mtimeMs: sourceStat.mtimeMs,
1696
+ size: sourceStat.size,
1697
+ fileName,
1698
+ url
1699
+ });
1700
+ }
1701
+ if (pruneSuperseded) pruneStaleAssets(assetsDir, expected);
1702
+ return substituteAssetPlaceholders(globalCss, urlBySpecifier);
1703
+ }
1704
+ trackAssetDependency(path) {
1705
+ this.assetDependencyPaths.add(path);
1706
+ for (const key of toWatchKeys(path)) this.assetDependencyKeys.add(key);
1707
+ }
1708
+ /** Resolve an asset specifier through the shared resolver, with one
1709
+ * last resort: a discovered source entry's package root. */
1710
+ resolveAssetSpecifier(specifier) {
1711
+ const resolved = resolveAssetFile(specifier, this.rootDir, this.pathAliasesJson);
1712
+ if (resolved) return resolved;
1713
+ const segments = specifier.split("/");
1714
+ const packageName = specifier.startsWith("@") ? segments.slice(0, 2).join("/") : segments[0];
1715
+ const subpath = specifier.slice(packageName.length + 1);
1716
+ if (!subpath) return null;
1717
+ const sourceEntry = this.externalSourceEntries.get(packageName) ?? [...this.externalSourceEntries].find(([declared]) => declared === packageName || declared.startsWith(`${packageName}/`))?.[1];
1718
+ if (sourceEntry) {
1719
+ const candidate = join(findPackageRoot(sourceEntry), subpath);
1720
+ if (existsSync(candidate)) return candidate;
1721
+ }
1722
+ return null;
1723
+ }
1724
+ };
1725
+ /** Sync `assets/` to the current build's file set; per-entry failures are
1726
+ * tolerated — a concurrent session may have removed the same file. */
1727
+ function pruneStaleAssets(assetsDir, expected) {
1728
+ if (!existsSync(assetsDir)) return;
1729
+ let entries;
1730
+ try {
1731
+ entries = readdirSync(assetsDir);
1732
+ } catch {
1733
+ return;
1734
+ }
1735
+ for (const entry of entries) {
1736
+ if (expected.has(entry)) continue;
1737
+ try {
1738
+ unlinkSync(join(assetsDir, entry));
1739
+ } catch {}
1740
+ }
1741
+ }
1742
+ //#endregion
1743
+ //#region session/turbopack-orchestrator.ts
1744
+ async function runSessionPipeline(session) {
1745
+ session.systemPropsModuleId = TURBOPACK_SYSTEM_PROPS_ID;
1746
+ session.persistAnalysisInputs = true;
1747
+ await session.runFullPipeline();
1748
+ }
1749
+ const activeWatcherRoots = /* @__PURE__ */ new Set();
1750
+ const IGNORED_SEGMENTS = /* @__PURE__ */ new Set([
1751
+ ANIMUS_ARTIFACT_DIR,
1752
+ ".next",
1753
+ "node_modules"
1754
+ ]);
1755
+ /** Registration failures meaning the OS is out of watch capacity: descriptor
1756
+ * and inotify limits surface under all of these. */
1757
+ const CAPACITY_CODES = /* @__PURE__ */ new Set([
1758
+ "EMFILE",
1759
+ "ENFILE",
1760
+ "ENOSPC",
1761
+ "EPERM"
1762
+ ]);
1763
+ function errnoCode(error) {
1764
+ if (!(error instanceof Object) || !("code" in error)) return null;
1765
+ const { code } = error;
1766
+ return isIntrinsicString(code) ? code : null;
1767
+ }
1768
+ /** A primitive string, decided by the intrinsic tag. `Object(value) !== value`
1769
+ * rejects boxed strings and a forged `Symbol.toStringTag`. */
1770
+ function isIntrinsicString(value) {
1771
+ return Object(value) !== value && Object.prototype.toString.call(value) === "[object String]";
1772
+ }
1773
+ function startTurbopackWatcher(session, rootDir, options = {}) {
1774
+ const { debounceMs = 75, watchFn = watch, holdEvents = false } = options;
1775
+ if (activeWatcherRoots.has(rootDir)) return { kind: "already-watched" };
1776
+ activeWatcherRoots.add(rootDir);
1777
+ session.debounceCeilingMs = debounceMs;
1778
+ const pendingPaths = /* @__PURE__ */ new Set();
1779
+ const watchers = /* @__PURE__ */ new Map();
1780
+ let timer = null;
1781
+ let updateChain = Promise.resolve();
1782
+ let closed = false;
1783
+ let died = false;
1784
+ let holdingEvents = holdEvents;
1785
+ const externalWatchers = /* @__PURE__ */ new Map();
1786
+ let pendingOpened = /* @__PURE__ */ new Map();
1787
+ let capturedDuringSnapshot = [];
1788
+ const externalDiagnosticKey = (root) => `external-watch:${root}`;
1789
+ const failureReason = (err) => {
1790
+ const code = errnoCode(err);
1791
+ if (code !== null && CAPACITY_CODES.has(code)) return `capacity(${code})`;
1792
+ if (/inotify|too many/i.test(String(err))) return "capacity";
1793
+ return code ?? "error";
1794
+ };
1795
+ const degradeExternalRoot = (root, err) => {
1796
+ externalWatchers.get(root)?.close();
1797
+ externalWatchers.delete(root);
1798
+ pendingOpened.get(root)?.close();
1799
+ pendingOpened.delete(root);
1800
+ const message = `ANIMUS_EXTERNAL_WATCH_UNAVAILABLE root=${relative(rootDir, root)} reason=${failureReason(err)} effect=changes in this workspace source may require restart`;
1801
+ session.stickyDiagnostics?.set(externalDiagnosticKey(root), message);
1802
+ console.warn(`[animus-extract] ${message}`);
1803
+ };
1804
+ const flush = () => {
1805
+ timer = null;
1806
+ const modifiedFiles = /* @__PURE__ */ new Set();
1807
+ const removedFiles = /* @__PURE__ */ new Set();
1808
+ for (const path of pendingPaths) (existsSync(path) ? modifiedFiles : removedFiles).add(path);
1809
+ pendingPaths.clear();
1810
+ updateChain = updateChain.then(() => {
1811
+ if (closed) return;
1812
+ return session.handleWatchUpdate({
1813
+ modifiedFiles,
1814
+ removedFiles
1815
+ }).catch((err) => {
1816
+ console.warn(`[animus-extract] watch update failed: ${String(err)}`);
1817
+ }).then(() => {
1818
+ if (pendingOpened.size > 0 || capturedDuringSnapshot.length > 0) rollbackPendingExternal();
1819
+ });
1820
+ });
1821
+ };
1822
+ const rollbackPendingExternal = () => {
1823
+ for (const watcher of pendingOpened.values()) watcher.close();
1824
+ pendingOpened = /* @__PURE__ */ new Map();
1825
+ capturedDuringSnapshot = [];
1826
+ };
1827
+ const closeAll = () => {
1828
+ if (closed) return;
1829
+ closed = true;
1830
+ if (timer) clearTimeout(timer);
1831
+ timer = null;
1832
+ for (const watcher of watchers.values()) watcher.close();
1833
+ watchers.clear();
1834
+ for (const watcher of externalWatchers.values()) watcher.close();
1835
+ externalWatchers.clear();
1836
+ rollbackPendingExternal();
1837
+ session.onExternalRootResolved = null;
1838
+ session.onExternalRootsCommitted = null;
1839
+ activeWatcherRoots.delete(rootDir);
1840
+ };
1841
+ const onWatcherError = (err) => {
1842
+ died = true;
1843
+ closeAll();
1844
+ console.warn(`[animus-extract] Turbopack dev watcher failed (${String(err)}); source edits require a dev-server restart`);
1845
+ handle.onDied?.();
1846
+ };
1847
+ const enqueuePath = (abs) => {
1848
+ pendingPaths.add(abs);
1849
+ if (holdingEvents) return;
1850
+ try {
1851
+ session.noteDebouncedWatchEvents?.([abs]);
1852
+ } catch (err) {
1853
+ console.warn(`[animus-extract] Turbopack debounce status update failed: ${String(err)}`);
1854
+ }
1855
+ if (!timer) {
1856
+ timer = setTimeout(flush, debounceMs);
1857
+ timer.unref?.();
1858
+ }
1859
+ };
1860
+ const onEvent = (baseDir, filename) => {
1861
+ if (closed || !filename) return;
1862
+ const rel = filename.toString();
1863
+ if (rel.split(/[\\/]/).some((segment) => IGNORED_SEGMENTS.has(segment))) return;
1864
+ const abs = join(baseDir, rel);
1865
+ if (baseDir === rootDir && !watchers.has(abs)) try {
1866
+ if (statSync(abs).isDirectory()) addWatcher(abs, true);
1867
+ } catch {}
1868
+ enqueuePath(abs);
1869
+ };
1870
+ const onExternalEvent = (root, self, filename) => {
1871
+ if (closed) return;
1872
+ const watcher = self();
1873
+ const active = watcher !== void 0 && externalWatchers.get(root) === watcher;
1874
+ const isPending = watcher !== void 0 && pendingOpened.get(root) === watcher;
1875
+ if (!active && !isPending) return;
1876
+ let abs;
1877
+ if (filename == null) abs = root;
1878
+ else {
1879
+ const rel = filename.toString();
1880
+ if (rel.split(/[\\/]/).some((segment) => IGNORED_SEGMENTS.has(segment))) return;
1881
+ abs = rel === "" ? root : join(root, rel);
1882
+ }
1883
+ if (isPending && !active) {
1884
+ capturedDuringSnapshot.push(abs);
1885
+ return;
1886
+ }
1887
+ enqueuePath(abs);
1888
+ };
1889
+ const openExternalWatcher = (root, into) => {
1890
+ if (root.split(/[\\/]/).includes("node_modules")) return;
1891
+ try {
1892
+ let watcher;
1893
+ watcher = watchFn(root, { recursive: true }, (_event, filename) => onExternalEvent(root, () => watcher, filename));
1894
+ watcher.on("error", (err) => degradeExternalRoot(root, err));
1895
+ watcher.unref?.();
1896
+ into.set(root, watcher);
1897
+ session.stickyDiagnostics?.delete(externalDiagnosticKey(root));
1898
+ } catch (err) {
1899
+ degradeExternalRoot(root, err);
1900
+ }
1901
+ };
1902
+ const addWatcher = (dir, recursive) => {
1903
+ const watcher = watchFn(dir, { recursive }, (_event, filename) => onEvent(dir, filename));
1904
+ watcher.on("error", onWatcherError);
1905
+ watcher.unref?.();
1906
+ watchers.set(dir, watcher);
1907
+ };
1908
+ try {
1909
+ addWatcher(rootDir, false);
1910
+ for (const entry of readdirSync(rootDir, { withFileTypes: true })) {
1911
+ if (!entry.isDirectory() || IGNORED_SEGMENTS.has(entry.name)) continue;
1912
+ addWatcher(join(rootDir, entry.name), true);
1913
+ }
1914
+ } catch (err) {
1915
+ closeAll();
1916
+ console.warn(`[animus-extract] Turbopack dev watcher unavailable (${String(err)}); source edits require a dev-server restart`);
1917
+ return { kind: "unavailable" };
1918
+ }
1919
+ for (const root of session.externalWatchRoots ?? []) openExternalWatcher(root, externalWatchers);
1920
+ session.onExternalRootResolved = (root) => {
1921
+ if (closed) return;
1922
+ if (externalWatchers.has(root) || pendingOpened.has(root)) return;
1923
+ openExternalWatcher(root, pendingOpened);
1924
+ };
1925
+ session.onExternalRootsCommitted = (roots) => {
1926
+ if (closed) return;
1927
+ const admitted = new Set(roots);
1928
+ for (const [root, watcher] of pendingOpened) externalWatchers.set(root, watcher);
1929
+ pendingOpened = /* @__PURE__ */ new Map();
1930
+ for (const [root, watcher] of [...externalWatchers]) if (!admitted.has(root)) {
1931
+ watcher.close();
1932
+ externalWatchers.delete(root);
1933
+ }
1934
+ for (const abs of capturedDuringSnapshot.splice(0)) enqueuePath(abs);
1935
+ };
1936
+ const handle = {
1937
+ close: closeAll,
1938
+ get died() {
1939
+ return died;
1940
+ },
1941
+ onDied: null,
1942
+ deliverHeldEvents: () => {
1943
+ holdingEvents = false;
1944
+ if (pendingPaths.size > 0) flush();
1945
+ return updateChain;
1946
+ },
1947
+ settle: () => updateChain
1948
+ };
1949
+ return {
1950
+ kind: "started",
1951
+ handle
1952
+ };
1953
+ }
1954
+ //#endregion
1955
+ export { ANIMUS_ARTIFACT_DIR, ANIMUS_CSS_MODULE_ID, CLI_COMMIT_ARTIFACT, CLI_LOCK_ARTIFACT, CLI_LOCK_HEARTBEAT_INTERVAL_MS, CLI_LOCK_STALE_AFTER_MS, ExtractionSession, MANIFEST_ARTIFACT, SESSION_ASSETS_DIR, STYLES_ARTIFACT, SYSTEM_PROPS_ARTIFACT, TURBOPACK_SYSTEM_PROPS_ID, analysisCommitPath, analysisInputsPath, analysisStatusPath, checkLockLiveness, collectSessionAssets, decodeCommitRecord, engineApi, getAnalysisStartedPromise, getAnalyzedHashes, getManifestJson, getReplacementEpoch, getSessionArtifactDir, getSharedCss, getSharedExternalDirs, getSharedExternalEntries, getSharedSystemProps, holdDirectoryClaim, isLockHolderAlive, lockRecordBytes, manifestPath, pruneStaleAssets, readCliLockRecord, readJsonEnvelope, replacementEpochPath, runSessionPipeline, sessionArtifactDir, setAnalysisStartedPromise, setSharedEngine, startTurbopackWatcher, stylesPath, systemPropsPath, verifyCommitRecord };