@animus-ui/extract 0.1.11 → 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 (103) hide show
  1. package/README.md +47 -0
  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 +11 -6
  7. package/dist/index.mjs +2 -2
  8. package/dist/pipeline/analyze-project-args.d.ts +0 -12
  9. package/dist/pipeline/analyze-project-args.d.ts.map +1 -1
  10. package/dist/pipeline/assemble-stylesheet.d.ts +0 -35
  11. package/dist/pipeline/assemble-stylesheet.d.ts.map +1 -1
  12. package/dist/pipeline/asset-placeholders.d.ts +0 -25
  13. package/dist/pipeline/asset-placeholders.d.ts.map +1 -1
  14. package/dist/pipeline/content-hash.d.ts +0 -7
  15. package/dist/pipeline/content-hash.d.ts.map +1 -1
  16. package/dist/pipeline/core-options.d.ts +27 -93
  17. package/dist/pipeline/core-options.d.ts.map +1 -1
  18. package/dist/pipeline/correlate-external-tokens.d.ts +10 -18
  19. package/dist/pipeline/correlate-external-tokens.d.ts.map +1 -1
  20. package/dist/pipeline/discover-files.d.ts +1 -9
  21. package/dist/pipeline/discover-files.d.ts.map +1 -1
  22. package/dist/pipeline/discover-packages.d.ts +18 -154
  23. package/dist/pipeline/discover-packages.d.ts.map +1 -1
  24. package/dist/pipeline/dynamic-prop-config.d.ts +4 -19
  25. package/dist/pipeline/dynamic-prop-config.d.ts.map +1 -1
  26. package/dist/pipeline/engine-adapter.d.ts +16 -52
  27. package/dist/pipeline/engine-adapter.d.ts.map +1 -1
  28. package/dist/pipeline/engine-retirement.d.ts +0 -19
  29. package/dist/pipeline/engine-retirement.d.ts.map +1 -1
  30. package/dist/pipeline/error-diagnostics.d.ts +0 -24
  31. package/dist/pipeline/error-diagnostics.d.ts.map +1 -1
  32. package/dist/pipeline/index.d.ts +9 -8
  33. package/dist/pipeline/index.d.ts.map +1 -1
  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 +32 -37
  37. package/dist/pipeline/manifest-diagnostics.d.ts.map +1 -1
  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 +9 -33
  41. package/dist/pipeline/mdx-preprocessor.d.ts.map +1 -1
  42. package/dist/pipeline/path-aliases.d.ts +0 -19
  43. package/dist/pipeline/path-aliases.d.ts.map +1 -1
  44. package/dist/pipeline/post-process-css.d.ts +0 -8
  45. package/dist/pipeline/post-process-css.d.ts.map +1 -1
  46. package/dist/pipeline/prefix.d.ts +3 -10
  47. package/dist/pipeline/prefix.d.ts.map +1 -1
  48. package/dist/pipeline/replacement-plans.d.ts +10 -34
  49. package/dist/pipeline/replacement-plans.d.ts.map +1 -1
  50. package/dist/pipeline/resolve-asset.d.ts +4 -7
  51. package/dist/pipeline/resolve-asset.d.ts.map +1 -1
  52. package/dist/pipeline/run-analysis.d.ts +15 -27
  53. package/dist/pipeline/run-analysis.d.ts.map +1 -1
  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 +23 -53
  57. package/dist/pipeline/source-identity.d.ts.map +1 -1
  58. package/dist/pipeline/source-ingestion.d.ts +48 -53
  59. package/dist/pipeline/source-ingestion.d.ts.map +1 -1
  60. package/dist/pipeline/static-css.d.ts +4 -13
  61. package/dist/pipeline/static-css.d.ts.map +1 -1
  62. package/dist/pipeline/structural-self-check.d.ts +0 -20
  63. package/dist/pipeline/structural-self-check.d.ts.map +1 -1
  64. package/dist/pipeline/svelte-source-adapter.d.ts +5 -12
  65. package/dist/pipeline/svelte-source-adapter.d.ts.map +1 -1
  66. package/dist/pipeline/system-config.d.ts +15 -32
  67. package/dist/pipeline/system-config.d.ts.map +1 -1
  68. package/dist/pipeline/system-props-module.d.ts +0 -15
  69. package/dist/pipeline/system-props-module.d.ts.map +1 -1
  70. package/dist/pipeline/timing-waterfall.d.ts +0 -11
  71. package/dist/pipeline/timing-waterfall.d.ts.map +1 -1
  72. package/dist/pipeline/tsconfig-paths.d.ts +2 -3
  73. package/dist/pipeline/tsconfig-paths.d.ts.map +1 -1
  74. package/dist/pipeline/unit-fallback.d.ts +0 -5
  75. package/dist/pipeline/unit-fallback.d.ts.map +1 -1
  76. package/dist/pipeline/utils.d.ts +1 -1
  77. package/dist/pipeline/utils.d.ts.map +1 -1
  78. package/dist/pipeline/watch-keys.d.ts +0 -10
  79. package/dist/pipeline/watch-keys.d.ts.map +1 -1
  80. package/dist/{pipeline-lkbOoyAr.mjs → pipeline-DeEENy5C.mjs} +1940 -2263
  81. package/dist/{pipeline-ojNeABi0.cjs → pipeline-x1yBZ2UP.cjs} +1970 -2269
  82. package/dist/session/extraction-session.d.ts +110 -316
  83. package/dist/session/extraction-session.d.ts.map +1 -1
  84. package/dist/session/index.d.ts +6 -20
  85. package/dist/session/index.d.ts.map +1 -1
  86. package/dist/session/published-set.d.ts +45 -25
  87. package/dist/session/published-set.d.ts.map +1 -1
  88. package/dist/session/resolve-packages.d.ts +6 -7
  89. package/dist/session/resolve-packages.d.ts.map +1 -1
  90. package/dist/session/session-paths.d.ts +38 -72
  91. package/dist/session/session-paths.d.ts.map +1 -1
  92. package/dist/session/singleton.d.ts +32 -71
  93. package/dist/session/singleton.d.ts.map +1 -1
  94. package/dist/session/timing.d.ts +0 -5
  95. package/dist/session/timing.d.ts.map +1 -1
  96. package/dist/session/turbopack-orchestrator.d.ts +17 -33
  97. package/dist/session/turbopack-orchestrator.d.ts.map +1 -1
  98. package/dist/session.cjs +530 -677
  99. package/dist/session.mjs +517 -672
  100. package/index-v2.js +0 -9
  101. package/package.json +9 -8
  102. package/dist/pipeline/external-keyframes.d.ts +0 -23
  103. package/dist/pipeline/external-keyframes.d.ts.map +0 -1
package/dist/session.cjs CHANGED
@@ -1,36 +1,25 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_pipeline = require("./pipeline-ojNeABi0.cjs");
2
+ const require_pipeline = require("./pipeline-x1yBZ2UP.cjs");
3
3
  let crypto = require("crypto");
4
4
  let fs = require("fs");
5
5
  let path = require("path");
6
6
  //#region session/session-paths.ts
7
7
  /**
8
- * Session-scoped artifact names, path derivations, and the analysis-status
9
- * shape (openspec: next-turbopack-served-transform-coherence, design D1–D3)
10
- * — shared by the session writer (extraction-session), the webpack
11
- * plugin/loader, and the Turbopack loader.
12
- *
13
- * Deliberately free of singleton/session imports: the Turbopack loader
14
- * executes in isolated worker processes and its module graph must stay
15
- * worker-local (guardrail G1) — this module is pure path/shape vocabulary.
8
+ * Session artifact names, path derivations, and artifact shapes. Imports no
9
+ * session or singleton module: the Turbopack loader graph stays worker-local.
16
10
  */
17
- /** Session-dir-relative name of the replacement-epoch disk witness
18
- * (openspec: next-webpack-served-transform-coherence, design D2;
19
- * session-scoped by next-turbopack-served-transform-coherence D2). */
11
+ /** Session-dir-relative name of the replacement-epoch disk witness. */
20
12
  const REPLACEMENT_EPOCH_ARTIFACT = "replacements-epoch";
21
- /** Virtual module id of the emitted system-props module — session
22
- * vocabulary shared by the orchestrator and the Turbopack config
23
- * assembly (hoisted here so the session home stays framework-free). */
13
+ /** Virtual module id of the emitted system-props module. */
24
14
  const TURBOPACK_SYSTEM_PROPS_ID = "virtual:animus/system-props";
25
- /** Session-dir-relative name of the analysis-commit transaction artifact
26
- * (design D1). */
15
+ /** Session-dir-relative name of the analysis-commit transaction artifact. */
27
16
  const ANALYSIS_COMMIT_ARTIFACT = "analysis-commit";
28
- /** Session-dir-relative name of the analysis-status artifact (design D3). */
17
+ /** Session-dir-relative name of the analysis-status artifact. */
29
18
  const ANALYSIS_STATUS_ARTIFACT = "analysis-status.json";
30
19
  /** Session-dir-relative name of the manifest payload artifact. */
31
20
  const MANIFEST_ARTIFACT = "manifest.json";
32
- /** Session-dir-relative name of the analysis-inputs payload artifact
33
- * (Turbopack loader-worker hydration corpus). */
21
+ /** Session-dir-relative name of the analysis-inputs payload artifact — the
22
+ * Turbopack loader-worker hydration corpus. */
34
23
  const ANALYSIS_INPUTS_ARTIFACT = "analysis-inputs.json";
35
24
  /** Session-dir-relative name of the emitted stylesheet artifact. */
36
25
  const STYLES_ARTIFACT = "styles.css";
@@ -38,126 +27,226 @@ const STYLES_ARTIFACT = "styles.css";
38
27
  const SYSTEM_PROPS_ARTIFACT = "system-props.js";
39
28
  /** Session-dir-relative directory holding copied asset() files. */
40
29
  const SESSION_ASSETS_DIR = "assets";
41
- /** The standalone CLI's published-set commit record, written into the flat
42
- * `.animus/` tree (openspec: standalone-extraction-cli D3). Shared here so
43
- * the CLI writer and the session's start hygiene agree on ONE name — the
44
- * hygiene confinement gate keys on this record, and a renamed literal on
45
- * either side would silently re-arm deletion of the CLI's published output. */
30
+ /** The standalone CLI's published-set commit record in the flat `.animus/`
31
+ * tree; the session's start hygiene keys on this name before deleting. */
46
32
  const CLI_COMMIT_ARTIFACT = "commit.json";
47
- /** The standalone CLI's single-writer advisory lock in the flat `.animus/`
48
- * tree. A live holder means a CLI invocation owns that tree right now. */
33
+ /** Advisory claim record (pid plus heartbeat) of whichever directory holds
34
+ * it; sessions write it into their own tree too, despite the `CLI_` name. */
49
35
  const CLI_LOCK_ARTIFACT = "lock.json";
50
- /** Root of every session-scoped artifact tree for a project. */
36
+ /** Project-relative artifact directory: the CLI's default output tree and
37
+ * the parent of every session tree. One spelling, or the tree splits. */
38
+ const ANIMUS_ARTIFACT_DIR = ".animus";
39
+ /** Module id the Rust emitter injects, and the alias key both bundlers
40
+ * register. A literal on purpose: a rename breaks built consumer packages.
41
+ * The Next plugin's webpack alias harvest skips this exact key, so a change
42
+ * here is a change in the emitter, both bundler arms and that harvest. */
43
+ const ANIMUS_CSS_MODULE_ID = ".animus/styles.css";
51
44
  function sessionsRootDir(rootDir) {
52
- return (0, path.join)(rootDir, ".animus", "sessions");
45
+ return (0, path.join)(rootDir, ANIMUS_ARTIFACT_DIR, "sessions");
53
46
  }
54
- /** One session's artifact directory (design D2: session-scoped trees,
55
- * no global pointer). */
56
47
  function sessionArtifactDir(rootDir, sessionId) {
57
48
  return (0, path.join)(sessionsRootDir(rootDir), sessionId);
58
49
  }
59
- /** Absolute path of a session's replacement-epoch artifact — the single
60
- * derivation shared by the session writer, the webpack plugin's
61
- * watch-ignore entry, and both loaders' file dependencies. Takes the
62
- * SESSION directory (not the project root) since the artifact was
63
- * relocated into the session tree. */
64
50
  function replacementEpochPath(sessionDir) {
65
51
  return (0, path.join)(sessionDir, REPLACEMENT_EPOCH_ARTIFACT);
66
52
  }
67
- /** Absolute path of a session's analysis-commit artifact. */
68
53
  function analysisCommitPath(sessionDir) {
69
54
  return (0, path.join)(sessionDir, ANALYSIS_COMMIT_ARTIFACT);
70
55
  }
71
- /** Absolute path of a session's analysis-status artifact. */
72
56
  function analysisStatusPath(sessionDir) {
73
57
  return (0, path.join)(sessionDir, ANALYSIS_STATUS_ARTIFACT);
74
58
  }
75
- /** Absolute path of a session's manifest artifact. */
76
59
  function manifestPath(sessionDir) {
77
60
  return (0, path.join)(sessionDir, MANIFEST_ARTIFACT);
78
61
  }
79
- /** Absolute path of a session's analysis-inputs artifact. */
80
62
  function analysisInputsPath(sessionDir) {
81
63
  return (0, path.join)(sessionDir, ANALYSIS_INPUTS_ARTIFACT);
82
64
  }
83
- /** Absolute path of a session's stylesheet artifact. */
84
65
  function stylesPath(sessionDir) {
85
66
  return (0, path.join)(sessionDir, STYLES_ARTIFACT);
86
67
  }
87
- /** Absolute path of a session's system-props module artifact. */
88
68
  function systemPropsPath(sessionDir) {
89
69
  return (0, path.join)(sessionDir, SYSTEM_PROPS_ARTIFACT);
90
70
  }
91
- /** Splice the session envelope into a JSON-object payload as a leading
92
- * `__animusSession` field, byte-preserving the payload's own content
93
- * (parse/re-stringify could reformat engine-emitted JSON). */
71
+ /** Splices the envelope in as a leading `__animusSession` field without
72
+ * re-serializing: parse/re-stringify would reformat engine-emitted JSON. */
94
73
  function envelopeJsonArtifact(payload, envelopeJson) {
95
74
  const trimmed = payload.trimStart();
96
75
  if (!trimmed.startsWith("{")) return payload;
97
76
  if (trimmed.slice(1).trimStart().startsWith("}")) return `{"__animusSession":${envelopeJson}}`;
98
77
  return `{"__animusSession":${envelopeJson},${trimmed.slice(1)}`;
99
78
  }
