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