100
- /** Trailing-comment side-band for the CSS artifact (a leading comment
101
- * would displace the @layer declaration consumers pin at offset 0). */
79
+ /** Trailing-comment side-band: a leading comment would displace the `@layer`
80
+ * declaration consumers pin at offset 0. */
102
81
  function envelopeCssArtifact(payload, envelopeJson) {
103
82
  return `${payload}\n/* __animusSession ${envelopeJson} */\n`;
104
83
  }
105
- /** Matches the CSS artifact's envelope side-band. */
106
84
  const ENVELOPE_CSS_COMMENT_RE = /\/\* __animusSession (\{.*\}) \*\//;
107
- /** Envelope of a JSON artifact's bytes, or undefined when the artifact
108
- * carries none. THROWS on unparseable bytes — callers decide whether a
109
- * torn artifact fails closed or degrades. */
85
+ /** Envelope of a JSON artifact's bytes, or undefined when it carries none.
86
+ * Throws on unparseable bytes; callers decide torn-artifact policy. */
110
87
  function readJsonEnvelope(bytes) {
111
88
  return JSON.parse(bytes).__animusSession;
112
89
  }
113
- /** Envelope of the CSS artifact's side-band comment, or undefined when the
114
- * side-band is absent. THROWS on an unparseable envelope body. */
90
+ /** Envelope of the CSS side-band comment, or undefined when it is absent.
91
+ * Throws on an unparseable envelope body. */
115
92
  function readCssEnvelope(bytes) {
116
93
  const match = bytes.match(ENVELOPE_CSS_COMMENT_RE);
117
94
  return match ? JSON.parse(match[1]) : void 0;
118
95
  }
119
96
  //#endregion
120
97
  //#region session/published-set.ts
121
- /**
122
- * The CLI published-set contract, shared logic (openspec:
123
- * standalone-extraction-cli D3): ONE implementation of "does this flat
124
- * artifact set verify against its commit record" and ONE collector for the
125
- * session's copied asset() files — consumed by the CLI writer
126
- * (staging/verification) and the session's start hygiene (confinement
127
- * gate). Two copies of either had already drifted once: the hygiene copy
128
- * verified payloads as UTF-8 strings while the writer hashed raw bytes, so
129
- * any asset-carrying set (fonts are not UTF-8) could never verify and a
130
- * legitimately published set was classified as debris.
131
- */
132
- /**
133
- * Recompute every payload hash recorded in the directory's commit record
134
- * and compare — raw bytes on both sides, matching the writer's hashing
135
- * domain. Returns failure lines; empty means the set verifies. An absent
136
- * or unparsable record is itself a failure (the record is written LAST, so
137
- * a torn or aborted publish cannot verify).
138
- */
98
+ function isPublishedSetRecord(value) {
99
+ return Object.prototype.toString.call(value) === "[object Object]";
100
+ }
101
+ function isPublishedSetString(value) {
102
+ return Object.prototype.toString.call(value) === "[object String]";
103
+ }
104
+ function isPublishedSetNumber(value) {
105
+ return Object.prototype.toString.call(value) === "[object Number]";
106
+ }
107
+ function decodeCommitRecord(bytes) {
108
+ let candidate;
109
+ try {
110
+ candidate = JSON.parse(bytes);
111
+ } catch {
112
+ return null;
113
+ }
114
+ if (!isPublishedSetRecord(candidate) || candidate.schema !== 1) return null;
115
+ const declared = candidate.payloads;
116
+ if (!isPublishedSetRecord(declared)) return null;
117
+ const payloads = {};
118
+ for (const [name, entry] of Object.entries(declared)) {
119
+ if (!isPublishedSetRecord(entry) || !isPublishedSetString(entry.hash)) return null;
120
+ payloads[name] = { hash: entry.hash };
121
+ }
122
+ return {
123
+ schema: 1,
124
+ payloads
125
+ };
126
+ }
127
+ /** Recompute every recorded payload hash over raw bytes — the writer's own
128
+ * hashing domain — and return the mismatches; empty means the set verifies. */
139
129
  function verifyCommitRecord(dir) {
140
- let record;
130
+ let bytes;
141
131
  try {
142
- record = JSON.parse((0, fs.readFileSync)((0, path.join)(dir, CLI_COMMIT_ARTIFACT), "utf-8"));
132
+ bytes = (0, fs.readFileSync)((0, path.join)(dir, CLI_COMMIT_ARTIFACT), "utf-8");
143
133
  } catch (error) {
144
134
  return [`commit record unreadable: ${String(error)}`];
145
135
  }
146
- if (record.schema !== 1 || typeof record.payloads !== "object" || record.payloads === null) return ["commit record is not a schema-1 payload record"];
136
+ const record = decodeCommitRecord(bytes);
137
+ if (record === null) return ["commit record is not a schema-1 payload record"];
147
138
  const failures = [];
148
139
  for (const [name, entry] of Object.entries(record.payloads)) try {
149
- if (require_pipeline.contentHash((0, fs.readFileSync)((0, path.join)(dir, name))) !== entry?.hash) failures.push(`${name}: bytes do not match the commit record`);
140
+ if (require_pipeline.contentHash((0, fs.readFileSync)((0, path.join)(dir, name))) !== entry.hash) failures.push(`${name}: bytes do not match the commit record`);
150
141
  } catch (error) {
151
142
  failures.push(`${name}: unreadable (${String(error)})`);
152
143
  }
153
144
  return failures;
154
145
  }
155
- /**
156
- * Snapshot the session's copied asset() files (sorted by name) — read
157
- * BEFORE the session tree is disposed. The emitted stylesheet references
158
- * them as `./assets/<name>`, so every driver must publish them beside its
159
- * stylesheet or the urls dangle.
160
- */
146
+ const CLI_LOCK_HEARTBEAT_INTERVAL_MS = 6e4;
147
+ /** A claim whose heartbeat is older than this has no live holder. Five missed
148
+ * refreshes: synchronous extraction can block the holder's refresh timer. */
149
+ const CLI_LOCK_STALE_AFTER_MS = 5 * CLI_LOCK_HEARTBEAT_INTERVAL_MS;
150
+ /** Absence of the file — the only read failure that means "no holder".
151
+ * ENOTDIR joins ENOENT: the path cannot exist under a non-directory. */
152
+ function isMissingFile(error) {
153
+ return error instanceof Object && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR");
154
+ }
155
+ function readCliLockRecord(dir) {
156
+ let bytes;
157
+ try {
158
+ bytes = (0, fs.readFileSync)((0, path.join)(dir, CLI_LOCK_ARTIFACT), "utf-8");
159
+ } catch (error) {
160
+ if (isMissingFile(error)) return { kind: "none" };
161
+ throw error;
162
+ }
163
+ let candidate;
164
+ try {
165
+ candidate = JSON.parse(bytes);
166
+ } catch {
167
+ return { kind: "indeterminate" };
168
+ }
169
+ if (!isPublishedSetRecord(candidate) || !isPublishedSetNumber(candidate.pid)) return { kind: "indeterminate" };
170
+ const startedAt = candidate.startedAt;
171
+ const heartbeatAt = candidate.heartbeatAt;
172
+ const record = { pid: candidate.pid };
173
+ if (isPublishedSetString(startedAt)) record.startedAt = startedAt;
174
+ if (isPublishedSetString(heartbeatAt)) record.heartbeatAt = heartbeatAt;
175
+ return {
176
+ kind: "held",
177
+ record
178
+ };
179
+ }
180
+ function isPermissionDenied(error) {
181
+ return error instanceof Object && "code" in error && error.code === "EPERM";
182
+ }
183
+ function isLockHolderAlive(pid) {
184
+ try {
185
+ process.kill(pid, 0);
186
+ return true;
187
+ } catch (error) {
188
+ return isPermissionDenied(error);
189
+ }
190
+ }
191
+ function checkLockLiveness(record) {
192
+ if (!isLockHolderAlive(record.pid)) return {
193
+ live: false,
194
+ reason: `holder pid ${record.pid} is not running`
195
+ };
196
+ const beatAt = record.heartbeatAt === void 0 ? NaN : Date.parse(record.heartbeatAt);
197
+ if (Number.isNaN(beatAt)) return { live: true };
198
+ const ageMs = Date.now() - beatAt;
199
+ if (ageMs <= 3e5) return { live: true };
200
+ return {
201
+ live: false,
202
+ reason: `holder pid ${record.pid} is running but its lock heartbeat (${record.heartbeatAt}) is ${Math.round(ageMs / 1e3)}s old, past the ${CLI_LOCK_STALE_AFTER_MS / 1e3}s staleness window — the pid names a different process now`
203
+ };
204
+ }
205
+ function lockRecordBytes(startedAt) {
206
+ return JSON.stringify({
207
+ pid: process.pid,
208
+ startedAt,
209
+ heartbeatAt: (/* @__PURE__ */ new Date()).toISOString()
210
+ });
211
+ }
212
+ const HEARTBEAT_TEMP_PREFIX = ".lock-heartbeat-";
213
+ function holdDirectoryClaim(dir, options = {}) {
214
+ const startedAt = options.startedAt ?? (/* @__PURE__ */ new Date()).toISOString();
215
+ const path$9 = (0, path.join)(dir, CLI_LOCK_ARTIFACT);
216
+ const tempPath = (0, path.join)(dir, `${HEARTBEAT_TEMP_PREFIX}${process.pid}`);
217
+ const rename = options.write === "rename";
218
+ let refresh = null;
219
+ let stopped = false;
220
+ const stop = () => {
221
+ stopped = true;
222
+ if (refresh !== null) clearInterval(refresh);
223
+ refresh = null;
224
+ };
225
+ const stamp = () => {
226
+ try {
227
+ if (rename) {
228
+ (0, fs.writeFileSync)(tempPath, lockRecordBytes(startedAt));
229
+ (0, fs.renameSync)(tempPath, path$9);
230
+ } else (0, fs.writeFileSync)(path$9, lockRecordBytes(startedAt));
231
+ } catch (error) {
232
+ stop();
233
+ if (isMissingFile(error)) return;
234
+ console.error(`[animus] Could not write the directory claim ${path$9}: ${String(error)}`);
235
+ }
236
+ };
237
+ stamp();
238
+ if (!stopped) {
239
+ refresh = setInterval(stamp, CLI_LOCK_HEARTBEAT_INTERVAL_MS);
240
+ refresh.unref?.();
241
+ }
242
+ return () => {
243
+ stop();
244
+ if (rename) (0, fs.rmSync)(tempPath, { force: true });
245
+ (0, fs.rmSync)(path$9, { force: true });
246
+ };
247
+ }
248
+ /** Snapshot the session's copied asset() files, sorted, BEFORE the session
249
+ * tree is disposed. Drivers publish them beside the emitted stylesheet. */
161
250
  function collectSessionAssets(sessionDir) {
162
251
  if (!sessionDir) return [];
163
252
  const dir = (0, path.join)(sessionDir, SESSION_ASSETS_DIR);
@@ -169,12 +258,6 @@ function collectSessionAssets(sessionDir) {
169
258
  }
170
259
  //#endregion
171
260
  //#region session/resolve-packages.ts
172
- /**
173
- * Resolve external DS package specifiers to rootDir-relative entry paths.
174
- * Node-generic (no webpack APIs): Pass 1 walks the consumer's workspaces for
175
- * matching package names; Pass 2 falls back to `require.resolve`. Specifiers
176
- * that resolve nowhere are simply omitted (spec: silent skip).
177
- */
178
261
  function resolvePackagesByName(rootDir, names) {
179
262
  if (names.length === 0) return {};
180
263
  const nameSet = new Set(names);
@@ -190,7 +273,8 @@ function resolvePackagesByName(rootDir, names) {
190
273
  const pkg = JSON.parse((0, fs.readFileSync)((0, path.join)(wsDir, "package.json"), "utf-8"));
191
274
  const name = pkg.name || "";
192
275
  if (nameSet.has(name)) {
193
- const entryPath = (0, path.resolve)(wsDir, pkg.main || pkg.module || "index.ts");
276
+ const main = pkg.main || pkg.module || "index.ts";
277
+ const entryPath = (0, path.resolve)(wsDir, main);
194
278
  if ((0, fs.existsSync)(entryPath)) {
195
279
  packageMap[name] = (0, path.relative)(rootDir, entryPath);
196
280
  resolved.add(name);
@@ -202,7 +286,8 @@ function resolvePackagesByName(rootDir, names) {
202
286
  for (const name of nameSet) {
203
287
  if (resolved.has(name)) continue;
204
288
  try {
205
- packageMap[name] = (0, path.relative)(rootDir, require.resolve(name, { paths: [rootDir] }));
289
+ const entryPath = require.resolve(name, { paths: [rootDir] });
290
+ packageMap[name] = (0, path.relative)(rootDir, entryPath);
206
291
  } catch {}
207
292
  }
208
293
  return packageMap;
@@ -210,86 +295,96 @@ function resolvePackagesByName(rootDir, names) {
210
295
  //#endregion
211
296
  //#region session/singleton.ts
212
297
  /**
213
- * Module-scope singleton for sharing analysis state between the webpack plugin and loader.
214
- *
215
- * Uses globalThis to survive the ESM/CJS module boundary: the plugin is loaded via ESM
216
- * (next.config.ts import) while the loader is loaded by webpack via require(). Without
217
- * globalThis, each module system gets its own singleton instance.
298
+ * globalThis-backed so the ESM plugin copy and the CJS webpack-loader copy
299
+ * share one store; module scope alone gives each copy its own instance.
218
300
  */
219
301
  const MANIFEST_KEY = "__animus_manifest_json__";
220
- const PROMISE_KEY = "__animus_analysis_promise__";
302
+ const ANALYSIS_STARTED_KEY = "__animus_analysis_started__";
221
303
  const SHARED_CSS_KEY = "__animus_shared_css__";
222
304
  const SHARED_SYSTEM_PROPS_KEY = "__animus_shared_system_props__";
223
305
  const SHARED_EXTERNAL_DIRS_KEY = "__animus_external_pkg_dirs__";
224
306
  const SHARED_EXTERNAL_ENTRIES_KEY = "__animus_external_source_entries__";
307
+ const ANALYZED_HASHES_KEY = "__animus_analyzed_hashes__";
308
+ const REPLACEMENT_EPOCH_KEY = "__animus_replacement_epoch__";
309
+ const WATCH_TRANSACTION_KEY = "__animus_watch_transaction__";
310
+ const PROCESS_SESSION_ID_KEY = "__animus_process_session_id__";
311
+ const SESSION_ARTIFACT_DIR_KEY = "__animus_session_artifact_dir__";
312
+ const OWNING_WATCH_SESSION_KEY = "__animus_owning_watch_session__";
313
+ const EXCLUSIVE_SESSION_OWNER_KEY = "__animus_exclusive_session_owner__";
314
+ const ENGINE_KEY = "__animus_engine__";
315
+ const V2_ENGINE_KEY = "__animus_v2_engine__";
316
+ const V2_SENT_SOURCES_KEY = "__animus_v2_sent_sources__";
317
+ const V2_DRIFT_WARNED_KEY = "__animus_v2_drift_warned__";
318
+ const ENGINE_API_OVERRIDE_KEY = "__animus_engine_api_override__";
319
+ const singletonStore = globalThis;
320
+ const isSingletonString = (value) => {
321
+ if (Object(value) === value) return false;
322
+ try {
323
+ String.prototype.valueOf.call(value);
324
+ return true;
325
+ } catch {
326
+ return false;
327
+ }
328
+ };
225
329
  function getManifestJson() {
226
- return globalThis[MANIFEST_KEY];
330
+ return singletonStore[MANIFEST_KEY];
227
331
  }
228
332
  function setManifestJson(json) {
229
- globalThis[MANIFEST_KEY] = json;
333
+ singletonStore[MANIFEST_KEY] = json;
230
334
  }
231
- function getAnalysisPromise() {
232
- return globalThis[PROMISE_KEY];
335
+ /**
336
+ * A latch over the most recently STARTED analysis, never cleared on settle:
337
+ * non-null means one was scheduled. In-flight lives in getWatchTransaction.
338
+ */
339
+ function getAnalysisStartedPromise() {
340
+ return singletonStore[ANALYSIS_STARTED_KEY];
233
341
  }
234
- function setAnalysisPromise(promise) {
235
- globalThis[PROMISE_KEY] = promise;
342
+ function setAnalysisStartedPromise(promise) {
343
+ singletonStore[ANALYSIS_STARTED_KEY] = promise;
236
344
  }
237
- function resetAnalysisPromise() {
238
- globalThis[PROMISE_KEY] = null;
345
+ function resetAnalysisStartedPromise() {
346
+ singletonStore[ANALYSIS_STARTED_KEY] = null;
239
347
  }
240
348
  function getSharedCss() {
241
- return globalThis[SHARED_CSS_KEY] || "";
349
+ return singletonStore[SHARED_CSS_KEY] || "";
242
350
  }
243
351
  function setSharedCss(css) {
244
- globalThis[SHARED_CSS_KEY] = css;
352
+ singletonStore[SHARED_CSS_KEY] = css;
245
353
  }
246
354
  function getSharedSystemProps() {
247
- return globalThis[SHARED_SYSTEM_PROPS_KEY] || "";
355
+ return singletonStore[SHARED_SYSTEM_PROPS_KEY] || "";
248
356
  }
249
357
  function setSharedSystemProps(content) {
250
- globalThis[SHARED_SYSTEM_PROPS_KEY] = content;
358
+ singletonStore[SHARED_SYSTEM_PROPS_KEY] = content;
251
359
  }
252
360
  function getSharedExternalDirs() {
253
- return globalThis[SHARED_EXTERNAL_DIRS_KEY] || [];
361
+ return singletonStore[SHARED_EXTERNAL_DIRS_KEY] || [];
254
362
  }
255
363
  function setSharedExternalDirs(dirs) {
256
- globalThis[SHARED_EXTERNAL_DIRS_KEY] = dirs;
364
+ singletonStore[SHARED_EXTERNAL_DIRS_KEY] = dirs;
257
365
  }
258
366
  function getSharedExternalEntries() {
259
- return globalThis[SHARED_EXTERNAL_ENTRIES_KEY] || /* @__PURE__ */ new Map();
367
+ return singletonStore[SHARED_EXTERNAL_ENTRIES_KEY] || /* @__PURE__ */ new Map();
260
368
  }
261
369
  function setSharedExternalEntries(entries) {
262
- globalThis[SHARED_EXTERNAL_ENTRIES_KEY] = entries;
370
+ singletonStore[SHARED_EXTERNAL_ENTRIES_KEY] = entries;
263
371
  }
264
- /** One typed globalThis slot — the single accessor shape every
265
- * singleton-published value shares (null when unset). */
266
372
  function globalSlot(key) {
267
- const store = globalThis;
268
373
  return {
269
- get: () => store[key] ?? null,
374
+ get: () => singletonStore[key] ?? null,
270
375
  set: (value) => {
271
- store[key] = value;
376
+ singletonStore[key] = value;
272
377
  }
273
378
  };
274
379
  }
275
- const ANALYZED_HASHES_KEY = "__animus_analyzed_hashes__";
276
- const REPLACEMENT_EPOCH_KEY = "__animus_replacement_epoch__";
277
- const WATCH_TRANSACTION_KEY = "__animus_watch_transaction__";
278
- const PROCESS_SESSION_ID_KEY = "__animus_process_session_id__";
279
- const SESSION_ARTIFACT_DIR_KEY = "__animus_session_artifact_dir__";
280
- const OWNING_WATCH_SESSION_KEY = "__animus_owning_watch_session__";
281
- const EXCLUSIVE_SESSION_OWNER_KEY = "__animus_exclusive_session_owner__";
282
380
  const analyzedHashesSlot = globalSlot(ANALYZED_HASHES_KEY);
283
381
  const replacementEpochSlot = globalSlot(REPLACEMENT_EPOCH_KEY);
284
382
  const watchTransactionSlot = globalSlot(WATCH_TRANSACTION_KEY);
285
383
  const sessionArtifactDirSlot = globalSlot(SESSION_ARTIFACT_DIR_KEY);
286
384
  const owningWatchSessionSlot = globalSlot(OWNING_WATCH_SESSION_KEY);
287
385
  /**
288
- * The session that completed the full pipeline and holds system state — the
289
- * only instance that can run watch analysis. Each MultiCompiler child holds
290
- * its own session AND its own watcher with its own modified set; a
291
- * non-owning session forwards its batch here instead of dropping it (a file
292
- * only that compiler watches would otherwise never be analyzed).
386
+ * The only session that can run watch analysis. A non-owning session forwards
387
+ * its batch here; dropping it loses files only that watcher sees.
293
388
  */
294
389
  function getOwningWatchSession() {
295
390
  return owningWatchSessionSlot.get();
@@ -298,12 +393,8 @@ function setOwningWatchSession(session) {
298
393
  owningWatchSessionSlot.set(session);
299
394
  }
300
395
  /**
301
- * Per-file analyzed content hashes of the last published analysis (relPath →
302
- * contentHash of the exact bytes analyzed) — the loader's witness for the
303
- * unconditional catching-up guard (openspec:
304
- * next-webpack-served-transform-coherence, design D4). Includes files
305
- * analyzed with ZERO animus entries: analyzed identity is membership in the
306
- * analysis input set, not manifest entry count.
396
+ * Per-file content hashes of the last published analysis (relPath → hash of
397
+ * the exact bytes analyzed), including files that produced zero entries.
307
398
  */
308
399
  function getAnalyzedHashes() {
309
400
  return analyzedHashesSlot.get();
@@ -312,52 +403,31 @@ function setAnalyzedHashes(hashes) {
312
403
  analyzedHashesSlot.set(hashes);
313
404
  }
314
405
  /**
315
- * Session identity is per Next INVOCATION — one process, however many
316
- * compiler instances it holds (openspec:
317
- * next-turbopack-served-transform-coherence, design D2). The first
318
- * ExtractionSession constructed in a process claims a fresh randomUUID;
319
- * every later instance (client/server/RSC compilers each construct one)
320
- * adopts it, so all compilers alias, watch-ignore, and publish ONE
321
- * session-scoped artifact tree. Separate invocations (`next dev` +
322
- * `next build` co-writing) are separate processes and therefore separate
323
- * sessions by construction.
406
+ * Session identity is per process: the first caller claims a fresh id and
407
+ * every later one adopts it, so all compilers share one artifact tree.
324
408
  */
325
409
  function claimProcessSessionId() {
326
- const store = globalThis;
327
- const existing = store[PROCESS_SESSION_ID_KEY];
328
- if (typeof existing === "string" && existing.length > 0) return existing;
410
+ const existing = singletonStore[PROCESS_SESSION_ID_KEY];
411
+ if (isSingletonString(existing) && existing.length > 0) return existing;
329
412
  const fresh = (0, crypto.randomUUID)();
330
- store[PROCESS_SESSION_ID_KEY] = fresh;
413
+ singletonStore[PROCESS_SESSION_ID_KEY] = fresh;
331
414
  return fresh;
332
415
  }
333
416
  /**
334
- * Exclusive-ownership claim over the process-global session state — the
335
- * hard form of the invariant `claimProcessSessionId` documents softly. The
336
- * shared slots above (manifest, css, artifact dir) plus the one session id
337
- * mean two CONCURRENT drive loops in one process (a webpack MultiCompiler
338
- * array config, parallel rollup array builds, two programmatic CLI runs)
339
- * would share one session directory, overwrite each other's manifests, and
340
- * delete each other's live trees on dispose. Drivers that run the loop
341
- * exclusively (the unplugin host) claim for the build's lifetime and
342
- * release on dispose; SEQUENTIAL claim/release cycles are legal. The Next
343
- * multi-compiler adoption path stays the non-exclusive default. Throws
344
- * naming both hosts and the remediation on overlap. The key lives in
345
- * `SINGLETON_GLOBAL_KEYS`, so per-test global resets clear a leaked claim.
417
+ * Two CONCURRENT drive loops in one process would share one session tree and
418
+ * clobber each other; sequential claim/release cycles are legal.
346
419
  */
347
420
  function claimExclusiveSessionOwner(label) {
348
- const store = globalThis;
349
- const active = store[EXCLUSIVE_SESSION_OWNER_KEY];
350
- if (typeof active === "string") 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.`);
351
- store[EXCLUSIVE_SESSION_OWNER_KEY] = label;
421
+ const active = singletonStore[EXCLUSIVE_SESSION_OWNER_KEY];
422
+ 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.`);
423
+ singletonStore[EXCLUSIVE_SESSION_OWNER_KEY] = label;
352
424
  return () => {
353
- if (store[EXCLUSIVE_SESSION_OWNER_KEY] === label) delete store[EXCLUSIVE_SESSION_OWNER_KEY];
425
+ if (singletonStore[EXCLUSIVE_SESSION_OWNER_KEY] === label) delete singletonStore[EXCLUSIVE_SESSION_OWNER_KEY];
354
426
  };
355
427
  }
356
428
  /**
357
- * Absolute session artifact directory of the OWNING session's last
358
- * publication — the webpack loader's source for the session-scoped epoch
359
- * dependency path (the loader shares the process with the pipeline; the
360
- * Turbopack loader instead receives the directory via its options).
429
+ * Absolute artifact directory of the OWNING session's last publication. The
430
+ * Turbopack loader runs out of process and receives it via options instead.
361
431
  */
362
432
  function getSessionArtifactDir() {
363
433
  return sessionArtifactDirSlot.get();
@@ -366,14 +436,8 @@ function setSessionArtifactDir(dir) {
366
436
  sessionArtifactDirSlot.set(dir);
367
437
  }
368
438
  /**
369
- * Canonical replacement epoch of the last published analysis
370
- * (`hashReplacementPlans(snapshotFilePlans(manifest), systemPropsContent)`
371
- * — openspec: next-webpack-served-transform-coherence, design D5; the
372
- * served system-props module rides as the served-dependency witness so
373
- * offline system-props changes move the epoch). Published by the
374
- * owning session AFTER the manifest so a reader that observes the epoch
375
- * always observes at least that generation's manifest. (Session
376
- * attribution lives on the DISK epoch artifact — no in-process mirror.)
439
+ * Replacement epoch of the last published analysis, written AFTER the
440
+ * manifest so an epoch reader always sees at least that generation's manifest.
377
441
  */
378
442
  function getReplacementEpoch() {
379
443
  return replacementEpochSlot.get();
@@ -382,10 +446,8 @@ function setReplacementEpoch(epoch) {
382
446
  replacementEpochSlot.set(epoch);
383
447
  }
384
448
  /**
385
- * The one in-flight watch-analysis transaction (design D3): the first
386
- * compiler entering a watch batch runs analysis + publication; every other
387
- * compiler (client/server/RSC — each holds its own session instance) joins
388
- * this promise instead of proceeding against a pre-transaction generation.
449
+ * The one in-flight watch-analysis transaction: a compiler that did not start
450
+ * it joins this promise rather than read a pre-transaction generation.
389
451
  */
390
452
  function getWatchTransaction() {
391
453
  return watchTransactionSlot.get();
@@ -393,69 +455,49 @@ function getWatchTransaction() {
393
455
  function setWatchTransaction(transaction) {
394
456
  watchTransactionSlot.set(transaction);
395
457
  }
396
- const ENGINE_KEY = "__animus_engine__";
397
- /** Engine selection travels through the singleton so non-owning compiler
398
- * instances and the webpack loader honor the same choice as the owner. */
458
+ /** Engine selection travels through the singleton so non-owning compilers
459
+ * and the webpack loader honor the owner's choice. */
399
460
  function setSharedEngine(engine) {
400
- globalThis[ENGINE_KEY] = engine;
461
+ singletonStore[ENGINE_KEY] = engine;
401
462
  }
402
463
  function getSharedEngine() {
403
- return globalThis[ENGINE_KEY] || "v2";
464
+ return singletonStore[ENGINE_KEY] || "v2";
404
465
  }
405
- /** Single engine choke-point for every native extraction call. Return type
406
- * mirrors the untyped `require` the call sites previously used — the NAPI
407
- * module's own .d.ts is the authoritative surface. */
466
+ /** Single choke-point for every native extraction call; the NAPI module's
467
+ * own `.d.ts` is the authoritative surface for what it returns. */
408
468
  function requireEngine() {
409
469
  return require("@animus-ui/extract");
410
470
  }
411
- const V2_ENGINE_KEY = "__animus_v2_engine__";
412
471
  /**
413
- * Engine-agnostic API over both engines (extract-v2-spine row 13). The
414
- * v2 leg adapts the v1 function surface onto a stateful ExtractEngine
415
- * (hoisted to `createV2EngineApi` in @animus-ui/extract/pipeline — the
416
- * single authoritative copy shared with vite-plugin). The engine INSTANCE,
417
- * its analyze-time sources, and the one-shot drift flag all live on
418
- * globalThis for the same reason the manifest does — the ESM plugin and the
419
- * CJS webpack loader must see one engine (and one drift warning) across the
420
- * double-load. (The next-plugin is already process-singleton by design:
421
- * manifest, css, and system props share the same globalThis keys.)
422
- * loadSystemModule is exported by both bindings from one engine-neutral
423
- * Rust crate, so the default path no longer loads the v1 binary.
472
+ * The one engine API for this process: the instance, its analyze-time
473
+ * sources, and the drift flag live on globalThis so both copies share them.
424
474
  */
425
- const V2_SENT_SOURCES_KEY = "__animus_v2_sent_sources__";
426
- const V2_DRIFT_WARNED_KEY = "__animus_v2_drift_warned__";
427
475
  const v2EngineApi = require_pipeline.createV2EngineApi({
428
476
  label: "animus",
429
477
  isV2: () => getSharedEngine() === "v2",
430
478
  loadNativeEngine: requireEngine,
431
479
  passThroughUnknownPaths: true,
432
480
  store: {
433
- getEngine: () => globalThis[V2_ENGINE_KEY],
481
+ getEngine: () => singletonStore[V2_ENGINE_KEY],
434
482
  setEngine: (engine) => {
435
- globalThis[V2_ENGINE_KEY] = engine;
483
+ singletonStore[V2_ENGINE_KEY] = engine;
436
484
  },
437
- getSentSources: () => globalThis[V2_SENT_SOURCES_KEY] ?? null,
485
+ getSentSources: () => singletonStore[V2_SENT_SOURCES_KEY] ?? null,
438
486
  setSentSources: (sources) => {
439
- globalThis[V2_SENT_SOURCES_KEY] = sources;
487
+ singletonStore[V2_SENT_SOURCES_KEY] = sources;
440
488
  },
441
- getDriftWarned: () => Boolean(globalThis[V2_DRIFT_WARNED_KEY]),
489
+ getDriftWarned: () => Boolean(singletonStore[V2_DRIFT_WARNED_KEY]),
442
490
  setDriftWarned: (value) => {
443
- globalThis[V2_DRIFT_WARNED_KEY] = value;
491
+ singletonStore[V2_DRIFT_WARNED_KEY] = value;
444
492
  }
445
493
  }
446
494
  });
447
- const ENGINE_API_OVERRIDE_KEY = "__animus_engine_api_override__";
448
495
  function engineApi() {
449
- const override = globalThis[ENGINE_API_OVERRIDE_KEY];
496
+ const override = singletonStore[ENGINE_API_OVERRIDE_KEY];
450
497
  return override ? override() : v2EngineApi();
451
498
  }
452
499
  //#endregion
453
500
  //#region session/timing.ts
454
- /**
455
- * Verbose build-timing waterfall: JS phases, analysis sub-phases, and the
456
- * shared Rust phase table. `ANIMUS_TIMING_JSON=1` additionally dumps a
457
- * machine-readable merged record.
458
- */
459
501
  function logBuildTimings(bt, rustTiming, log, verbose) {
460
502
  if (!verbose) return;
461
503
  const pad = (label, width) => label + " ".repeat(Math.max(0, width - label.length));
@@ -486,28 +528,14 @@ function logBuildTimings(bt, rustTiming, log, verbose) {
486
528
  if (process.env.ANIMUS_TIMING_JSON === "1") {
487
529
  const merged = {};
488
530
  for (const [k, v] of Object.entries(bt)) merged[`buildStart.${k}`] = v;
489
- if (rustTiming) {
490
- for (const [k, v] of Object.entries(rustTiming)) if (typeof v === "number") merged[`rust.${k}`] = v;
491
- }
531
+ if (rustTiming) for (const [k, v] of Object.entries(rustTiming)) merged[`rust.${k}`] = v;
492
532
  console.error(`[animus:timing] ${JSON.stringify(merged)}`);
493
533
  }
494
534
  }
495
535
  //#endregion
496
536
  //#region session/extraction-session.ts
497
- /**
498
- * Module id the Rust emitter injects for the extracted stylesheet — also the
499
- * exact resolve.alias key with-animus registers for it, which the adapter's
500
- * alias harvesting must skip.
501
- */
502
- const ANIMUS_CSS_MODULE_ID = ".animus/styles.css";
503
- /** Retention window for sibling session directories (design D2). */
504
- const SESSION_DIR_MAX_AGE_MS = 1440 * 60 * 1e3;
505
- /** Watchdog added to the debounce ceiling for status deadlines (D3). */
537
+ const SESSION_DIR_MAX_AGE_MS = 864e5;
506
538
  const STATUS_WATCHDOG_MS = 2e3;
507
- /** Flat legacy `.animus/` artifacts removed at session start — unreachable
508
- * by session-scoped loaders. The standalone CLI still publishes the three
509
- * payload names flat; its verified/live sets are fenced off inside
510
- * `runSessionStartHygiene`. */
511
539
  const LEGACY_FLAT_ARTIFACTS = [
512
540
  MANIFEST_ARTIFACT,
513
541
  ANALYSIS_INPUTS_ARTIFACT,
@@ -517,22 +545,16 @@ const LEGACY_FLAT_ARTIFACTS = [
517
545
  ANALYSIS_COMMIT_ARTIFACT,
518
546
  ANALYSIS_STATUS_ARTIFACT
519
547
  ];
520
- /**
521
- * Bundler-agnostic extraction pipeline: system loading, source discovery
522
- * and ingestion, external-package collection, analysis, stylesheet and
523
- * system-props emission, and watch-cycle diffing.
524
- *
525
- * The webpack adapter (plugin.ts) owns only bundler wiring: hook
526
- * registration, alias harvesting from the compiler config, in-memory asset
527
- * replacement, and translating watch events into `handleWatchUpdate`
528
- * change sets. Keeping this class free of webpack types is deliberate — a
529
- * future Turbopack integration must drive the same session from outside
530
- * the bundler (Turbopack has no compiler-hook surface), reusing everything
531
- * here unchanged.
532
- *
533
- * Outputs are published through the package singleton (shared CSS,
534
- * manifest, system props) and written to `.animus/` on disk.
535
- */
548
+ /** Brands values read back off DISK. `Object(value) !== value` rejects
549
+ * boxed primitives; the tag is unspoofable by `Symbol.toStringTag`. */
550
+ function isDiskString(value) {
551
+ return Object(value) !== value && Object.prototype.toString.call(value) === "[object String]";
552
+ }
553
+ function isDiskNumber(value) {
554
+ return Object(value) !== value && Object.prototype.toString.call(value) === "[object Number]";
555
+ }
556
+ /** Bundler-agnostic extraction pipeline: system load, source ingestion,
557
+ * analysis, emission, and watch-cycle diffing. Carries no bundler types. */
536
558
  var ExtractionSession = class {
537
559
  /** Set by the adapter before any pipeline run. */
538
560
  rootDir = null;
@@ -542,19 +564,13 @@ var ExtractionSession = class {
542
564
  /** Physical asset files registered with the host watcher. */
543
565
  assetDependencyPaths = /* @__PURE__ */ new Set();
544
566
  assetDependencyKeys = /* @__PURE__ */ new Set();
545
- /** Emitter identity override for the system-props module id. Webpack mode
546
- * (null) injects the absolute `.animus/system-props.js` path, resolved by
547
- * NormalModuleReplacement; Turbopack rejects absolute-path imports, so
548
- * its driver sets the virtual id that `resolveAlias` maps to disk. */
567
+ /** System-props module id override. Turbopack rejects absolute-path
568
+ * imports, so its driver sets a virtual id; null injects the abs path. */
549
569
  systemPropsModuleId = null;
550
- /** Whether the analysis-inputs hydration corpus is serialized + written.
551
- * False (webpack mode): the loader shares the pipeline process and reads
552
- * the manifest from memory — the corpus is never persisted. True is set
553
- * by the Turbopack orchestration driver, whose isolated loader workers
554
- * replay it (spec: next-turbopack-integration, "Manifest disk artifact" /
555
- * "Webpack mode skips the hydration corpus"). */
570
+ /** Whether the analysis-inputs hydration corpus is written to disk. Only
571
+ * isolated loader workers replay it; an in-process loader reads memory. */
556
572
  persistAnalysisInputs = false;
557
- /** Absolute directory prefixes for external DS packages (loader allowlisting). */
573
+ /** Absolute directory prefixes for external packages (loader allowlist). */
558
574
  externalPackageDirs = [];
559
575
  /** Absolute package dir → owning specifier (cross-source correlation). */
560
576
  externalDirOwners = {};
@@ -562,14 +578,11 @@ var ExtractionSession = class {
562
578
  externalFileOwners = {};
563
579
  /** External package specifier → absolute source entry path. */
564
580
  externalSourceEntries = /* @__PURE__ */ new Map();
565
- /** SourceId derivation authority for the current generation (openspec:
566
- * external-source-watch-ingestion, design D2) — canonical roots
567
- * realpath'd once per full pipeline; alias→SourceId associations
568
- * recorded while files exist so deletion resolves through the cache. */
581
+ /** SourceId authority for this generation — roots realpath'd once per
582
+ * pipeline; aliases recorded while files exist so deletions resolve. */
569
583
  sourceIdentity = null;
570
- /** Canonical admitted external root → declared specifiers (set-valued
571
- * ownership, design D2; duplicate specifiers resolving to one canonical
572
- * root share it). Engine watch wiring and tests read this. */
584
+ /** Canonical admitted external root → declared specifiers; duplicate
585
+ * specifiers resolving to one canonical root share an entry. */
573
586
  externalRootOwners = /* @__PURE__ */ new Map();
574
587
  /** Canonical external root → the extension set its collection walk used
575
588
  * (widened for dist-only roots); rewalks and classification share it. */
@@ -577,90 +590,81 @@ var ExtractionSession = class {
577
590
  /** Canonical admitted external watch roots — the engine-side watch
578
591
  * surface (webpack contextDependencies / Turbopack watchers). */
579
592
  externalWatchRoots = [];
580
- /** Canonical external root → (raw sourceKey → {raw content hash, recorded
581
- * abs spelling}): the previous-generation inventory a dirty-root rewalk
582
- * diffs against (design D3). Raw hashes — MDX preprocessing never skews
583
- * the diff; the recorded abs path is a registered deletion alias, so a
584
- * reconstructed deletion always resolves through cached identity. */
593
+ /** Canonical external root → sourceKey → {raw content hash, recorded abs}:
594
+ * the inventory a dirty-root rewalk diffs against, hashed before MDX. */
585
595
  externalInventory = /* @__PURE__ */ new Map();
586
- /** Sticky diagnostics (design D5/D7): stable key → message, re-surfaced
587
- * on every full pipeline until the underlying condition clears. */
596
+ /** Stable key → message, re-surfaced on every full pipeline until the
597
+ * underlying condition clears. */
588
598
  stickyDiagnostics = /* @__PURE__ */ new Map();
589
- /** Volume-membership predicate for the cross-volume gate (design D5).
590
- * Injected seam: the shared predicate only discriminates on Windows
591
- * (win32 semantics are unit-tested via path.win32), so runtime tests
592
- * drive the gate through this. */
599
+ /** Volume-membership predicate for the cross-volume gate. Injected seam:
600
+ * the real predicate only discriminates on Windows, so tests replace it. */
593
601
  sharesProjectVolume = require_pipeline.sharesVolumeRoot;
594
- /** Orchestrator seams (openspec: external-source-watch-ingestion, design
595
- * D4). `onExternalRootResolved` fires per volume-admitted external root
596
- * DURING collection — after resolution, BEFORE that root's sources are
597
- * walked — so a watching host can open its handle with no blind gap
598
- * between scan and watch. `onExternalRootsCommitted` fires once after a
599
- * successful full-pipeline publication with the complete admitted
600
- * canonical root set (the host's promote/replay/close-old phase; a
601
- * failed pipeline never fires it, so the host rolls back). */
602
+ /** `onExternalRootResolved` fires per admitted root DURING collection,
603
+ * before its walk; `onExternalRootsCommitted` only after publication. */
602
604
  onExternalRootResolved = null;
603
605
  onExternalRootsCommitted = null;
606
+ /** Fires exactly once per settled `handleWatchUpdate` — the failure cause,
607
+ * or null when clean. Observation never alters delivery. */
608
+ onCycleSettled = null;
604
609
  options;
605
610
  staticCssJson;
606
611
  system = null;
607
- /** Discovery-time keyframes diagnostics awaiting the shared surfacing pass. */
608
- externalKeyframesDiagnostics = [];
612
+ /** Vocabulary witness diagnostics from the sealed system's registration
613
+ * record, awaiting the shared surfacing pass. */
614
+ systemVocabularyDiagnostics = [];
615
+ /** Configured external files that could not be read. Incremental passes
616
+ * re-collect nothing, so they replay these instead of passing strict. */
617
+ ingestionFailureDiagnostics = [];
609
618
  /** Full package-resolution map from the last full pipeline — replayed by
610
619
  * incremental passes (sourceEntries alone omits dist-resolved packages). */
611
620
  lastPackageMap = {};
612
621
  fileCache = /* @__PURE__ */ new Map();
613
- analysisEntryCache = /* @__PURE__ */ new Map();
614
- sourceOwnership = {};
622
+ corpus = require_pipeline.createSourceCorpus({
623
+ engineApi: () => engineApi(),
624
+ prefix: "[animus-next]",
625
+ strict: () => !!this.options.strict,
626
+ warn: (message) => this.warn(message)
627
+ });
615
628
  systemDependencyKeys = /* @__PURE__ */ new Set();
616
629
  /** Loader-reported dependency paths for bundler watch registration. */
617
630
  systemDependencyPaths = [];
618
631
  systemDependencyHashes = /* @__PURE__ */ new Map();
619
632
  lastSystemPropsHash = null;
620
- /** Per-payload write guards: payloadHash gates rewrites (byte-identical
621
- * payloads leave disk untouched); diskHash is the hash of the CURRENT
622
- * disk bytes (envelope included) recorded in the analysis-commit. null =
623
- * not yet seeded (first publication reads the artifact's envelope so a
624
- * same-session restart never rewrites byte-identical artifacts). */
625
633
  artifactRecords = {
626
634
  manifest: null,
627
635
  inputs: null,
628
636
  styles: null
629
637
  };
630
- /** Last written analysis-commit; null = seeded from disk on first use. */
638
+ /** Last written analysis-commit; null seeds from disk on first use. Its
639
+ * generation counter is correct only because one session owns the dir. */
631
640
  lastCommit = null;
632
- /** Session identity — claimed once per PROCESS (one Next invocation),
633
- * adopted by every subsequent session instance so all compilers share
634
- * one artifact tree (design D2: `next dev`/`next build` co-writing are
635
- * separate processes and therefore separate sessions). */
641
+ /** Release handle for the in-process owner slot AND the directory's
642
+ * on-disk owner record, taken and released as one; null when unheld. */
643
+ releasePublicationClaim = null;
644
+ /** Session identity, claimed once per PROCESS and adopted by every later
645
+ * session instance so all compilers share one artifact tree. */
636
646
  sessionId = claimProcessSessionId();
637
- /** Epoch value of the last published analysis; null = not yet known
638
- * (seeded from the session dir's artifact on first publication so a
639
- * same-session restart with unchanged plans never rewrites bytes). */
647
+ /** Epoch of the last published analysis; null seeds from disk so a
648
+ * same-session restart with unchanged plans rewrites no bytes. */
640
649
  lastEpochValue = null;
641
- /** Watcher debounce ceiling feeding status deadlines (design D3). */
650
+ /** Watcher debounce ceiling feeding status deadlines. */
642
651
  debounceCeilingMs = 75;
643
652
  /** Test seam: observes every session-artifact write (name, content)
644
653
  * post-rename — write ORDER is part of the transaction contract. */
645
654
  onArtifactWrite = null;
646
- /** Status attempt bookkeeping (design D3). */
647
655
  statusAttemptId = 0;
648
656
  statusAttemptOpen = false;
649
- /** Monotonic first-emission witness (openspec: standalone-extraction-cli,
650
- * watch readiness): flips true once the first complete publication
651
- * succeeds and never regresses — every later status write (idle AND
652
- * failed) carries it, so readiness stays observable across attempts. */
657
+ /** Monotonic readiness witness: flips true on the first complete
658
+ * publication and never regresses; every later status write carries it. */
653
659
  firstEmissionComplete = false;
654
660
  /** Debounce-window observations pending analysis (sourceKey → hash). */
655
661
  debouncePending = /* @__PURE__ */ new Map();
656
- /** Session-start hygiene (pruning + legacy cleanup) runs once. */
657
662
  sessionStartHygieneDone = false;
658
663
  lcssTargets = null;
659
664
  constructor(options) {
660
665
  this.options = options;
661
666
  this.staticCssJson = require_pipeline.serializeStaticCss(options.staticCss);
662
667
  }
663
- /** This session's artifact directory. Requires `rootDir` to be set. */
664
668
  get sessionDir() {
665
669
  return sessionArtifactDir(this.rootDir, this.sessionId);
666
670
  }
@@ -679,24 +683,20 @@ var ExtractionSession = class {
679
683
  elapsed(t) {
680
684
  return this.verbose ? Math.round(performance.now() - t) : 0;
681
685
  }
682
- /** Lazily-computed scan config (options are constructor-fixed, so the
683
- * derivation is stable for the session's lifetime). */
684
- /** Diagnostic prefix label — drivers set their own so a host's errors
685
- * never masquerade as another driver's (inc 05 review S1). The default
686
- * keeps the Next plugin's historical prefix. */
686
+ /** Diagnostic prefix label — drivers set their own so one host's errors
687
+ * never masquerade as another driver's. */
687
688
  driverLabel = "animus-next";
688
689
  /** Per-specifier discovery outcomes from the last full collection —
689
690
  * driver-consumed reporting surface (the CLI's summary). */
690
691
  lastExternalOutcomes = [];
691
- /** Component count of the last published analysis, or null before the
692
- * first — the CLI's publish path reads this instead of re-parsing the
693
- * manifest JSON every cycle. */
692
+ /** Component count of the last published analysis, null before the first —
693
+ * read by drivers instead of re-parsing the manifest JSON every cycle. */
694
694
  lastComponentCount = null;
695
- /** Driver-owned STRUCTURAL exclusions (a CLI outDir inside the root),
696
- * joined to the never-replaceable set — never the user `exclude` list,
697
- * whose presence would flip the replace semantics and silently drop the
698
- * replaceable defaults. Set before the first pipeline run (the scan
699
- * config is memoized). */
695
+ /** When superseded asset copies leave `assets/`: `'full-pipeline'` suits a
696
+ * driver serving the dir in place, `'every-cycle'` one republishing it. */
697
+ staleAssetPruning = "full-pipeline";
698
+ /** Driver-owned STRUCTURAL exclusions, joined to the never-replaceable set
699
+ * — never the user list, whose presence flips the replace semantics. */
700
700
  structuralExclude = [];
701
701
  scanConfigMemo = null;
702
702
  /** Per-pattern exclusion hit counts from this session's matcher —
@@ -716,17 +716,18 @@ var ExtractionSession = class {
716
716
  }
717
717
  return this.scanConfigMemo;
718
718
  }
719
- /**
720
- * Watch entry point: one single-flight analysis transaction per event
721
- * batch (openspec: next-webpack-served-transform-coherence, design D3).
722
- * The first entering session runs the transaction; every concurrent
723
- * entry — same session re-entered, or another compiler's session
724
- * (client/server/RSC each hold their own instance) — joins the in-flight
725
- * promise, so no compiler proceeds against a generation older than the
726
- * one the transaction publishes. A rejected transaction rejects every
727
- * joiner; the gate always clears.
728
- */
719
+ /** One single-flight analysis transaction per event batch: concurrent
720
+ * entries join the in-flight promise; each settled entry reports outcome. */
729
721
  async handleWatchUpdate(changes) {
722
+ try {
723
+ await this.routeWatchUpdate(changes);
724
+ } catch (error) {
725
+ this.onCycleSettled?.(error);
726
+ throw error;
727
+ }
728
+ this.onCycleSettled?.(null);
729
+ }
730
+ async routeWatchUpdate(changes) {
730
731
  const inflight = getWatchTransaction();
731
732
  if (inflight) {
732
733
  await inflight;
@@ -746,13 +747,8 @@ var ExtractionSession = class {
746
747
  setWatchTransaction(null);
747
748
  }
748
749
  }
749
- /**
750
- * Owner-side entry for a batch observed by a NON-OWNING compiler's
751
- * watcher. Serializes behind any in-flight transaction, then runs the
752
- * forwarded batch as its own transaction — never drops it (a forwarded
753
- * batch has no other delivery; content-hash diffing makes an
754
- * already-covered batch a cheap no-op).
755
- */
750
+ /** Owner-side entry for a batch a non-owning compiler observed. Serializes
751
+ * behind any in-flight transaction — it has no other delivery. */
756
752
  async ingestForwardedBatch(changes) {
757
753
  if (!this.system) return;
758
754
  for (;;) {
@@ -768,11 +764,6 @@ var ExtractionSession = class {
768
764
  setWatchTransaction(null);
769
765
  }
770
766
  }
771
- /**
772
- * Incremental watch pass: geological reset when the system file changed,
773
- * otherwise content-hash diffing restricted to the watcher's change sets
774
- * (falling back to a full discovery walk when no sets are provided).
775
- */
776
767
  async processWatchUpdate(changes) {
777
768
  const rootDir = this.rootDir;
778
769
  try {
@@ -796,14 +787,14 @@ var ExtractionSession = class {
796
787
  }
797
788
  else systemHit = changed.find((path$2) => require_pipeline.toWatchKeys(path$2).some((key) => this.systemDependencyKeys.has(key)));
798
789
  } catch (err) {
799
- this.warn(`HMR geological-reset check failed: ${String(err)}`);
790
+ this.warn(`HMR system-reload check failed: ${String(err)}`);
800
791
  }
801
792
  if (systemHit) {
802
- this.log(`geological reset: system dependency changed (${(0, path.relative)(rootDir, systemHit)})`);
793
+ this.log(`system reload: system dependency changed (${(0, path.relative)(rootDir, systemHit)})`);
803
794
  this.resetForHmr();
804
795
  try {
805
796
  const promise = this.runFullPipeline(this.pendingFromBatch(changes));
806
- setAnalysisPromise(promise);
797
+ setAnalysisStartedPromise(promise);
807
798
  await promise;
808
799
  } catch (err) {
809
800
  try {
@@ -907,11 +898,14 @@ var ExtractionSession = class {
907
898
  let ingested;
908
899
  try {
909
900
  this.beginStatusAttempt();
910
- ingested = await this.ingestAccepted();
911
- this.externalFileOwners = this.projectExternalFileOwners(ingested, this.externalFileOwners);
912
- resetAnalysisPromise();
901
+ ingested = await this.corpus.prepare({
902
+ fileCache: this.fileCache,
903
+ externalFileOwners: this.externalFileOwners
904
+ });
905
+ this.externalFileOwners = require_pipeline.projectExternalFileOwners(ingested, this.externalFileOwners);
906
+ resetAnalysisStartedPromise();
913
907
  const promise = this.runIncrementalPipeline(ingested.analysisEntries, pending);
914
- setAnalysisPromise(promise);
908
+ setAnalysisStartedPromise(promise);
915
909
  await promise;
916
910
  } catch (err) {
917
911
  for (const [key, prior] of priorCacheEntries) if (prior === null) this.fileCache.delete(key);
@@ -942,60 +936,39 @@ var ExtractionSession = class {
942
936
  this.writeAnalysisStatus("idle", []);
943
937
  }
944
938
  }
945
- /** The shared ingestion policy point (vite-plugin parity BY CODE): the
946
- * capability guard, facts memo, and warn-dedupe lifecycle live in the
947
- * pipeline; this host holds only its prefix, strict flag, and warn sink. */
948
- sourceIngestor = require_pipeline.createSourceIngestor({
949
- engineApi: () => engineApi(),
950
- prefix: "[animus-next]",
951
- strict: () => !!this.options.strict,
952
- warn: (message) => this.warn(message)
953
- });
954
- /** Prepare one raw-source corpus through the shared adaptation boundary. */
955
- async ingestRawSources(entries) {
956
- return this.sourceIngestor.ingest(entries);
957
- }
958
- /** Ingest and apply the shared per-file quarantine: one invalid original
959
- * (an `.mdx` with the optional peer absent, an unsupported `.svelte`
960
- * shape) warns and drops; the rest of the corpus still analyzes. Strict
961
- * mode throws from the shared policy — callers route that into their own
962
- * status/rollback handling. */
963
- async ingestAccepted(entries) {
964
- const ingested = await this.ingestRawSources(entries ?? this.buildRawEntriesFromCache());
965
- return require_pipeline.withoutInvalidOriginals(ingested, this.surfaceSourceDiagnostics(ingested.diagnostics));
966
- }
967
- /** Surface parser diagnostics under the shared strict/warn policy. */
968
- surfaceSourceDiagnostics(diagnostics) {
969
- return this.sourceIngestor.surfaceDiagnostics(diagnostics);
970
- }
971
- /**
972
- * Correlate diagnostics emitted against generated children back to the
973
- * external package owning their raw original.
974
- */
975
- projectExternalFileOwners(result, rawOwners) {
976
- const projected = {};
977
- for (const owner of Object.values(result.ownership)) {
978
- const packageOwner = rawOwners[owner.originalPath];
979
- if (!packageOwner) continue;
980
- projected[owner.originalPath] = packageOwner;
981
- for (const analysisPath of owner.analysisPaths) projected[analysisPath] = packageOwner;
982
- }
983
- return projected;
984
- }
985
- /** Publish the raw cache and complete parser projection atomically. */
986
939
  publishSourceIngestion(result) {
987
- this.sourceIngestor.markPublished(result);
988
940
  this.fileCache = new Map(result.originalEntries.map((entry) => [entry.path, {
989
941
  hash: entry.hash,
990
942
  source: entry.source
991
943
  }]));
992
- this.analysisEntryCache = new Map(result.analysisEntries.map((entry) => [entry.path, {
993
- hash: entry.hash,
994
- source: entry.source
995
- }]));
996
- this.sourceOwnership = result.ownership;
944
+ this.corpus.publish(result);
997
945
  }
946
+ /** Full analysis + publication, registered in the one in-flight
947
+ * transaction slot; a nested reload leaves the enclosing registration. */
998
948
  async runFullPipeline(pending = []) {
949
+ if (this.releasePublicationClaim === null) {
950
+ const releaseOwner = claimExclusiveSessionOwner(`${this.driverLabel}:${this.sessionDir}`);
951
+ (0, fs.mkdirSync)(this.sessionDir, { recursive: true });
952
+ const releaseClaimRecord = holdDirectoryClaim(this.sessionDir);
953
+ this.releasePublicationClaim = () => {
954
+ releaseOwner();
955
+ releaseClaimRecord();
956
+ };
957
+ }
958
+ const priorSystem = this.system;
959
+ const nested = getWatchTransaction() !== null;
960
+ const transaction = this.runPipelineTransaction(pending);
961
+ if (!nested) setWatchTransaction(transaction);
962
+ try {
963
+ await transaction;
964
+ } catch (error) {
965
+ this.system = priorSystem;
966
+ throw error;
967
+ } finally {
968
+ if (!nested) setWatchTransaction(null);
969
+ }
970
+ }
971
+ async runPipelineTransaction(pending) {
999
972
  const pipelineStart = this.now();
1000
973
  const bt = {};
1001
974
  this.runSessionStartHygiene();
@@ -1009,6 +982,7 @@ var ExtractionSession = class {
1009
982
  rootDir,
1010
983
  prefix: this.options.prefix
1011
984
  });
985
+ this.systemVocabularyDiagnostics = require_pipeline.vocabularyWitnessDiagnostics(this.system.vocabularyWitnessesJson);
1012
986
  this.assetCopyCache.clear();
1013
987
  this.assetDependencyPaths.clear();
1014
988
  this.assetDependencyKeys.clear();
@@ -1049,6 +1023,7 @@ var ExtractionSession = class {
1049
1023
  const packageNames = require_pipeline.extractSystemFilePackages(resolvedSystemPath);
1050
1024
  const preResolved = resolvePackagesByName(rootDir, packageNames);
1051
1025
  const rawExternalFiles = /* @__PURE__ */ new Map();
1026
+ const ingestionFailures = [];
1052
1027
  const collected = await require_pipeline.collectExternalPackageSources({
1053
1028
  specifiers: packageNames,
1054
1029
  resolveSpecifier: (name) => preResolved[name] ? (0, path.resolve)(rootDir, preResolved[name]) : null,
@@ -1058,7 +1033,7 @@ var ExtractionSession = class {
1058
1033
  onSourceRead: (source, _relPath, absPath) => {
1059
1034
  rawExternalFiles.set(absPath, require_pipeline.contentHash(source));
1060
1035
  },
1061
- onUnreadable: (relPath, err) => this.warn(`skipped unreadable package file ${relPath}: ${String(err)}`),
1036
+ onUnreadable: (relPath, err) => ingestionFailures.push(require_pipeline.unreadableSourceDiagnostic(relPath, err)),
1062
1037
  onPackageResolved: (_specifier, packageDir) => {
1063
1038
  if (!this.onExternalRootResolved) return;
1064
1039
  if (!this.sharesProjectVolume(rootDir, packageDir)) return;
@@ -1070,6 +1045,7 @@ var ExtractionSession = class {
1070
1045
  }
1071
1046
  });
1072
1047
  this.lastExternalOutcomes = collected.outcomes;
1048
+ this.ingestionFailureDiagnostics = ingestionFailures;
1073
1049
  for (const record of collected.outcomes) if (record.outcome === "empty") this.warn(`include '${record.specifier}' resolved but discovered no component sources`);
1074
1050
  const unresolvableMessage = require_pipeline.unresolvableIncludesMessage(collected.outcomes);
1075
1051
  if (unresolvableMessage !== null) {
@@ -1138,23 +1114,17 @@ var ExtractionSession = class {
1138
1114
  });
1139
1115
  }
1140
1116
  this.externalPackageDirs = admitted.packageDirs;
1141
- if (this.system && admitted.keyframesScanEntries.size > 0) {
1142
- const api = engineApi();
1143
- const merge = require_pipeline.mergeExternalKeyframes((entry, root) => api.scanKeyframesExports(entry, root), this.system.keyframesJson, admitted.keyframesScanEntries.values(), this.rootDir);
1144
- this.system.keyframesJson = merge.keyframesJson;
1145
- this.externalKeyframesDiagnostics = merge.diagnostics;
1146
- } else this.externalKeyframesDiagnostics = [];
1147
1117
  bt.packageResolve = this.elapsed(t);
1148
1118
  this.beginStatusAttempt();
1149
1119
  let accepted;
1150
1120
  try {
1151
- accepted = await this.ingestAccepted(rawEntries);
1121
+ accepted = await this.corpus.prepare(rawEntries);
1152
1122
  } catch (err) {
1153
1123
  this.debouncePending.clear();
1154
1124
  this.writeAnalysisStatus("failed", pending, String(err));
1155
1125
  throw err;
1156
1126
  }
1157
- this.externalFileOwners = this.projectExternalFileOwners(accepted, this.externalFileOwners);
1127
+ this.externalFileOwners = require_pipeline.projectExternalFileOwners(accepted, this.externalFileOwners);
1158
1128
  bt.fileCount = accepted.analysisEntries.length;
1159
1129
  require_pipeline.clearEngineCache(engineApi);
1160
1130
  await this.analyzeAndEmit(accepted.analysisEntries, packageMap, false, bt, pipelineStart, pending);
@@ -1168,10 +1138,8 @@ var ExtractionSession = class {
1168
1138
  throw err;
1169
1139
  }
1170
1140
  }
1171
- /** The per-generation source state runFullPipeline mutates, captured as
1172
- * ONE object so the restore below cannot omit a member. The three
1173
- * deep-copied fields are the ones later steps mutate in place rather
1174
- * than reassign; everything else is rebound wholesale on success. */
1141
+ /** Per-generation source state captured as ONE object so the restore
1142
+ * cannot omit a member; deep-copied fields are mutated in place. */
1175
1143
  snapshotSourceState() {
1176
1144
  return {
1177
1145
  sourceIdentity: this.sourceIdentity,
@@ -1184,20 +1152,23 @@ var ExtractionSession = class {
1184
1152
  externalFileOwners: this.externalFileOwners,
1185
1153
  externalSourceEntries: this.externalSourceEntries,
1186
1154
  externalPackageDirs: this.externalPackageDirs,
1187
- externalKeyframesDiagnostics: this.externalKeyframesDiagnostics
1155
+ systemVocabularyDiagnostics: this.systemVocabularyDiagnostics,
1156
+ ingestionFailureDiagnostics: this.ingestionFailureDiagnostics
1188
1157
  };
1189
1158
  }
1190
1159
  restoreSourceState(snapshot) {
1191
1160
  Object.assign(this, snapshot);
1192
1161
  }
1193
- /**
1194
- * Reset analysis state for HMR geological reset. Payload write guards go
1195
- * back to null so the next publication reseeds them from the disk
1196
- * envelopes — a byte-identical post-reset artifact is still not
1197
- * rewritten.
1198
- */
1162
+ /** Release this session's publication claim so a successor may publish.
1163
+ * Idempotent, and scoped to this session's own claim. */
1164
+ close() {
1165
+ this.releasePublicationClaim?.();
1166
+ this.releasePublicationClaim = null;
1167
+ }
1168
+ /** Reset analysis state for a system reload: write guards go back to null
1169
+ * and reseed from the disk envelopes on the next publication. */
1199
1170
  resetForHmr() {
1200
- resetAnalysisPromise();
1171
+ resetAnalysisStartedPromise();
1201
1172
  this.artifactRecords = {
1202
1173
  manifest: null,
1203
1174
  inputs: null,
@@ -1205,12 +1176,8 @@ var ExtractionSession = class {
1205
1176
  };
1206
1177
  this.lastSystemPropsHash = null;
1207
1178
  }
1208
- /**
1209
- * The (sourceKey, observedSourceHash) pairs of a watch batch — cheap
1210
- * evidence for the status file's pending set on the geological-reset
1211
- * path, where the batch's component edits ride along with the system
1212
- * edit (design D3: loaders wait only on observed inputs).
1213
- */
1179
+ /** The (sourceKey, observed hash) pairs of a watch batch — the status
1180
+ * file's pending set; loaders wait only on observed inputs. */
1214
1181
  pendingFromBatch(changes) {
1215
1182
  const { excludeMatcher, extensionsSet } = this.resolveScanConfig();
1216
1183
  const pending = [];
@@ -1226,17 +1193,8 @@ var ExtractionSession = class {
1226
1193
  }
1227
1194
  return pending;
1228
1195
  }
1229
- /**
1230
- * Root-dirty inventory reconciliation (design D3): partition the
1231
- * watcher's change sets into explicit FILE events and external ROOT hits
1232
- * (a reported path that IS an admitted root, or a directory inside one),
1233
- * rewalk each dirty root through the shared discovery policy, diff the
1234
- * SourceId+raw-hash inventory against the previous generation, and merge
1235
- * the reconstructed created/edited/deleted deltas with the explicit
1236
- * events (identity-level dedup happens downstream at the cache gate).
1237
- * Deletion falls out of the inventory diff — webpack may report only the
1238
- * directory (probe-proven).
1239
- */
1196
+ /** Rewalk dirty external roots and diff the inventory into created,
1197
+ * edited and deleted deltas: a watcher may report only the directory. */
1240
1198
  reconcileExternalRoots(changes) {
1241
1199
  const identity = this.sourceIdentity;
1242
1200
  if (!identity) return changes;
@@ -1289,14 +1247,8 @@ var ExtractionSession = class {
1289
1247
  removedFiles: removed
1290
1248
  };
1291
1249
  }
1292
- /**
1293
- * Route one watcher-reported path (design D1/D2): a project-root member
1294
- * (lexical containment — existing local semantics preserved; consumer
1295
- * exclude patterns apply) or an admitted external member (identity
1296
- * resolution with symlink-escape rejection; package-relative excludes
1297
- * mirror the collection walk's filters — guardrail G1, one policy).
1298
- * Returns null for dropped paths.
1299
- */
1250
+ /** Route one watcher path: a project-root member (consumer excludes apply)
1251
+ * or an admitted external member (symlink escapes rejected). */
1300
1252
  classifyWatchPath(absPath, scan) {
1301
1253
  const ext = (0, path.extname)(absPath);
1302
1254
  const rootDir = this.rootDir;
@@ -1326,51 +1278,13 @@ var ExtractionSession = class {
1326
1278
  owningRoot: resolved.owningRoot
1327
1279
  };
1328
1280
  }
1329
- /** Build full raw originals for one shared adaptation attempt. */
1330
- buildRawEntriesFromCache() {
1331
- const entries = [];
1332
- for (const [path$8, { hash, source }] of this.fileCache) entries.push({
1333
- path: path$8,
1334
- source,
1335
- hash
1336
- });
1337
- return entries;
1338
- }
1339
- /**
1340
- * Run incremental pipeline with cache-aware file entries.
1341
- * Reuses system config from the last full pipeline run.
1342
- */
1343
1281
  async runIncrementalPipeline(fileEntries, pending = []) {
1344
1282
  const bt = {};
1345
1283
  const pipelineStart = this.now();
1346
1284
  await this.analyzeAndEmit(fileEntries, this.lastPackageMap, true, bt, pipelineStart, pending);
1347
1285
  }
1348
- /**
1349
- * Shared analysis + emit core for both pipelines — the single call site
1350
- * that routes every manifest through the shared `runProjectAnalysis`,
1351
- * reachable from both runFullPipeline (production) and
1352
- * runIncrementalPipeline (HMR).
1353
- *
1354
- * Owns diagnostic surfacing, CSS assembly + styles.css write guard,
1355
- * system-props module emit, and the timing log. The engine's own
1356
- * `dev_mode` (reconciliation pruning) is derived separately via
1357
- * `engineDevMode` — an explicit `mode` option overrides the pipeline
1358
- * path there. For everything else the `devMode` flag is the ONLY
1359
- * behavioral fork:
1360
- *
1361
- * - `false` (production): computes bt.analysis + logs the extraction
1362
- * report, and writes system-props.js UNCONDITIONALLY (no
1363
- * lastSystemPropsHash guard).
1364
- * - `true` (HMR): skips the report log, and guards the system-props.js
1365
- * write by lastSystemPropsHash.
1366
- *
1367
- * The disk artifacts land under the session directory in transaction
1368
- * order (design D1): manifest → analysis-inputs (Turbopack orchestration
1369
- * only) → styles.css → system-props → analysis-commit →
1370
- * replacements-epoch (last, only when moved). The analysis-status file walks starting → analyzing →
1371
- * committing → idle around the attempt, landing in `failed` (with the
1372
- * diagnostic) on any throw (design D3).
1373
- */
1286
+ /** Shared analysis + emit core for both pipelines. `devMode` forks only
1287
+ * the report log and the system-props write guard. */
1374
1288
  async analyzeAndEmit(fileEntries, packageMap, devMode, bt, pipelineStart, pending = []) {
1375
1289
  this.beginStatusAttempt();
1376
1290
  this.debouncePending.clear();
@@ -1384,16 +1298,8 @@ var ExtractionSession = class {
1384
1298
  throw err;
1385
1299
  }
1386
1300
  }
1387
- /**
1388
- * The engine's `dev_mode` flag (retain all components vs reconciliation
1389
- * pruning) is an EMISSION decision, so an explicit `mode` option wins over
1390
- * the pipeline path (core-options: `mode` "decides emitted bytes …
1391
- * engine devMode"). Without an explicit mode the historical per-pipeline
1392
- * default applies: full = production pruning, incremental = dev retention.
1393
- * A pinned-production watch must not flip to unpruned CSS on its first
1394
- * incremental republication, and a pinned-development full build must
1395
- * retain all components.
1396
- */
1301
+ /** An explicit `mode` wins over the pipeline path: a pinned-production
1302
+ * watch must not emit unpruned CSS on its first incremental republish. */
1397
1303
  engineDevMode(pipelineDefault) {
1398
1304
  if (this.options.mode !== void 0) return this.options.mode === "development";
1399
1305
  return pipelineDefault;
@@ -1419,7 +1325,7 @@ var ExtractionSession = class {
1419
1325
  ...analysisOptions,
1420
1326
  warn: (message) => this.warn(message),
1421
1327
  strict: this.options.strict,
1422
- extraDiagnostics: this.externalKeyframesDiagnostics
1328
+ extraDiagnostics: [...this.systemVocabularyDiagnostics, ...this.ingestionFailureDiagnostics]
1423
1329
  });
1424
1330
  require_pipeline.assertNoErrorDiagnostics(result.manifest?.diagnostics);
1425
1331
  require_pipeline.enforceExternalTokenContracts({
@@ -1440,7 +1346,7 @@ var ExtractionSession = class {
1440
1346
  bt.analysis = (bt.jsonSerialize ?? 0) + (bt.rustExtract ?? 0) + (bt.jsonParse ?? 0);
1441
1347
  if (manifest?.report) this.log(`Extracted ${manifest.report.components_extracted ?? "?"}/${manifest.report.components_total ?? "?"} components (${bt.analysis}ms)`);
1442
1348
  }
1443
- const globalCss = this.substituteAssetReferences(result.globalCss, devMode);
1349
+ const globalCss = this.substituteAssetReferences(result.globalCss, !devMode || this.staleAssetPruning === "every-cycle");
1444
1350
  const { declaration, variables, body } = require_pipeline.assembleStylesheet({
1445
1351
  layers: this.options.layers,
1446
1352
  variableCss: system.variableCss,
@@ -1497,8 +1403,8 @@ var ExtractionSession = class {
1497
1403
  this.statusAttemptOpen = true;
1498
1404
  }
1499
1405
  }
1500
- /** Write the session's analysis-status artifact (design D3). Terminal
1501
- * states (idle/failed) close the attempt. */
1406
+ /** Write the session's analysis-status artifact. Terminal states
1407
+ * (idle/failed) close the attempt. */
1502
1408
  writeAnalysisStatus(state, pending, diagnostic) {
1503
1409
  const status = {
1504
1410
  schema: 2,
@@ -1506,19 +1412,15 @@ var ExtractionSession = class {
1506
1412
  attemptId: this.statusAttemptId,
1507
1413
  state,
1508
1414
  pending,
1509
- deadlineAt: Date.now() + this.debounceCeilingMs + STATUS_WATCHDOG_MS,
1510
- ...diagnostic !== void 0 ? { diagnostic } : {},
1511
- ready: this.firstEmissionComplete
1415
+ deadlineAt: Date.now() + this.debounceCeilingMs + STATUS_WATCHDOG_MS
1512
1416
  };
1417
+ if (diagnostic !== void 0) status.diagnostic = diagnostic;
1418
+ status.ready = this.firstEmissionComplete;
1513
1419
  this.writeSessionArtifact(ANALYSIS_STATUS_ARTIFACT, JSON.stringify(status));
1514
1420
  if (state === "idle" || state === "failed") this.statusAttemptOpen = false;
1515
1421
  }
1516
- /**
1517
- * Orchestrator seam (Turbopack watcher): record watch events observed
1518
- * during the debounce window so a loader running ahead of the analysis
1519
- * has positive evidence to wait on (design D3 'debouncing'). Paths are
1520
- * filtered by the scan config and hashed at observation time.
1521
- */
1422
+ /** Record watch events observed during the debounce window so a loader
1423
+ * running ahead of the analysis has evidence to wait on. */
1522
1424
  noteDebouncedWatchEvents(absPaths) {
1523
1425
  if (!this.rootDir) return;
1524
1426
  const additions = this.pendingFromBatch({ modifiedFiles: new Set(absPaths) });
@@ -1539,14 +1441,8 @@ var ExtractionSession = class {
1539
1441
  }
1540
1442
  /** One pending microtask flushes a burst of debounce observations. */
1541
1443
  debounceStatusWriteScheduled = false;
1542
- /**
1543
- * Write one payload artifact into the session directory, enveloped with
1544
- * `{sessionId, generation, replacementEpoch, payloadHash}` and rewritten
1545
- * only when the PAYLOAD bytes changed (byte-identical re-analyses leave
1546
- * disk untouched — spec: "Unchanged manifest is not rewritten"). The
1547
- * recorded diskHash (hash of the enveloped bytes) feeds the
1548
- * analysis-commit.
1549
- */
1444
+ /** Write one enveloped payload artifact, rewritten only when the PAYLOAD
1445
+ * bytes changed; the recorded diskHash feeds the analysis-commit. */
1550
1446
  publishPayloadArtifact(key, name, payload, epoch, generation, wrap) {
1551
1447
  const payloadHash = require_pipeline.contentHash(payload);
1552
1448
  if (this.artifactRecords[key] === null) this.artifactRecords[key] = this.seedPayloadRecord(key, name);
@@ -1580,7 +1476,7 @@ var ExtractionSession = class {
1580
1476
  } catch {
1581
1477
  return null;
1582
1478
  }
1583
- if (!envelope || typeof envelope.payloadHash !== "string") return null;
1479
+ if (!envelope || !isDiskString(envelope.payloadHash)) return null;
1584
1480
  return {
1585
1481
  payloadHash: envelope.payloadHash,
1586
1482
  diskHash: require_pipeline.contentHash(bytes)
@@ -1590,18 +1486,13 @@ var ExtractionSession = class {
1590
1486
  seedCommitFromDisk() {
1591
1487
  try {
1592
1488
  const parsed = JSON.parse((0, fs.readFileSync)(analysisCommitPath(this.sessionDir), "utf-8"));
1593
- return parsed.schema === 1 && parsed.sessionId === this.sessionId && typeof parsed.generation === "number" ? parsed : null;
1489
+ return parsed.schema === 1 && parsed.sessionId === this.sessionId && isDiskNumber(parsed.generation) ? parsed : null;
1594
1490
  } catch {
1595
1491
  return null;
1596
1492
  }
1597
1493
  }
1598
- /**
1599
- * Publish the analysis-commit — the transaction identity (design D1).
1600
- * Written AFTER every payload and BEFORE the epoch; skipped entirely when
1601
- * the payload set (disk hashes) and epoch are unchanged, so a no-op
1602
- * re-analysis neither re-keys loader-worker hydration nor burns a
1603
- * generation.
1604
- */
1494
+ /** Publish the analysis-commit: written after every payload and before the
1495
+ * epoch, skipped when the payload hashes and epoch are unchanged. */
1605
1496
  publishAnalysisCommit(epoch, generation) {
1606
1497
  const manifestHash = this.artifactRecords.manifest?.diskHash ?? "";
1607
1498
  const inputsHash = this.persistAnalysisInputs ? this.artifactRecords.inputs?.diskHash ?? "" : void 0;
@@ -1614,21 +1505,14 @@ var ExtractionSession = class {
1614
1505
  generation,
1615
1506
  replacementEpoch: epoch,
1616
1507
  manifestHash,
1617
- ...inputsHash !== void 0 ? { inputsHash } : {},
1618
1508
  stylesHash
1619
1509
  };
1510
+ if (inputsHash !== void 0) commit.inputsHash = inputsHash;
1620
1511
  this.writeSessionArtifact(ANALYSIS_COMMIT_ARTIFACT, JSON.stringify(commit));
1621
1512
  this.lastCommit = commit;
1622
1513
  }
1623
- /**
1624
- * Publish the canonical replacement epoch: maintain the session-scoped
1625
- * disk witness `{schema, sessionId, epoch}`, rewritten ONLY when the
1626
- * epoch VALUE changes so style-only analyses and same-session restarts
1627
- * leave bytes and mtime untouched, then expose the value through the
1628
- * singleton for the plugin's needBuild fan-out and the loader's catch-up
1629
- * re-check. Every VALUE move also reconciles sibling sessions' epoch
1630
- * artifacts (below) — the webpack cold-cache validity witness.
1631
- */
1514
+ /** Maintain the session's epoch witness, rewritten ONLY when the value
1515
+ * changes, then expose it through the singleton. */
1632
1516
  publishReplacementEpoch(epoch) {
1633
1517
  const onDisk = this.diskEpochValue();
1634
1518
  if (this.lastEpochValue === null) this.lastEpochValue = onDisk;
@@ -1643,26 +1527,13 @@ var ExtractionSession = class {
1643
1527
  }
1644
1528
  setReplacementEpoch(epoch);
1645
1529
  }
1646
- /**
1647
- * Sibling epoch reconciliation — the cross-session half of the webpack
1648
- * persistent-cache witness (spec: dev-served-transform-coherence,
1649
- * "Offline change invalidates restored modules"). Restored-module
1650
- * snapshots reference the epoch artifact of the session that BUILT them;
1651
- * with session-scoped trees that file would otherwise sit untouched
1652
- * forever, keeping stale snapshots valid. Whenever this session's epoch
1653
- * value moves, delete every sibling epoch artifact that disagrees (their
1654
- * snapshots must invalidate — restore-on-demand then rebuilds from the
1655
- * current generation) and leave agreeing siblings byte-untouched (warm
1656
- * restores stay valid). Deleting a stale artifact is pruning, not a
1657
- * foreign-session write; races with concurrent sessions are tolerated
1658
- * (S14).
1659
- */
1660
- /** Sibling ids already reconciled away (artifact deleted or absent) —
1661
- * nothing left to invalidate for them on later moves. */
1662
- reconciledSiblingIds = /* @__PURE__ */ new Set();
1663
1530
  /** sessions-root listing memo, keyed by the root dir's mtime — a new or
1664
1531
  * pruned sibling DIRECTORY moves it; agreeing siblings stay listed. */
1665
1532
  siblingListing = null;
1533
+ /** Restored-module snapshots point at the epoch artifact of the session
1534
+ * that built them, so a moved epoch deletes every DISAGREEING sibling
1535
+ * artifact to invalidate them. Agreeing siblings stay byte-untouched so
1536
+ * warm restores keep working; a sibling with a live owner is exempt. */
1666
1537
  reconcileSiblingEpochs(epoch) {
1667
1538
  const rootPath = sessionsRootDir(this.rootDir);
1668
1539
  let entries;
@@ -1681,61 +1552,61 @@ var ExtractionSession = class {
1681
1552
  }
1682
1553
  for (const entry of entries) {
1683
1554
  if (entry === this.sessionId) continue;
1684
- if (this.reconciledSiblingIds.has(entry)) continue;
1685
- const siblingEpochPath = (0, path.join)(rootPath, entry, REPLACEMENT_EPOCH_ARTIFACT);
1555
+ const siblingDir = (0, path.join)(rootPath, entry);
1556
+ const siblingEpochPath = (0, path.join)(siblingDir, REPLACEMENT_EPOCH_ARTIFACT);
1686
1557
  try {
1687
1558
  if (JSON.parse((0, fs.readFileSync)(siblingEpochPath, "utf-8")).epoch === epoch) continue;
1688
1559
  } catch (err) {
1689
- if (err?.code === "ENOENT") {
1690
- this.reconciledSiblingIds.add(entry);
1691
- continue;
1692
- }
1560
+ if (err?.code === "ENOENT") continue;
1561
+ }
1562
+ const ownerPid = this.liveSiblingOwnerPid(siblingDir);
1563
+ if (ownerPid !== null) {
1564
+ 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`);
1565
+ continue;
1693
1566
  }
1694
1567
  try {
1695
1568
  (0, fs.unlinkSync)(siblingEpochPath);
1696
1569
  } catch {}
1697
- this.reconciledSiblingIds.add(entry);
1698
1570
  }
1699
1571
  }
1572
+ /** The pid of `siblingDir`'s live owner, or null when nothing proves one.
1573
+ * Our own pid is never one — publication is process-exclusive. A missing,
1574
+ * undecodable or unreadable claim all mean no owner: the opposite default
1575
+ * from the CLI lock check on purpose, so do not unify the two. */
1576
+ liveSiblingOwnerPid(siblingDir) {
1577
+ let claim;
1578
+ try {
1579
+ claim = readCliLockRecord(siblingDir);
1580
+ } catch {
1581
+ return null;
1582
+ }
1583
+ if (claim.kind !== "held") return null;
1584
+ if (claim.record.pid === process.pid) return null;
1585
+ return checkLockLiveness(claim.record).live ? claim.record.pid : null;
1586
+ }
1700
1587
  /** Epoch value held by this session's on-disk artifact, or null when
1701
1588
  * absent, unreadable, or not the expected schema. */
1702
1589
  diskEpochValue() {
1703
1590
  try {
1704
1591
  const parsed = JSON.parse((0, fs.readFileSync)(replacementEpochPath(this.sessionDir), "utf-8"));
1705
- return parsed.schema === 1 && typeof parsed.epoch === "string" ? parsed.epoch : null;
1592
+ return parsed.schema === 1 && isDiskString(parsed.epoch) ? parsed.epoch : null;
1706
1593
  } catch {
1707
1594
  return null;
1708
1595
  }
1709
1596
  }
1710
- /** True when the flat `.animus/` advisory lock names a live pid — a CLI
1711
- * invocation (build mid-publish, or a whole watch run) owns the flat
1712
- * tree right now, and any instantaneous inconsistency is its in-flight
1713
- * write, not debris. */
1597
+ /** True when the flat `.animus/` advisory lock claims the tree. A lock that
1598
+ * exists but cannot be decoded claims it too — unknown is never absent. */
1714
1599
  cliWriterHoldsLock(animusDir) {
1715
- let holder;
1716
- try {
1717
- holder = JSON.parse((0, fs.readFileSync)((0, path.join)(animusDir, CLI_LOCK_ARTIFACT), "utf-8"));
1718
- } catch {
1719
- return false;
1720
- }
1721
- if (typeof holder.pid !== "number") return false;
1722
- try {
1723
- process.kill(holder.pid, 0);
1724
- return true;
1725
- } catch {
1726
- return false;
1727
- }
1600
+ const lock = readCliLockRecord(animusDir);
1601
+ if (lock.kind === "none") return false;
1602
+ return lock.kind === "indeterminate" || checkLockLiveness(lock.record).live;
1728
1603
  }
1729
- /** Session-start hygiene (design D2): delete legacy flat artifacts
1730
- * (unreachable by session-scoped loaders; the standalone CLI is the one
1731
- * remaining writer of flat payload names, and its live output is
1732
- * fenced off below) and prune sibling session directories older than
1733
- * the retention window — never the own dir, tolerating races with
1734
- * concurrently-pruning sessions (S14). */
1604
+ /** Delete legacy flat artifacts and prune sibling session directories past
1605
+ * the retention window — never this session's own dir. */
1735
1606
  runSessionStartHygiene() {
1736
1607
  if (this.sessionStartHygieneDone) return;
1737
1608
  this.sessionStartHygieneDone = true;
1738
- const animusDir = (0, path.join)(this.rootDir, ".animus");
1609
+ const animusDir = (0, path.join)(this.rootDir, ANIMUS_ARTIFACT_DIR);
1739
1610
  const isCliPublishedSet = this.cliWriterHoldsLock(animusDir) || verifyCommitRecord(animusDir).length === 0;
1740
1611
  const cliPublishedNames = [
1741
1612
  MANIFEST_ARTIFACT,
@@ -1762,18 +1633,17 @@ var ExtractionSession = class {
1762
1633
  if (entry === this.sessionId) continue;
1763
1634
  const dir = (0, path.join)(sessionsRootDir(this.rootDir), entry);
1764
1635
  try {
1765
- if ((0, fs.statSync)(dir).mtimeMs < cutoff) (0, fs.rmSync)(dir, {
1636
+ if ((0, fs.statSync)(dir).mtimeMs >= cutoff) continue;
1637
+ if (this.liveSiblingOwnerPid(dir) !== null) continue;
1638
+ (0, fs.rmSync)(dir, {
1766
1639
  recursive: true,
1767
1640
  force: true
1768
1641
  });
1769
1642
  } catch {}
1770
1643
  }
1771
1644
  }
1772
- /** Ensure the session directory exists and write one artifact into it.
1773
- * Write-then-rename so cross-process readers (Turbopack loader workers)
1774
- * can never observe a torn half-written file. The tmp name carries the
1775
- * pid — Next dev evaluates the config in more than one process, and two
1776
- * sessions writing the same artifact must not race on one tmp path. */
1645
+ /** Write one artifact write-then-rename, so a cross-process reader never
1646
+ * sees a torn file; the tmp name carries the pid to keep writers apart. */
1777
1647
  writeSessionArtifact(name, content) {
1778
1648
  const dir = this.sessionDir;
1779
1649
  (0, fs.mkdirSync)(dir, { recursive: true });
@@ -1782,19 +1652,9 @@ var ExtractionSession = class {
1782
1652
  (0, fs.renameSync)(tmpPath, (0, path.join)(dir, name));
1783
1653
  this.onArtifactWrite?.(name, content);
1784
1654
  }
1785
- /**
1786
- * asset() placeholder substitution (global-styles-system): resolve each
1787
- * referenced specifier through Node resolution, copy the bytes into the
1788
- * session directory's `assets/` under a content-hashed name, and
1789
- * substitute a RELATIVE url — relative to the session-scoped styles.css,
1790
- * which sits beside `assets/`, so the emitted `./assets/<file>` form is
1791
- * unchanged. The stylesheet is processed by Next's own CSS pipeline
1792
- * (webpack and Turbopack alike), which applies its native asset handling
1793
- * — publicPath and output hashing — to relative url() references.
1794
- * Unsubstitutable specifiers warn and emit literally in non-strict mode,
1795
- * fail the build under `strict: true`.
1796
- */
1797
- substituteAssetReferences(globalCss, devMode) {
1655
+ /** Copy each asset() specifier's bytes into `assets/` under a
1656
+ * content-hashed name and substitute a url relative to styles.css. */
1657
+ substituteAssetReferences(globalCss, pruneSuperseded) {
1798
1658
  const specifiers = require_pipeline.findAssetSpecifiers(globalCss);
1799
1659
  const assetsDir = (0, path.join)(this.sessionDir, SESSION_ASSETS_DIR);
1800
1660
  const expected = /* @__PURE__ */ new Set();
@@ -1839,19 +1699,15 @@ var ExtractionSession = class {
1839
1699
  url
1840
1700
  });
1841
1701
  }
1842
- if (!devMode) pruneStaleAssets(assetsDir, expected);
1702
+ if (pruneSuperseded) pruneStaleAssets(assetsDir, expected);
1843
1703
  return require_pipeline.substituteAssetPlaceholders(globalCss, urlBySpecifier);
1844
1704
  }
1845
- trackAssetDependency(path$9) {
1846
- this.assetDependencyPaths.add(path$9);
1847
- for (const key of require_pipeline.toWatchKeys(path$9)) this.assetDependencyKeys.add(key);
1705
+ trackAssetDependency(path$8) {
1706
+ this.assetDependencyPaths.add(path$8);
1707
+ for (const key of require_pipeline.toWatchKeys(path$8)) this.assetDependencyKeys.add(key);
1848
1708
  }
1849
- /**
1850
- * Resolve an asset specifier to an absolute file via the shared pipeline
1851
- * resolver (host aliases → Node resolution → package root), with one
1852
- * session-local last resort: an already-discovered source entry's package
1853
- * root (dist-less workspace kits the shared resolver cannot see).
1854
- */
1709
+ /** Resolve an asset specifier through the shared resolver, with one
1710
+ * last resort: a discovered source entry's package root. */
1855
1711
  resolveAssetSpecifier(specifier) {
1856
1712
  const resolved = require_pipeline.resolveAssetFile(specifier, this.rootDir, this.pathAliasesJson);
1857
1713
  if (resolved) return resolved;
@@ -1867,15 +1723,8 @@ var ExtractionSession = class {
1867
1723
  return null;
1868
1724
  }
1869
1725
  };
1870
- /**
1871
- * Sync `.animus/assets/` to the current build's content-hashed file set:
1872
- * anything else in the directory is a superseded revision (the copies are
1873
- * content-addressed and never overwritten) or the leftover of an asset()
1874
- * reference that no longer exists. Failures are tolerated per entry — Next
1875
- * dev evaluates the config in more than one process, and a concurrent
1876
- * session may have removed (or be about to rewrite) the same file; every
1877
- * pass rewrites whatever of its own set is missing, so races self-heal.
1878
- */
1726
+ /** Sync `assets/` to the current build's file set; per-entry failures are
1727
+ * tolerated — a concurrent session may have removed the same file. */
1879
1728
  function pruneStaleAssets(assetsDir, expected) {
1880
1729
  if (!(0, fs.existsSync)(assetsDir)) return;
1881
1730
  let entries;
@@ -1893,61 +1742,54 @@ function pruneStaleAssets(assetsDir, expected) {
1893
1742
  }
1894
1743
  //#endregion
1895
1744
  //#region session/turbopack-orchestrator.ts
1896
- /**
1897
- * Out-of-band pipeline orchestration for Turbopack (spec:
1898
- * next-turbopack-integration). Turbopack exposes no compiler hooks, so the
1899
- * pipeline runs during next.config resolution and, in dev, from a
1900
- * filesystem watcher — the bundler only ever sees the `.animus/` disk
1901
- * artifacts.
1902
- */
1903
- /** Run the full pipeline with the Turbopack emitter identity: the virtual
1904
- * system-props id (Turbopack rejects the webpack path's absolute-path
1905
- * imports). Turbopack orchestration persists the analysis-inputs
1906
- * hydration corpus — its isolated loader workers replay it (spec:
1907
- * next-turbopack-integration, "Manifest disk artifact"; webpack mode
1908
- * skips the corpus). */
1909
1745
  async function runSessionPipeline(session) {
1910
1746
  session.systemPropsModuleId = TURBOPACK_SYSTEM_PROPS_ID;
1911
1747
  session.persistAnalysisInputs = true;
1912
1748
  await session.runFullPipeline();
1913
1749
  }
1914
1750
  const activeWatcherRoots = /* @__PURE__ */ new Set();
1915
- const IGNORED_SEGMENTS = new Set([
1916
- ".animus",
1751
+ const IGNORED_SEGMENTS = /* @__PURE__ */ new Set([
1752
+ ANIMUS_ARTIFACT_DIR,
1917
1753
  ".next",
1918
1754
  "node_modules"
1919
1755
  ]);
1920
- /**
1921
- * Start the dev watcher: fs.watch per eligible top-level directory (plus a
1922
- * non-recursive watch on the root itself), debounced into
1923
- * existence-partitioned modified/removed sets feeding
1924
- * `session.handleWatchUpdate` (serialized — updates never overlap).
1925
- * Generated and vendored trees (`.animus`, `.next`, node_modules) are never
1926
- * registered with the OS — recursive registration of those trees exhausts
1927
- * inotify/kqueue descriptors (EMFILE/ENOSPC) on large projects.
1928
- * Idempotent per project root; unref'd so it never holds the process open.
1929
- * Asynchronous FSWatcher errors degrade to no-watch with a warning instead
1930
- * of crashing the dev server. Returns a close handle, or null when this
1931
- * root is already watched or the platform lacks recursive fs.watch (Linux
1932
- * before Node 20 — degrades to no-watch with a warning).
1933
- */
1934
- function startTurbopackWatcher(session, rootDir, debounceMs = 75, watchFn = fs.watch) {
1935
- if (activeWatcherRoots.has(rootDir)) return null;
1756
+ /** Registration failures meaning the OS is out of watch capacity: descriptor
1757
+ * and inotify limits surface under all of these. */
1758
+ const CAPACITY_CODES = /* @__PURE__ */ new Set([
1759
+ "EMFILE",
1760
+ "ENFILE",
1761
+ "ENOSPC",
1762
+ "EPERM"
1763
+ ]);
1764
+ function errnoCode(error) {
1765
+ if (!(error instanceof Object) || !("code" in error)) return null;
1766
+ const { code } = error;
1767
+ return isIntrinsicString(code) ? code : null;
1768
+ }
1769
+ /** A primitive string, decided by the intrinsic tag. `Object(value) !== value`
1770
+ * rejects boxed strings and a forged `Symbol.toStringTag`. */
1771
+ function isIntrinsicString(value) {
1772
+ return Object(value) !== value && Object.prototype.toString.call(value) === "[object String]";
1773
+ }
1774
+ function startTurbopackWatcher(session, rootDir, options = {}) {
1775
+ const { debounceMs = 75, watchFn = fs.watch, holdEvents = false } = options;
1776
+ if (activeWatcherRoots.has(rootDir)) return { kind: "already-watched" };
1936
1777
  activeWatcherRoots.add(rootDir);
1937
- if (typeof session.noteDebouncedWatchEvents === "function") session.debounceCeilingMs = debounceMs;
1778
+ session.debounceCeilingMs = debounceMs;
1938
1779
  const pendingPaths = /* @__PURE__ */ new Set();
1939
1780
  const watchers = /* @__PURE__ */ new Map();
1940
1781
  let timer = null;
1941
1782
  let updateChain = Promise.resolve();
1942
1783
  let closed = false;
1943
1784
  let died = false;
1785
+ let holdingEvents = holdEvents;
1944
1786
  const externalWatchers = /* @__PURE__ */ new Map();
1945
1787
  let pendingOpened = /* @__PURE__ */ new Map();
1946
1788
  let capturedDuringSnapshot = [];
1947
1789
  const externalDiagnosticKey = (root) => `external-watch:${root}`;
1948
1790
  const failureReason = (err) => {
1949
- const code = err?.code;
1950
- if (code === "EMFILE" || code === "ENFILE" || code === "ENOSPC" || code === "EPERM") return `capacity(${code})`;
1791
+ const code = errnoCode(err);
1792
+ if (code !== null && CAPACITY_CODES.has(code)) return `capacity(${code})`;
1951
1793
  if (/inotify|too many/i.test(String(err))) return "capacity";
1952
1794
  return code ?? "error";
1953
1795
  };
@@ -1966,14 +1808,17 @@ function startTurbopackWatcher(session, rootDir, debounceMs = 75, watchFn = fs.w
1966
1808
  const removedFiles = /* @__PURE__ */ new Set();
1967
1809
  for (const path$1 of pendingPaths) ((0, fs.existsSync)(path$1) ? modifiedFiles : removedFiles).add(path$1);
1968
1810
  pendingPaths.clear();
1969
- updateChain = updateChain.then(() => session.handleWatchUpdate({
1970
- modifiedFiles,
1971
- removedFiles
1972
- }).catch((err) => {
1973
- console.warn(`[animus-extract] Turbopack watch update failed: ${String(err)}`);
1974
- }).then(() => {
1975
- if (pendingOpened.size > 0 || capturedDuringSnapshot.length > 0) rollbackPendingExternal();
1976
- }));
1811
+ updateChain = updateChain.then(() => {
1812
+ if (closed) return;
1813
+ return session.handleWatchUpdate({
1814
+ modifiedFiles,
1815
+ removedFiles
1816
+ }).catch((err) => {
1817
+ console.warn(`[animus-extract] watch update failed: ${String(err)}`);
1818
+ }).then(() => {
1819
+ if (pendingOpened.size > 0 || capturedDuringSnapshot.length > 0) rollbackPendingExternal();
1820
+ });
1821
+ });
1977
1822
  };
1978
1823
  const rollbackPendingExternal = () => {
1979
1824
  for (const watcher of pendingOpened.values()) watcher.close();
@@ -2002,6 +1847,7 @@ function startTurbopackWatcher(session, rootDir, debounceMs = 75, watchFn = fs.w
2002
1847
  };
2003
1848
  const enqueuePath = (abs) => {
2004
1849
  pendingPaths.add(abs);
1850
+ if (holdingEvents) return;
2005
1851
  try {
2006
1852
  session.noteDebouncedWatchEvents?.([abs]);
2007
1853
  } catch (err) {
@@ -2022,11 +1868,6 @@ function startTurbopackWatcher(session, rootDir, debounceMs = 75, watchFn = fs.w
2022
1868
  } catch {}
2023
1869
  enqueuePath(abs);
2024
1870
  };
2025
- /** External-root event path: fenced by watcher identity ({watcherRootId,
2026
- * generation} — a handle that is neither current nor pending belongs to
2027
- * a retired generation and its events are rejected), `filename == null`
2028
- * marks the ROOT dirty for rediscovery, and events observed while the
2029
- * root's generation is still snapshotting are buffered for replay. */
2030
1871
  const onExternalEvent = (root, self, filename) => {
2031
1872
  if (closed) return;
2032
1873
  const watcher = self();
@@ -2046,10 +1887,6 @@ function startTurbopackWatcher(session, rootDir, debounceMs = 75, watchFn = fs.w
2046
1887
  }
2047
1888
  enqueuePath(abs);
2048
1889
  };
2049
- /** Open one external-root watcher into `into` (the committed set at
2050
- * startup, the pending set during reset reconciliation). Failures
2051
- * degrade ONLY this root (design D7); node_modules-resident roots are
2052
- * documented unwatchable and never registered (guardrail G2). */
2053
1890
  const openExternalWatcher = (root, into) => {
2054
1891
  if (root.split(/[\\/]/).includes("node_modules")) return;
2055
1892
  try {
@@ -2078,7 +1915,7 @@ function startTurbopackWatcher(session, rootDir, debounceMs = 75, watchFn = fs.w
2078
1915
  } catch (err) {
2079
1916
  closeAll();
2080
1917
  console.warn(`[animus-extract] Turbopack dev watcher unavailable (${String(err)}); source edits require a dev-server restart`);
2081
- return null;
1918
+ return { kind: "unavailable" };
2082
1919
  }
2083
1920
  for (const root of session.externalWatchRoots ?? []) openExternalWatcher(root, externalWatchers);
2084
1921
  session.onExternalRootResolved = (root) => {
@@ -2103,14 +1940,25 @@ function startTurbopackWatcher(session, rootDir, debounceMs = 75, watchFn = fs.w
2103
1940
  return died;
2104
1941
  },
2105
1942
  onDied: null,
1943
+ deliverHeldEvents: () => {
1944
+ holdingEvents = false;
1945
+ if (pendingPaths.size > 0) flush();
1946
+ return updateChain;
1947
+ },
2106
1948
  settle: () => updateChain
2107
1949
  };
2108
- return handle;
1950
+ return {
1951
+ kind: "started",
1952
+ handle
1953
+ };
2109
1954
  }
2110
1955
  //#endregion
1956
+ exports.ANIMUS_ARTIFACT_DIR = ANIMUS_ARTIFACT_DIR;
2111
1957
  exports.ANIMUS_CSS_MODULE_ID = ANIMUS_CSS_MODULE_ID;
2112
1958
  exports.CLI_COMMIT_ARTIFACT = CLI_COMMIT_ARTIFACT;
2113
1959
  exports.CLI_LOCK_ARTIFACT = CLI_LOCK_ARTIFACT;
1960
+ exports.CLI_LOCK_HEARTBEAT_INTERVAL_MS = CLI_LOCK_HEARTBEAT_INTERVAL_MS;
1961
+ exports.CLI_LOCK_STALE_AFTER_MS = CLI_LOCK_STALE_AFTER_MS;
2114
1962
  exports.ExtractionSession = ExtractionSession;
2115
1963
  exports.MANIFEST_ARTIFACT = MANIFEST_ARTIFACT;
2116
1964
  exports.SESSION_ASSETS_DIR = SESSION_ASSETS_DIR;
@@ -2120,10 +1968,11 @@ exports.TURBOPACK_SYSTEM_PROPS_ID = TURBOPACK_SYSTEM_PROPS_ID;
2120
1968
  exports.analysisCommitPath = analysisCommitPath;
2121
1969
  exports.analysisInputsPath = analysisInputsPath;
2122
1970
  exports.analysisStatusPath = analysisStatusPath;
2123
- exports.claimExclusiveSessionOwner = claimExclusiveSessionOwner;
1971
+ exports.checkLockLiveness = checkLockLiveness;
2124
1972
  exports.collectSessionAssets = collectSessionAssets;
1973
+ exports.decodeCommitRecord = decodeCommitRecord;
2125
1974
  exports.engineApi = engineApi;
2126
- exports.getAnalysisPromise = getAnalysisPromise;
1975
+ exports.getAnalysisStartedPromise = getAnalysisStartedPromise;
2127
1976
  exports.getAnalyzedHashes = getAnalyzedHashes;
2128
1977
  exports.getManifestJson = getManifestJson;
2129
1978
  exports.getReplacementEpoch = getReplacementEpoch;
@@ -2132,13 +1981,17 @@ exports.getSharedCss = getSharedCss;
2132
1981
  exports.getSharedExternalDirs = getSharedExternalDirs;
2133
1982
  exports.getSharedExternalEntries = getSharedExternalEntries;
2134
1983
  exports.getSharedSystemProps = getSharedSystemProps;
1984
+ exports.holdDirectoryClaim = holdDirectoryClaim;
1985
+ exports.isLockHolderAlive = isLockHolderAlive;
1986
+ exports.lockRecordBytes = lockRecordBytes;
2135
1987
  exports.manifestPath = manifestPath;
2136
1988
  exports.pruneStaleAssets = pruneStaleAssets;
1989
+ exports.readCliLockRecord = readCliLockRecord;
2137
1990
  exports.readJsonEnvelope = readJsonEnvelope;
2138
1991
  exports.replacementEpochPath = replacementEpochPath;
2139
1992
  exports.runSessionPipeline = runSessionPipeline;
2140
1993
  exports.sessionArtifactDir = sessionArtifactDir;
2141
- exports.setAnalysisPromise = setAnalysisPromise;
1994
+ exports.setAnalysisStartedPromise = setAnalysisStartedPromise;
2142
1995
  exports.setSharedEngine = setSharedEngine;
2143
1996
  exports.startTurbopackWatcher = startTurbopackWatcher;
2144
1997
  exports.stylesPath = stylesPath;