@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
@@ -1,21 +1,17 @@
1
1
  import { createRequire } from "node:module";
2
+ import path, { basename, dirname, join, normalize, posix } from "node:path";
2
3
  import { createHash } from "crypto";
3
4
  import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "fs";
4
- import { dirname, extname, isAbsolute, join, relative, resolve } from "path";
5
+ import { dirname as dirname$1, extname, isAbsolute, join as join$1, relative, resolve } from "path";
5
6
  import { realpathSync as realpathSync$1 } from "node:fs";
6
- import path, { basename as basename$1, dirname as dirname$1, join as join$1, normalize, posix } from "node:path";
7
7
  import { createRequire as createRequire$1 } from "module";
8
8
  import browserslist from "browserslist";
9
9
  import { browserslistToTargets, transform } from "lightningcss";
10
10
  import { isUnitlessProperty } from "@animus-ui/properties";
11
11
  //#region \0rolldown/runtime.js
12
- var __require = /* @__PURE__ */ createRequire(import.meta.url);
12
+ var __require = /* #__PURE__ */ (() => createRequire(import.meta.url))();
13
13
  //#endregion
14
14
  //#region pipeline/assemble-stylesheet.ts
15
- /**
16
- * The 7 Animus cascade layers. Always prefixed with `anm-` to avoid
17
- * collision with other frameworks' layer names (e.g., Tailwind's `base`).
18
- */
19
15
  const ANIMUS_LAYERS = [
20
16
  "anm-global",
21
17
  "anm-base",
@@ -25,22 +21,9 @@ const ANIMUS_LAYERS = [
25
21
  "anm-system",
26
22
  "anm-custom"
27
23
  ];
28
- /**
29
- * Build the `@layer` declaration line.
30
- *
31
- * When `isCustom` is false (default layers), uses ANIMUS_LAYERS directly.
32
- * When `isCustom` is true, passes names through as-is — the consumer already
33
- * wrote the final names including the `anm-` prefixed entries.
34
- */
35
24
  function buildLayerDeclaration(layers, isCustom) {
36
25
  return `@layer ${(isCustom ? layers : [...ANIMUS_LAYERS]).join(", ")};\n`;
37
26
  }
38
- /**
39
- * Validate that a consumer `layers` array contains all 7 Animus layers
40
- * in the correct relative order. Consumer layers may be interleaved.
41
- *
42
- * @throws Error with descriptive message on violation
43
- */
44
27
  function validateLayerOrder(layers) {
45
28
  const expected = [...ANIMUS_LAYERS];
46
29
  let cursor = 0;
@@ -55,11 +38,6 @@ function validateLayerOrder(layers) {
55
38
  throw new Error(`[animus-extract] Custom layers have wrong order. Found ${found.join(", ")} but then expected ${missing[0]}. Required order: (${expected.join(" < ")}). You may interleave custom layers but must preserve this subsequence.`);
56
39
  }
57
40
  }
58
- /**
59
- * Strip a leading `@layer ...;` declaration line from CSS if present.
60
- * The Rust crate embeds this in prod-mode output; we strip it so the
61
- * shared assembler controls placement.
62
- */
63
41
  function stripLeadingLayerDeclaration(css) {
64
42
  return css.replace(/^@layer\s+[^;{]+;\s*\n?/, "");
65
43
  }
@@ -108,2091 +86,1891 @@ function buildAnalyzeProjectArgs(inputs) {
108
86
  ];
109
87
  }
110
88
  //#endregion
111
- //#region pipeline/engine-adapter.ts
112
- /**
113
- * Build the engine-agnostic `engineApi()` both plugins call. Returns a function
114
- * (invoked per call site) so `isV2()` and `loadNativeEngine()` re-evaluate each
115
- * time — next reads its engine choice and native module dynamically from
116
- * globalThis.
117
- */
118
- function createV2EngineApi(deps) {
119
- const { label, isV2, loadNativeEngine, store } = deps;
120
- return () => {
121
- if (!isV2()) return loadNativeEngine();
122
- const native = loadNativeEngine();
123
- return {
124
- loadSystemModule: (...args) => native.loadSystemModule(...args),
125
- scanKeyframesExports: (entryPath, rootDir) => native.scanKeyframesExports(entryPath, rootDir) ?? null,
126
- extractFacts: (filesJson) => native.extractFacts(filesJson),
127
- analyzeProject: (filesJsonRaw, scalesJson, variableMapJson, contextualVarsJson, propConfigJson, groupRegistryJson, packageResolutionJson, devMode, emitterConfigJson, selectorAliasesJson, _selectorOrderJson, globalStyleBlocksJson, pathAliasesJson, keyframesJson, staticCssJson, conditionAliasesJson, externalDirsJson, transformSourcesJson) => {
128
- const filesJson = deps.rehydrateFilesJson ? deps.rehydrateFilesJson(filesJsonRaw) : filesJsonRaw;
129
- const sent = /* @__PURE__ */ new Map();
130
- for (const entry of JSON.parse(filesJson)) sent.set(entry.path, entry.source);
131
- store.setSentSources(sent);
132
- const emitterConfig = emitterConfigJson ? JSON.parse(emitterConfigJson) : {};
133
- store.setEngine(null);
134
- const config = {
135
- runtimeImport: emitterConfig.runtime_import ?? void 0,
136
- cssModuleId: emitterConfig.css_module_id ?? void 0,
137
- systemPropsModuleId: emitterConfig.system_props_module_id ?? void 0,
138
- themeJson: scalesJson,
139
- variableMapJson,
140
- contextualVarsJson: contextualVarsJson ?? void 0,
141
- configJson: propConfigJson,
142
- groupRegistryJson,
143
- selectorAliasesJson: selectorAliasesJson ?? void 0,
144
- conditionAliasesJson: conditionAliasesJson ?? void 0,
145
- globalStyleBlocksJson: globalStyleBlocksJson ?? void 0,
146
- keyframesJson: keyframesJson ?? void 0,
147
- packageResolutionJson: packageResolutionJson ?? void 0,
148
- pathAliasesJson: pathAliasesJson ?? void 0,
149
- staticCssJson: staticCssJson ?? void 0,
150
- externalDirsJson: externalDirsJson ?? void 0,
151
- transformSourcesJson: transformSourcesJson ?? void 0,
152
- devMode
153
- };
154
- const engine = new native.ExtractEngine(config);
155
- store.setEngine(engine);
156
- return engine.analyze(filesJson);
157
- },
158
- transformFile: (source, path, _manifest) => {
159
- const engine = store.getEngine();
160
- if (!engine) throw new Error(`[${label}] v2 transform before analyze — engine instance not initialized`);
161
- const sentMap = store.getSentSources();
162
- if (deps.passThroughUnknownPaths && sentMap && !sentMap.has(path)) return {
163
- code: source,
164
- hasComponents: false
165
- };
166
- const sent = sentMap?.get(path);
167
- if (sent !== void 0 && sent !== source && !store.getDriftWarned()) {
168
- store.setDriftWarned(true);
169
- console.warn(`[${label}] v2: transform-time source for ${path} differs from analyze-time source — an upstream transform may be reverted`);
170
- }
171
- return JSON.parse(engine.transformFile(path));
172
- },
173
- clearAnalysisCache: () => {
174
- const engine = store.getEngine();
175
- if (engine) engine.clearCache();
176
- store.setEngine(null);
177
- }
178
- };
179
- };
180
- }
181
- //#endregion
182
- //#region pipeline/engine-retirement.ts
183
- /**
184
- * Loud rejection of the retired v1 extraction engine (openspec:
185
- * retire-extract-v1).
186
- *
187
- * v2 is the only engine. Selecting v1 — via a plugin `engine: 'v1'` option or
188
- * the `ANIMUS_ENGINE=v1` fixture env override — is a build-stopping error, never
189
- * a silent upgrade to v2 (design D1: a silent flip would let a pinned consumer's
190
- * build change engines without notice). Both extraction plugins call this before
191
- * any engine selection so the rejection is uniform and the message is defined
192
- * once.
193
- */
194
- /** The one canonical retirement message, shared verbatim by both plugins and
195
- * the env path (tasks.md cross-cutting: define once, reuse). */
196
- const RETIRED_ENGINE_MESSAGE = "animus: extraction engine 'v1' was retired (openspec: retire-extract-v1) — v2 is the only engine; remove the engine option / ANIMUS_ENGINE override";
197
- /**
198
- * Throw the canonical retirement error when v1 is selected through either
199
- * surface. Accepts the raw engine option as a string because the option type no
200
- * longer admits `'v1'` — callers cast it so a stale `'v1'` config still fails
201
- * loud at runtime instead of being silently coerced.
202
- */
203
- function assertNoRetiredEngineSelection(engineOption) {
204
- if (engineOption === "v1" || process.env.ANIMUS_ENGINE === "v1") throw new Error(RETIRED_ENGINE_MESSAGE);
205
- }
206
- //#endregion
207
89
  //#region pipeline/content-hash.ts
208
- /**
209
- * MD5 content hash used for file-change detection (HMR diffing) by both
210
- * extraction plugins, and for content-addressing copied asset bytes. The
211
- * algorithm/encoding is a cross-plugin contract: cache keys written by one
212
- * build path must compare equal in the next. Accepts a Buffer directly so
213
- * binary sources hash without an intermediate string copy.
214
- */
215
90
  function contentHash(source) {
216
91
  return createHash("md5").update(source).digest("hex");
217
92
  }
218
93
  //#endregion
219
- //#region pipeline/external-keyframes.ts
220
- /** Stable codes for external keyframes discovery. */
221
- const KEYFRAMES_EXTERNAL_ENTRY_FAILED = "animus.keyframes.external-entry-failed";
222
- const KEYFRAMES_EXPORT_COLLISION = "animus.keyframes.export-collision";
223
- /**
224
- * Merge `Keyframes` collections discovered from external package entries into
225
- * the consumer system's collections. Keyframes are the sole carve-out from
226
- * the consumer-config singular-authority rule: nothing but branded
227
- * collections is read from an entry, an entry that fails to evaluate
228
- * degrades to a coded diagnostic naming it, and a collection whose export
229
- * name collides keeps the earlier (consumer-first) collection with a coded
230
- * diagnostic instead of silently reordering names.
231
- */
232
- function mergeExternalKeyframes(scan, consumerKeyframesJson, externalEntries, rootDir) {
233
- const merged = {};
234
- if (consumerKeyframesJson) try {
235
- Object.assign(merged, JSON.parse(consumerKeyframesJson));
236
- } catch {
237
- return {
238
- keyframesJson: consumerKeyframesJson,
239
- diagnostics: []
240
- };
241
- }
242
- const diagnostics = [];
243
- const seenEntries = /* @__PURE__ */ new Set();
244
- for (const entryPath of externalEntries) {
245
- if (seenEntries.has(entryPath)) continue;
246
- seenEntries.add(entryPath);
247
- let scanned;
248
- try {
249
- scanned = scan(entryPath, rootDir);
250
- } catch (error) {
251
- diagnostics.push({
252
- file: entryPath,
253
- component: "keyframes",
254
- kind: "warn",
255
- message: `external package entry failed the keyframes scan — its collections are invisible to extraction: ${String(error)}. Keyframe collections must be reachable from the package's definition entry without evaluating framework re-exports: export them (directly or as a named re-export) from the definition entry, and avoid \`export *\` of framework packages there (${KEYFRAMES_EXTERNAL_ENTRY_FAILED})`,
256
- code: KEYFRAMES_EXTERNAL_ENTRY_FAILED,
257
- severity: "warn"
258
- });
259
- continue;
260
- }
261
- if (!scanned) continue;
262
- let collections;
263
- try {
264
- collections = JSON.parse(scanned);
265
- } catch {
266
- continue;
267
- }
268
- for (const [exportName, collection] of Object.entries(collections)) {
269
- const existing = merged[exportName];
270
- if (existing !== void 0) {
271
- if (JSON.stringify(existing) !== JSON.stringify(collection)) diagnostics.push({
272
- file: entryPath,
273
- component: exportName,
274
- kind: "warn",
275
- message: `keyframes collection export '${exportName}' collides with an earlier collection of the same name — the earlier one wins (${KEYFRAMES_EXPORT_COLLISION})`,
276
- code: KEYFRAMES_EXPORT_COLLISION,
277
- severity: "warn"
278
- });
279
- continue;
280
- }
281
- merged[exportName] = collection;
282
- }
94
+ //#region pipeline/internal-wire.ts
95
+ function parseInternalWire(json, wire) {
96
+ let parsed;
97
+ try {
98
+ parsed = JSON.parse(json);
99
+ } catch (error) {
100
+ throw new TypeError(`[animus] ${wire} is not valid JSON. animus produces this wire, so this is an engine bug rather than a configuration error: ${String(error)}`, { cause: error });
283
101
  }
284
- return {
285
- keyframesJson: Object.keys(merged).length > 0 ? JSON.stringify(merged) : null,
286
- diagnostics
287
- };
102
+ return parsed;
288
103
  }
289
104
  //#endregion
290
- //#region pipeline/core-options.ts
291
- /** Closed set of driver namespaces a shared config file may carry. */
292
- const DRIVER_NAMESPACES = [
293
- "vite",
294
- "next",
295
- "cli"
296
- ];
105
+ //#region pipeline/mdx-preprocessor.ts
297
106
  /**
298
- * Structural exclusions applied unconditionally, whether or not the user
299
- * supplies an `exclude` list. `node_modules` is owned by the external-package
300
- * collection path (never the source walk), and `.next`/`.animus` are artifact
301
- * output directories inside the scanned root that must never be re-ingested
302
- * as source.
107
+ * MDX compiled to scanner-consumable JSX, plus the extension sets every
108
+ * driver imports. `@mdx-js/mdx` is an optional peer, so its import is lazy.
303
109
  */
304
- const STRUCTURAL_EXCLUDE = [
305
- "node_modules",
306
- ".next",
307
- ".animus"
110
+ const DEFAULT_EXTENSIONS = [
111
+ ".ts",
112
+ ".tsx",
113
+ ".js",
114
+ ".jsx",
115
+ ".mdx"
308
116
  ];
309
117
  /**
310
- * Convenience defaults applied only when the user supplies no `exclude`
311
- * list — a user list replaces these (but never the structural set).
118
+ * The one engine-transform file gate every driver imports — the suffixes the
119
+ * engine's parser accepts. `.mdx` is preprocessed to `.tsx` before it.
312
120
  */
313
- const REPLACEABLE_DEFAULT_EXCLUDE = [
314
- "dist",
315
- ".test.",
316
- ".spec."
121
+ const ENGINE_TRANSFORM_EXTENSIONS = [
122
+ "ts",
123
+ "tsx",
124
+ "js",
125
+ "jsx",
126
+ "mjs"
317
127
  ];
128
+ const ENGINE_TRANSFORM_RE = new RegExp(`\\.(?:${ENGINE_TRANSFORM_EXTENSIONS.join("|")})$`);
129
+ function isEngineTransformExtension(path) {
130
+ return ENGINE_TRANSFORM_RE.test(path);
131
+ }
318
132
  /**
319
- * Default path patterns excluded from source discovery — the single
320
- * authoritative set for every driver when no user `exclude` is given.
321
- * Composed, never hand-listed: a structural addition must not silently
322
- * miss this union.
133
+ * Compile MDX to scanner-consumable JSX. `missing-dep` means the consumer
134
+ * must install `@mdx-js/mdx`; hosts warn and skip the file on `error`.
323
135
  */
324
- const DEFAULT_EXCLUDE = [...STRUCTURAL_EXCLUDE, ...REPLACEABLE_DEFAULT_EXCLUDE];
325
- /** Core keys accepted at the top level of any driver's options. */
326
- const CORE_OPTION_KEYS = new Set([
327
- "system",
328
- "exclude",
329
- "extensions",
330
- "strict",
331
- "verbose",
332
- "prefix",
333
- "targets",
334
- "minify",
335
- "staticCss",
336
- "layers",
337
- "engine",
338
- "mode",
339
- "root"
340
- ]);
341
- /** Configuration errors carry a stable name so drivers can map them to
342
- * their config-error surface (the CLI's exit code 2). */
343
- var AnimusConfigError = class extends Error {
344
- constructor(message) {
345
- super(message);
346
- this.name = "AnimusConfigError";
136
+ async function preprocessMdx(source, filename) {
137
+ const mdxMod = await import("@mdx-js/mdx").catch(() => null);
138
+ if (mdxMod === null) return { kind: "missing-dep" };
139
+ try {
140
+ const vfile = await mdxMod.compile(source, {
141
+ outputFormat: "program",
142
+ development: false,
143
+ jsx: true
144
+ });
145
+ return {
146
+ kind: "ok",
147
+ source: `/* @mdx-source: ${filename} */\n${String(vfile)}`
148
+ };
149
+ } catch (err) {
150
+ return {
151
+ kind: "error",
152
+ error: err instanceof Error ? err.message : String(err)
153
+ };
154
+ }
155
+ }
156
+ //#endregion
157
+ //#region pipeline/svelte-source-adapter.ts
158
+ const isCompilerReference = (value) => Object(value) === value;
159
+ const isCompilerCallable = (value) => {
160
+ if (!isCompilerReference(value)) return false;
161
+ try {
162
+ Function.prototype.toString.call(value);
163
+ return true;
164
+ } catch {
165
+ return false;
347
166
  }
348
167
  };
349
- /**
350
- * Reject unknown top-level option keys and invalid core values. `ownKeys`
351
- * is the calling driver's legacy/driver-specific top-level surface (e.g.
352
- * Vite's `verify`, Next's `cssImportTarget`); driver namespaces are always
353
- * legal and inert for non-owning drivers. `rejectKeys` names core keys the
354
- * calling driver does NOT honor — accepting-and-ignoring a key the
355
- * validator vouches for is the exact silent failure this seam exists to
356
- * kill, so the driver must reject it with its reason (or, in `'warn'`
357
- * mode, name it loudly while the key stays inert as it always was).
358
- */
359
- function assertKnownOptionKeys(raw, ownKeys = [], rejectKeys = [], opts = {}) {
360
- const surface = (message) => {
361
- if (opts.onUnknownKey === "warn") {
362
- opts.warn(message);
363
- return;
364
- }
365
- throw new AnimusConfigError(message);
366
- };
367
- const own = new Set(ownKeys);
368
- for (const key of Object.keys(raw)) {
369
- if (raw[key] === void 0) continue;
370
- const rejected = rejectKeys.find((entry) => entry.key === key);
371
- if (rejected) {
372
- surface(`Option "${key}" is not supported by this driver: ${rejected.reason}`);
373
- continue;
374
- }
375
- if (CORE_OPTION_KEYS.has(key) || DRIVER_NAMESPACES.includes(key) || own.has(key)) continue;
376
- const nearest = suggestNearest(key);
377
- surface(`Unknown option "${key}".${nearest ? ` Did you mean "${nearest}"?` : ""} Core keys: ${[...CORE_OPTION_KEYS].join(", ")}. Driver namespaces: ${DRIVER_NAMESPACES.join(", ")}.`);
168
+ const isCompilerRecord = (value) => isCompilerReference(value) && !isCompilerCallable(value);
169
+ const acceptsCompilerPrimitive = (value, read) => {
170
+ if (isCompilerReference(value)) return false;
171
+ try {
172
+ read();
173
+ return true;
174
+ } catch {
175
+ return false;
378
176
  }
379
- const mode = raw["mode"];
380
- if (mode !== void 0 && mode !== "development" && mode !== "production") throw new AnimusConfigError(`Invalid mode "${String(mode)}" — expected "development" or "production".`);
381
- for (const { key, ok, expected } of CORE_VALUE_GATES) {
382
- const value = raw[key];
383
- if (value === void 0 || ok(value)) continue;
384
- throw new AnimusConfigError(`Invalid value for "${key}" — expected ${expected}, got ${JSON.stringify(value)}.`);
177
+ };
178
+ const isCompilerString = (value) => acceptsCompilerPrimitive(value, () => String.prototype.valueOf.call(value));
179
+ const isCompilerNumber = (value) => acceptsCompilerPrimitive(value, () => Number.prototype.valueOf.call(value));
180
+ function isNode(value) {
181
+ return isCompilerRecord(value) && isCompilerString(value.type) && isCompilerNumber(value.start) && isCompilerNumber(value.end);
182
+ }
183
+ function childNode(value) {
184
+ return isNode(value) ? value : null;
185
+ }
186
+ function isProgramNode(value) {
187
+ return isNode(value) && Array.isArray(value.body) && value.body.every((child) => isNode(child));
188
+ }
189
+ function isScriptNode(value) {
190
+ return isNode(value) && isProgramNode(value.content);
191
+ }
192
+ function parseSvelteAst(value) {
193
+ if (value.module !== void 0 && value.module !== null && !isScriptNode(value.module)) throw new TypeError("svelte/compiler returned an invalid module script");
194
+ if (value.instance !== void 0 && value.instance !== null && !isScriptNode(value.instance)) throw new TypeError("svelte/compiler returned an invalid instance script");
195
+ if (!isCompilerRecord(value.fragment)) throw new TypeError("svelte/compiler returned an invalid fragment");
196
+ return {
197
+ module: value.module,
198
+ instance: value.instance,
199
+ fragment: value.fragment
200
+ };
201
+ }
202
+ function nodeName(node) {
203
+ return node?.type === "Identifier" && isCompilerString(node.name) ? node.name : null;
204
+ }
205
+ /** Adaptation touches one source at a time, so a last-value memo runs the
206
+ * all-ASCII check once per file rather than per span endpoint. */
207
+ let asciiMemoSource = null;
208
+ let asciiMemoValue = false;
209
+ function isAsciiOnly(source) {
210
+ if (asciiMemoSource !== source) {
211
+ asciiMemoSource = source;
212
+ asciiMemoValue = Buffer.byteLength(source) === source.length;
385
213
  }
214
+ return asciiMemoValue;
386
215
  }
387
- const isStringArray = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
388
- const CORE_VALUE_GATES = [
389
- {
390
- key: "system",
391
- ok: (v) => typeof v === "string",
392
- expected: "a string path"
393
- },
394
- {
395
- key: "exclude",
396
- ok: isStringArray,
397
- expected: "an array of string patterns"
398
- },
399
- {
400
- key: "extensions",
401
- ok: isStringArray,
402
- expected: "an array of string extensions"
403
- },
404
- {
405
- key: "strict",
406
- ok: (v) => typeof v === "boolean",
407
- expected: "a boolean"
408
- },
409
- {
410
- key: "verbose",
411
- ok: (v) => typeof v === "boolean",
412
- expected: "a boolean"
413
- },
414
- {
415
- key: "minify",
416
- ok: (v) => typeof v === "boolean",
417
- expected: "a boolean"
418
- },
419
- {
420
- key: "prefix",
421
- ok: (v) => typeof v === "string",
422
- expected: "a string"
423
- },
424
- {
425
- key: "targets",
426
- ok: (v) => typeof v === "string" || isStringArray(v),
427
- expected: "a string or an array of strings"
428
- },
429
- {
430
- key: "layers",
431
- ok: isStringArray,
432
- expected: "an array of layer names"
433
- }
434
- ];
435
- function suggestNearest(key) {
436
- const lower = key.toLowerCase();
437
- let best = null;
438
- let bestScore = 3;
439
- for (const candidate of CORE_OPTION_KEYS) {
440
- const score = editDistance(lower, candidate.toLowerCase());
441
- if (score < bestScore) {
442
- bestScore = score;
443
- best = candidate;
444
- }
445
- }
446
- return best;
447
- }
448
- function editDistance(a, b) {
449
- const dp = Array.from({ length: a.length + 1 }, (_, i) => [i]);
450
- for (let j = 1; j <= b.length; j++) dp[0][j] = j;
451
- for (let i = 1; i <= a.length; i++) for (let j = 1; j <= b.length; j++) dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
452
- return dp[a.length][b.length];
216
+ function byteOffset(source, characterOffset) {
217
+ if (isAsciiOnly(source)) return characterOffset;
218
+ return Buffer.byteLength(source.slice(0, characterOffset));
453
219
  }
454
- const GLOB_META = /[*?]/;
455
- /**
456
- * Compile one exclusion pattern. Glob patterns (containing `*` or `?`)
457
- * compile to a RegExp over the slash-normalized root-relative path — a
458
- * match on the path itself OR any ancestor directory excludes it, so a
459
- * directory-shaped glob (double-star slash "generated") excludes files
460
- * inside that directory identically for tree discovery (which prunes at
461
- * the directory) and per-file watch classification (which only ever sees
462
- * file paths).
463
- * Plain patterns keep the historical substring semantics over both path
464
- * forms.
465
- * A leading `./` is stripped first: relative paths are computed without
466
- * one, so `./fixtures/**` must mean exactly `fixtures/**` rather than
467
- * compiling to a regex that can never match.
468
- */
469
- function compilePattern(raw) {
470
- let pattern = raw.split("\\").join("/");
471
- while (pattern.startsWith("./")) pattern = pattern.slice(2);
472
- if (pattern === "") return () => false;
473
- if (!GLOB_META.test(pattern)) return (full, rel) => full.includes(pattern) || rel.includes(pattern);
474
- const regex = globToRegExp(pattern);
475
- return (_full, rel) => {
476
- const normalized = rel.split("\\").join("/");
477
- if (regex.test(normalized)) return true;
478
- for (let slash = normalized.indexOf("/"); slash !== -1; slash = normalized.indexOf("/", slash + 1)) if (regex.test(normalized.slice(0, slash))) return true;
479
- return false;
220
+ function byteSpan(source, node) {
221
+ return {
222
+ start: byteOffset(source, node.start),
223
+ end: byteOffset(source, node.end)
480
224
  };
481
225
  }
482
- /** Minimal glob → RegExp: `**` spans segments, `*`/`?` stay within one. */
483
- function globToRegExp(glob) {
484
- let out = "";
485
- let i = 0;
486
- const normalized = glob.split("\\").join("/");
487
- while (i < normalized.length) {
488
- const ch = normalized[i];
489
- if (ch === "*") if (normalized[i + 1] === "*") if (normalized[i + 2] === "/") {
490
- out += "(?:[^/]+/)*";
491
- i += 3;
492
- } else {
493
- out += ".*";
494
- i += 2;
495
- }
496
- else {
497
- out += "[^/]*";
498
- i += 1;
499
- }
500
- else if (ch === "?") {
501
- out += "[^/]";
502
- i += 1;
503
- } else {
504
- out += ch.replace(/[.+^${}()|[\]\\]/g, "\\$&");
505
- i += 1;
226
+ function sourcePosition(source, characterOffset) {
227
+ let line = 1;
228
+ let lineStart = 0;
229
+ for (let index = 0; index < characterOffset; index += 1) {
230
+ const code = source.charCodeAt(index);
231
+ if (code === 13) {
232
+ if (index + 1 < characterOffset && source.charCodeAt(index + 1) === 10) index += 1;
233
+ line += 1;
234
+ lineStart = index + 1;
235
+ } else if (code === 10 || code === 8232 || code === 8233) {
236
+ line += 1;
237
+ lineStart = index + 1;
506
238
  }
507
239
  }
508
- return new RegExp(`^${out}$`);
509
- }
510
- /**
511
- * Build the effective exclusion matcher. A user list REPLACES the
512
- * replaceable defaults (`dist`, `.test.`, `.spec.`) — the HEAD driver
513
- * contract (`options.exclude ?? DEFAULT_EXCLUDE`) — while the structural
514
- * exclusions (`node_modules`, `.next`, `.animus`) always apply: those
515
- * protect pipeline invariants, not preferences, so no user list can
516
- * re-admit them. `extraStructural` joins that never-replaceable set — the
517
- * channel for driver-owned invariants (a CLI outDir inside the root),
518
- * which must NOT ride the user list: appending there would flip the
519
- * replace semantics and silently drop the replaceable defaults.
520
- */
521
- function createExcludeMatcher(userPatterns, extraStructural = []) {
522
- const merged = [...STRUCTURAL_EXCLUDE];
523
- for (const pattern of extraStructural) if (!merged.includes(pattern)) merged.push(pattern);
524
- for (const pattern of userPatterns ?? REPLACEABLE_DEFAULT_EXCLUDE) if (!merged.includes(pattern)) merged.push(pattern);
525
- const compiled = merged.map((pattern) => [pattern, compilePattern(pattern)]);
526
- const hits = new Map(merged.map((p) => [p, 0]));
527
- const explain = (full, rel) => {
528
- for (const [pattern, test] of compiled) if (test(full, rel)) {
529
- hits.set(pattern, (hits.get(pattern) ?? 0) + 1);
530
- return pattern;
531
- }
532
- return null;
533
- };
534
240
  return {
535
- patterns: merged,
536
- matches: (full, rel) => explain(full, rel) !== null,
537
- explain,
538
- stats: () => hits
241
+ line,
242
+ column: characterOffset - lineStart
539
243
  };
540
244
  }
541
- /**
542
- * Resolve the effective mode: the explicit `mode` key wins over every
543
- * environment-derived signal; otherwise the driver's documented default
544
- * applies.
545
- */
546
- function resolveMode(explicit, driverDefault) {
547
- if (explicit === "development" || explicit === "production") return {
548
- mode: explicit,
549
- provenance: "explicit"
550
- };
245
+ function sourceLocation(source, node) {
551
246
  return {
552
- mode: driverDefault(),
553
- provenance: "driver-default"
247
+ start: sourcePosition(source, node.start),
248
+ end: sourcePosition(source, node.end)
554
249
  };
555
250
  }
556
- //#endregion
557
- //#region pipeline/discover-files.ts
558
- /**
559
- * Recursively discover source files under `dir`, skipping excluded paths
560
- * (an `ExcludeMatcher` — see core-options, the one pattern authority — or
561
- * undefined for NO exclusion at all: the dist-package walks deliberately
562
- * run under node_modules, which `createExcludeMatcher`'s structural set
563
- * would prune) and keeping files whose extension is in `extensionsSet`.
564
- *
565
- * Single authoritative copy for both extraction plugins.
566
- */
567
- function discoverFiles(dir, rootDir, exclude, extensionsSet) {
568
- const results = [];
569
- let entries;
570
- try {
571
- entries = readdirSync(dir, { encoding: "utf8" });
572
- } catch {
573
- return results;
574
- }
575
- entries.sort();
576
- const isExcluded = exclude ? (full, rel) => exclude.matches(full, rel) : () => false;
577
- for (const entry of entries) {
578
- const fullPath = join(dir, entry);
579
- if (isExcluded(fullPath, relative(rootDir, fullPath))) continue;
580
- let stat;
581
- try {
582
- stat = statSync(fullPath);
583
- } catch {
584
- continue;
585
- }
586
- if (stat.isDirectory()) results.push(...discoverFiles(fullPath, rootDir, exclude, extensionsSet));
587
- else if (extensionsSet.has(extname(entry))) results.push(fullPath);
251
+ function createsNestedBindingScope(node) {
252
+ return node.type === "BlockStatement" || node.type === "CatchClause" || node.type === "ClassBody" || node.type === "ClassDeclaration" || node.type === "ClassExpression" || node.type === "ForInStatement" || node.type === "ForOfStatement" || node.type === "ForStatement" || node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression" || node.type === "StaticBlock" || node.type === "SwitchStatement" || node.type === "TSModuleBlock";
253
+ }
254
+ function walk(node, visit, nestedScopeDepth = 0) {
255
+ visit(node, nestedScopeDepth);
256
+ const childScopeDepth = nestedScopeDepth + (createsNestedBindingScope(node) ? 1 : 0);
257
+ for (const [key, value] of Object.entries(node)) {
258
+ if (key === "loc" || key === "start" || key === "end") continue;
259
+ if (Array.isArray(value)) {
260
+ for (const item of value) if (isNode(item)) walk(item, visit, childScopeDepth);
261
+ } else if (isNode(value)) walk(value, visit, childScopeDepth);
588
262
  }
589
- return results;
590
263
  }
591
- //#endregion
592
- //#region pipeline/source-identity.ts
593
- /**
594
- * SourceId derivation authority + allowlist membership (openspec:
595
- * external-source-watch-ingestion, design D1/D2/D5).
596
- *
597
- * One shared derivation produces the canonical source identity consumed by
598
- * discovery, watch ingestion, ownership, deletion pruning, and diagnostics
599
- * — raw event paths are lookup INPUTS only, so event spelling (symlink
600
- * alias vs canonical, lexical vs realpath) can never fork identity.
601
- *
602
- * The containment and volume helpers are pure and path-API-injectable so
603
- * Windows semantics are unit-testable via `path.win32` on any host; runtime
604
- * callers use the ambient platform implementation.
605
- */
606
264
  /**
607
- * Structural containment: `target` is the root itself or a descendant of it,
608
- * decided by `relative()` shape — never by string prefixing, so `/ui` can
609
- * never claim `/ui-old`, and a cross-drive win32 target (whose relative()
610
- * result is absolute) is never contained.
265
+ * Svelte `Fragment` containers carry no `start`/`end`, so the `isNode` gate
266
+ * would stop at each one: descend plain objects, visit span-carrying nodes.
611
267
  */
612
- function isPathWithinRoot(root, target, pathApi = path) {
613
- const rel = pathApi.relative(root, target);
614
- return rel === "" || !pathApi.isAbsolute(rel) && rel !== ".." && !rel.startsWith(".." + pathApi.sep);
268
+ function walkFragment(value, visit) {
269
+ const seen = /* @__PURE__ */ new WeakSet();
270
+ const descend = (current) => {
271
+ if (Array.isArray(current)) {
272
+ for (const item of current) descend(item);
273
+ return;
274
+ }
275
+ if (!isCompilerRecord(current)) return;
276
+ if (seen.has(current)) return;
277
+ seen.add(current);
278
+ if (isNode(current)) visit(current);
279
+ for (const [key, child] of Object.entries(current)) {
280
+ if (key === "loc" || key === "start" || key === "end" || key === "metadata" || key === "parent") continue;
281
+ descend(child);
282
+ }
283
+ };
284
+ descend(value);
615
285
  }
616
- /**
617
- * Cross-volume gate (design D5): two paths share a platform path root
618
- * (win32 drive letter or UNC share; always true on posix). Compared
619
- * case-insensitively — win32 drive letters are case-insensitive.
620
- */
621
- function sharesVolumeRoot(a, b, pathApi = path) {
622
- const volumeOf = (p) => pathApi.parse(pathApi.resolve(p)).root.toLowerCase();
623
- return volumeOf(a) === volumeOf(b);
286
+ function importBindings(program) {
287
+ const bindings = /* @__PURE__ */ new Map();
288
+ for (const declaration of program.body) {
289
+ if (declaration.type !== "ImportDeclaration" || declaration.importKind === "type" || !Array.isArray(declaration.specifiers)) continue;
290
+ for (const value of declaration.specifiers) {
291
+ if (!isNode(value) || value.importKind === "type") continue;
292
+ const local = childNode(value.local);
293
+ const localName = nodeName(local);
294
+ if (!local || !localName) continue;
295
+ const sourceNode = childNode(declaration.source);
296
+ const importSource = sourceNode && isCompilerString(sourceNode.value) ? sourceNode.value : null;
297
+ if (!importSource) continue;
298
+ const namedImported = importedName(value);
299
+ if (value.type === "ImportSpecifier" && namedImported === null) continue;
300
+ const importedNode = childNode(value.imported);
301
+ const kind = value.type === "ImportSpecifier" ? importedNode?.type === "Literal" ? "string-named" : namedImported === "default" ? "named-default" : "named" : value.type === "ImportDefaultSpecifier" ? "default" : value.type === "ImportNamespaceSpecifier" ? "namespace" : null;
302
+ if (kind) bindings.set(localName, {
303
+ kind,
304
+ source: importSource,
305
+ imported: kind === "default" || kind === "named-default" ? "default" : kind === "namespace" ? "*" : namedImported,
306
+ declaration,
307
+ specifier: value,
308
+ local
309
+ });
310
+ }
311
+ }
312
+ return bindings;
624
313
  }
625
- function canonicalize(p) {
626
- try {
627
- return realpathSync$1(p);
628
- } catch {
629
- return null;
314
+ function diagnostic(code, message, originalPath, source, node, location) {
315
+ const result = {
316
+ code,
317
+ message,
318
+ originalPath
319
+ };
320
+ if (node) {
321
+ result.span = byteSpan(source, node);
322
+ result.location = location ?? sourceLocation(source, node);
630
323
  }
324
+ return result;
631
325
  }
632
- function createSourceIdentity(rootDir) {
633
- const normalizedRootDir = path.normalize(rootDir);
634
- const canonicalRootDir = canonicalize(normalizedRootDir) ?? normalizedRootDir;
635
- const roots = [];
636
- const aliases = /* @__PURE__ */ new Map();
637
- const owningRecord = (canonicalPath) => {
638
- let owner = null;
639
- for (const record of roots) {
640
- if (!isPathWithinRoot(record.canonical, canonicalPath)) continue;
641
- if (!owner || record.canonical.length > owner.canonical.length) owner = record;
326
+ function parseCompilerError(value) {
327
+ if (!isCompilerRecord(value)) return null;
328
+ const parsed = { positions: Array.isArray(value.position) ? value.position : [] };
329
+ if (isCompilerRecord(value.start)) parsed.start = value.start;
330
+ if (isCompilerRecord(value.end)) parsed.end = value.end;
331
+ return parsed;
332
+ }
333
+ function compilerParseErrorRange(error, source) {
334
+ const candidate = parseCompilerError(error);
335
+ if (candidate === null) return null;
336
+ const positions = candidate.positions;
337
+ const startCharacter = isCompilerNumber(candidate.start?.character) ? candidate.start.character : isCompilerNumber(positions[0]) ? positions[0] : null;
338
+ const endCharacter = isCompilerNumber(candidate.end?.character) ? candidate.end.character : isCompilerNumber(positions[1]) ? positions[1] : startCharacter;
339
+ if (startCharacter === null || endCharacter === null || startCharacter < 0 || endCharacter < startCharacter || endCharacter > source.length) return null;
340
+ const node = {
341
+ type: "SvelteParseError",
342
+ start: startCharacter,
343
+ end: endCharacter
344
+ };
345
+ const fallback = sourceLocation(source, node);
346
+ const point = (value, fallbackPoint) => ({
347
+ line: isCompilerNumber(value?.line) ? value.line : fallbackPoint.line,
348
+ column: isCompilerNumber(value?.column) ? value.column : fallbackPoint.column
349
+ });
350
+ return {
351
+ node,
352
+ location: {
353
+ start: point(candidate.start, fallback.start),
354
+ end: point(candidate.end, fallback.end)
642
355
  }
643
- return owner;
644
356
  };
357
+ }
358
+ function calleeParts(node) {
359
+ if (node.type !== "CallExpression") return null;
360
+ const callee = childNode(node.callee);
361
+ if (!callee || callee.type !== "MemberExpression") return null;
362
+ const object = childNode(callee.object);
363
+ const property = childNode(callee.property);
364
+ if (!object || !property) return null;
365
+ if ((nodeName(property) ?? (property.type === "Literal" && property.value === "attrs" ? "attrs" : null)) !== "attrs") return null;
645
366
  return {
646
- rootDir: normalizedRootDir,
647
- canonicalRootDir,
648
- registerExternalRoot(root) {
649
- const asRegistered = path.normalize(root);
650
- const canonical = canonicalize(asRegistered) ?? asRegistered;
651
- const existing = roots.find((record) => record.canonical === canonical);
652
- if (existing) return existing.canonical;
653
- roots.push({
654
- asRegistered,
655
- canonical
656
- });
657
- return canonical;
658
- },
659
- externalRoots() {
660
- return roots.map((record) => record.canonical);
661
- },
662
- resolveSourceId(inputPath) {
663
- const lexical = path.normalize(inputPath);
664
- const canonical = canonicalize(lexical);
665
- if (canonical === null) return null;
666
- const owner = owningRecord(canonical);
667
- const ownedBy = (record) => {
668
- const pathInRoot = path.relative(record.canonical, canonical);
669
- return {
670
- sourceKey: path.relative(normalizedRootDir, path.join(record.asRegistered, pathInRoot)),
671
- owningRoot: record.canonical,
672
- pathInRoot
673
- };
674
- };
675
- const ownerIsMoreSpecific = owner !== null && owner.canonical.length > canonicalRootDir.length;
676
- let resolved;
677
- if (!ownerIsMoreSpecific && isPathWithinRoot(canonicalRootDir, canonical)) {
678
- const key = path.relative(canonicalRootDir, canonical);
679
- resolved = {
680
- sourceKey: key,
681
- owningRoot: null,
682
- pathInRoot: key
683
- };
684
- } else if (owner) resolved = ownedBy(owner);
685
- else return null;
686
- aliases.set(lexical, resolved);
687
- aliases.set(canonical, resolved);
688
- return resolved;
689
- },
690
- resolveDeletedSourceId(inputPath) {
691
- return aliases.get(path.normalize(inputPath)) ?? null;
692
- },
693
- containingExternalRoot(inputPath) {
694
- const lexical = path.normalize(inputPath);
695
- let owner = null;
696
- for (const record of roots) {
697
- if (!(isPathWithinRoot(record.canonical, lexical) || isPathWithinRoot(record.asRegistered, lexical))) continue;
698
- if (!owner || record.canonical.length > owner.canonical.length) owner = record;
367
+ object,
368
+ property,
369
+ computed: callee.computed === true,
370
+ optional: callee.optional === true || node.optional === true
371
+ };
372
+ }
373
+ function resolverImportAccess(receiver, bindings) {
374
+ const directName = nodeName(receiver);
375
+ if (directName) {
376
+ const binding = bindings.get(directName);
377
+ if (!binding) return null;
378
+ return {
379
+ binding,
380
+ resolver: receiver,
381
+ displayName: directName,
382
+ request: {
383
+ source: binding.source,
384
+ imported: binding.imported,
385
+ local: directName,
386
+ access: {
387
+ kind: "direct",
388
+ importKind: binding.kind
389
+ }
699
390
  }
700
- if (owner) return owner.canonical;
701
- const canonical = canonicalize(lexical);
702
- if (canonical === null) return null;
703
- for (const record of roots) {
704
- if (!isPathWithinRoot(record.canonical, canonical)) continue;
705
- if (!owner || record.canonical.length > owner.canonical.length) owner = record;
391
+ };
392
+ }
393
+ if (receiver.type !== "MemberExpression" || receiver.computed === true || receiver.optional === true) return null;
394
+ const namespaceNode = childNode(receiver.object);
395
+ const memberNode = childNode(receiver.property);
396
+ const namespaceName = nodeName(namespaceNode);
397
+ const memberName = nodeName(memberNode) ?? (memberNode?.type === "Literal" && isCompilerString(memberNode.value) ? memberNode.value : null);
398
+ if (!namespaceNode || !namespaceName || !memberName) return null;
399
+ const binding = bindings.get(namespaceName);
400
+ if (!binding || binding.kind !== "namespace") return null;
401
+ return {
402
+ binding,
403
+ resolver: receiver,
404
+ displayName: `${namespaceName}.${memberName}`,
405
+ request: {
406
+ source: binding.source,
407
+ imported: memberName,
408
+ local: namespaceName,
409
+ access: {
410
+ kind: "namespace-member",
411
+ importKind: "namespace"
706
412
  }
707
- return owner?.canonical ?? null;
708
413
  }
709
414
  };
710
415
  }
711
- //#endregion
712
- //#region pipeline/discover-packages.ts
713
- /**
714
- * Walk up from a resolved package entry file to the nearest directory
715
- * containing a package.json (the package root). Stops at the filesystem
716
- * root and returns it if no package.json is found on the way up.
717
- */
718
- function findPackageRoot(absEntryPath) {
719
- let pkgRoot = dirname(absEntryPath);
720
- while (pkgRoot !== dirname(pkgRoot) && !existsSync(join(pkgRoot, "package.json"))) pkgRoot = dirname(pkgRoot);
721
- return pkgRoot;
416
+ function isSupportedResolverAccess(access) {
417
+ return access.binding.kind === "named" && access.request.access.kind === "direct";
722
418
  }
723
- /**
724
- * Exclusion fragments for discovery inside an external package's src/,
725
- * matched against paths RELATIVE TO the src dir. Per spec
726
- * external-package-file-discovery, the filters apply "within the package" —
727
- * an npm-installed package whose own location is under node_modules (or a
728
- * repo path containing 'dist') must still have its sources discovered.
729
- */
730
- const PACKAGE_SRC_EXCLUDES = [
731
- "dist",
732
- "node_modules",
733
- ".test.",
734
- ".spec."
735
- ];
736
- /** True when a PACKAGE-RELATIVE path matches a package-source exclusion
737
- * fragment (the "within the package" filter — see PACKAGE_SRC_EXCLUDES). */
738
- function isExcludedPackageRelativePath(rel) {
739
- return PACKAGE_SRC_EXCLUDES.some((pattern) => rel.includes(pattern));
419
+ function resolverCallSite(node, bindings) {
420
+ if (node.type !== "CallExpression") return null;
421
+ const parts = calleeParts(node);
422
+ if (parts) {
423
+ const access = resolverImportAccess(parts.object, bindings);
424
+ if (!access) return null;
425
+ return {
426
+ access,
427
+ form: "attrs",
428
+ computed: parts.computed,
429
+ optional: parts.optional
430
+ };
431
+ }
432
+ const callee = childNode(node.callee);
433
+ if (!callee) return null;
434
+ const access = resolverImportAccess(callee, bindings);
435
+ if (!access) return null;
436
+ return {
437
+ access,
438
+ form: "callable",
439
+ computed: callee.type === "MemberExpression" && callee.computed === true,
440
+ optional: node.optional === true || callee.type === "MemberExpression" && callee.optional === true
441
+ };
740
442
  }
741
- /**
742
- * THE package-source walk (guardrail G1 — one policy): shared discovery with
743
- * no consumer patterns, then exclusion by package-relative path so fragments
744
- * in the package's own location can't blank out its sources. Used by the
745
- * collector, hosts' dirty-root rewalks, and watch-path classification alike.
746
- */
747
- function walkPackageSources(packageDir, extensionsSet) {
748
- return discoverFiles(packageDir, packageDir, void 0, extensionsSet).filter((absPath) => !isExcludedPackageRelativePath(relative(packageDir, absPath)));
443
+ function argumentSpan(argumentsList, fallback) {
444
+ if (argumentsList.length === 0) return fallback;
445
+ return {
446
+ type: "ArgumentRange",
447
+ start: argumentsList[0].start,
448
+ end: argumentsList[argumentsList.length - 1].end
449
+ };
749
450
  }
750
- /** Exclusions for the compiled-output fallback when a package does not ship src/. */
751
- const PACKAGE_OUTPUT_EXCLUDES = [
752
- "node_modules",
753
- ".test.",
754
- ".spec.",
755
- ".d.ts",
756
- ".map"
757
- ];
758
- const isFile = (path) => {
759
- try {
760
- return statSync(path).isFile();
761
- } catch {
762
- return false;
763
- }
764
- };
765
- /**
766
- * Resolve an absolute-path specifier (a relative `includes` entry already
767
- * resolved against the system file) without a bundler: the path itself when it
768
- * is a file, otherwise the extension and directory-index candidates built from
769
- * the caller's extension set. Used only as a fallback — a bundler resolver that
770
- * answers first still wins, since it also knows aliases and `.js`→`.ts` mapping.
771
- */
772
- function resolveAbsolutePathSpecifier(absSpecifier, extensionsSet) {
773
- return [
774
- absSpecifier,
775
- ...Array.from(extensionsSet, (ext) => absSpecifier + ext),
776
- ...Array.from(extensionsSet, (ext) => join(absSpecifier, `index${ext}`))
777
- ].find(isFile) ?? null;
451
+ function propertyName(node) {
452
+ const key = childNode(node.key);
453
+ if (!key) return null;
454
+ if (key.type === "Identifier" && isCompilerString(key.name)) return key.name;
455
+ if (key.type === "Literal" && isCompilerString(key.value)) return key.value;
456
+ return null;
778
457
  }
779
- /** The package-name half of a bare specifier (`@scope/name/sub` → `@scope/name`). */
780
- function bareSpecifierPackageName(specifier) {
781
- const segments = specifier.split("/");
782
- return specifier.startsWith("@") ? segments.slice(0, 2).join("/") : segments[0];
458
+ function isJsxAttributeName(name) {
459
+ return /^[A-Za-z_$][A-Za-z0-9_$-]*$/.test(name);
783
460
  }
784
- function sourceEntryForSpecifier(specifier, srcDir, extensionsSet) {
785
- if (isAbsolute(specifier)) {
786
- const resolved = resolveAbsolutePathSpecifier(specifier, extensionsSet);
787
- if (!resolved) return null;
788
- return isPathWithinRoot(srcDir, resolved) ? resolved : null;
789
- }
790
- const packageName = bareSpecifierPackageName(specifier);
791
- return resolveAbsolutePathSpecifier(join(srcDir, specifier.slice(packageName.length + 1) || "index"), extensionsSet);
461
+ function tsxIncompatibleValue(value) {
462
+ let incompatible = null;
463
+ walk(value, (node) => {
464
+ if (incompatible) return;
465
+ if (node.type === "TSTypeAssertion" || node.type === "ArrowFunctionExpression" && childNode(node.typeParameters) !== null) incompatible = node;
466
+ });
467
+ return incompatible;
792
468
  }
793
- /**
794
- * The D13 dist-freshness check: applicable only when a specifier has BOTH a
795
- * walked src/ tree and a resolved dist entry that exists on disk outside that
796
- * tree; stale when the dist entry's mtime is older than the newest walked
797
- * source file's. Not applicable (never stale) when the entry resolves inside
798
- * src/ (no dist to skew), when the entry file is missing, or when no source
799
- * file's mtime is readable. Detection only — reporting is the caller's policy.
800
- */
801
- function distEntryIsStale(absEntry, srcDir, srcFiles) {
802
- if (isPathWithinRoot(srcDir, absEntry)) return false;
803
- let distMtime;
804
- try {
805
- distMtime = statSync(absEntry).mtimeMs;
806
- } catch {
807
- return false;
469
+ function readProperties(argument, source, originalPath) {
470
+ if (!Array.isArray(argument.properties)) return diagnostic("SVELTE_ATTRS_ARGUMENT_UNRESOLVED", "Resolver .attrs() requires an object literal with statically named properties.", originalPath, source, argument);
471
+ const properties = [];
472
+ for (const value of argument.properties) {
473
+ if (!isNode(value)) continue;
474
+ if (value.type === "SpreadElement") return diagnostic("SVELTE_ATTRS_SPREAD_UNRESOLVED", "Resolver .attrs() object spreads are unresolved; enumerate properties explicitly.", originalPath, source, value);
475
+ if (value.type !== "Property") return diagnostic("SVELTE_ATTRS_PROPERTY_UNSUPPORTED", "Resolver .attrs() accepts ordinary object properties only.", originalPath, source, value);
476
+ if (value.computed === true) return diagnostic("SVELTE_ATTRS_COMPUTED_KEY", "Resolver .attrs() computed property names cannot be attributed safely.", originalPath, source, value);
477
+ if (value.method === true || value.kind !== "init") return diagnostic("SVELTE_ATTRS_PROPERTY_UNSUPPORTED", "Resolver .attrs() methods and accessors cannot be projected as attributes.", originalPath, source, value);
478
+ const key = childNode(value.key);
479
+ const propertyValue = childNode(value.value);
480
+ const name = propertyName(value);
481
+ if (!key || !propertyValue || !name || !isJsxAttributeName(name)) return diagnostic("SVELTE_ATTRS_PROPERTY_UNSUPPORTED", "Resolver .attrs() property names must be JSX-compatible identifier or string names.", originalPath, source, value);
482
+ const incompatibleValue = tsxIncompatibleValue(propertyValue);
483
+ if (incompatibleValue) return diagnostic("SVELTE_ATTRS_VALUE_UNSUPPORTED", "Resolver .attrs() values using angle-bracket type assertions or generic arrow parameters cannot be projected into TSX safely.", originalPath, source, incompatibleValue);
484
+ properties.push({
485
+ name,
486
+ property: value,
487
+ key,
488
+ value: propertyValue
489
+ });
808
490
  }
809
- let newestSrcMtime = -Infinity;
810
- for (const srcFile of srcFiles) try {
811
- newestSrcMtime = Math.max(newestSrcMtime, statSync(srcFile).mtimeMs);
812
- } catch {}
813
- return distMtime < newestSrcMtime;
491
+ return properties;
814
492
  }
815
- /**
816
- * Shared external-package source collection (spec:
817
- * external-package-file-discovery), consumed by both extraction plugins.
818
- * For each specifier: resolve to an absolute entry (null/throw → silently
819
- * skip; an absolute-path specifier falls back to a filesystem probe), walk up
820
- * to the package root, then either discover sources under src/ (redirecting
821
- * module resolution to src/index.ts when present) or fall back to ingesting
822
- * the resolved entry file itself.
823
- *
824
- * The only bundler-specific seams are callbacks: specifier resolution,
825
- * dedup against already-ingested files, per-file preprocessing (MDX), and
826
- * the unreadable-file warning. Hashing and caching stay in the plugins —
827
- * their cache policies legitimately differ.
828
- *
829
- * Alongside the aggregates, one `ExternalPackageOutcome` per declared
830
- * specifier records what that specifier produced. Collection never reports on
831
- * them — any warning or gate is the caller's policy.
832
- */
833
- async function collectExternalPackageSources(opts) {
834
- const { specifiers, resolveSpecifier, rootDir, extensionsSet, hasEntry, onSourceRead, onUnreadable, onPackageResolved } = opts;
835
- const entries = [];
836
- const pushed = /* @__PURE__ */ new Set();
837
- const packageMap = {};
838
- const sourceEntries = /* @__PURE__ */ new Map();
839
- const keyframesScanEntries = /* @__PURE__ */ new Map();
840
- const packageDirs = [];
841
- const dirOwnerSets = {};
842
- const dirExtensions = {};
843
- const fileOwners = {};
844
- const outcomes = [];
845
- const claimDir = (dir, specifier) => {
846
- (dirOwnerSets[dir] ??= []).push(specifier);
847
- };
848
- const alreadyIngested = (relPath) => hasEntry(relPath) || pushed.has(relPath);
849
- for (const specifier of specifiers) {
850
- let absEntry;
851
- try {
852
- absEntry = await resolveSpecifier(specifier);
853
- } catch {
854
- absEntry = null;
855
- }
856
- if (!absEntry && isAbsolute(specifier)) absEntry = resolveAbsolutePathSpecifier(specifier, extensionsSet);
857
- if (!absEntry) {
858
- outcomes.push({
859
- specifier,
860
- outcome: "unresolvable",
861
- fileCount: 0
862
- });
863
- continue;
493
+ var VirtualSourceBuilder = class {
494
+ originalSource;
495
+ chunks = [];
496
+ bytes = 0;
497
+ mappings = [];
498
+ constructor(originalSource) {
499
+ this.originalSource = originalSource;
500
+ }
501
+ append(text) {
502
+ this.chunks.push(text);
503
+ this.bytes += Buffer.byteLength(text);
504
+ }
505
+ appendMapped(text, kind, original) {
506
+ const start = this.bytes;
507
+ this.append(text);
508
+ this.mappings.push({
509
+ kind,
510
+ generated: {
511
+ start,
512
+ end: this.bytes
513
+ },
514
+ original: byteSpan(this.originalSource, original)
515
+ });
516
+ }
517
+ toString() {
518
+ return this.chunks.join("");
519
+ }
520
+ };
521
+ function importedName(specifier) {
522
+ const imported = childNode(specifier.imported);
523
+ if (!imported) return null;
524
+ if (imported.type === "Identifier" && isCompilerString(imported.name)) return imported.name;
525
+ if (imported.type === "Literal" && isCompilerString(imported.value)) return imported.value;
526
+ return null;
527
+ }
528
+ function renderImport(declaration, witnessedBindings, source) {
529
+ if (!Array.isArray(declaration.specifiers)) return null;
530
+ const names = [];
531
+ const locals = [];
532
+ for (const value of declaration.specifiers) {
533
+ if (!isNode(value) || value.type !== "ImportSpecifier") continue;
534
+ const localName = nodeName(childNode(value.local));
535
+ if (!localName || !witnessedBindings.has(localName)) continue;
536
+ names.push(source.slice(value.start, value.end));
537
+ locals.push(localName);
538
+ }
539
+ if (names.length === 0) return null;
540
+ const sourceNode = childNode(declaration.source);
541
+ const sourceText = sourceNode && isCompilerString(sourceNode.raw) ? sourceNode.raw : sourceNode && isCompilerString(sourceNode.value) ? JSON.stringify(sourceNode.value) : null;
542
+ return sourceText ? {
543
+ text: `import { ${names.join(", ")} } from ${sourceText};\n`,
544
+ locals
545
+ } : null;
546
+ }
547
+ /** Memoizes success only: a missing optional peer retries per file, so
548
+ * installing `svelte` mid-session recovers without a restart. */
549
+ let compilerMemo = null;
550
+ async function loadCompiler() {
551
+ if (compilerMemo) return compilerMemo;
552
+ compilerMemo = await import("svelte/compiler").catch(() => null);
553
+ return compilerMemo;
554
+ }
555
+ function projectScope(script, scope, source, originalPath, options, inherited, precomputedOwnBindings) {
556
+ const ownBindings = precomputedOwnBindings ?? importBindings(script.content);
557
+ const bindings = inherited ? new Map([...inherited.bindings, ...ownBindings]) : ownBindings;
558
+ const witnesses = [];
559
+ const diagnostics = [];
560
+ if (bindings.size > 0) walk(script.content, (node, nestedScopeDepth) => {
561
+ const site = resolverCallSite(node, bindings);
562
+ if (!site) return;
563
+ const { access, form } = site;
564
+ const attribution = options.attributeResolver(access.request);
565
+ if (attribution === "other") return;
566
+ const resolverName = access.displayName;
567
+ const callDisplay = form === "attrs" ? `${resolverName}.attrs()` : `${resolverName}()`;
568
+ if (attribution === "unsupported-resolver-form" || !isSupportedResolverAccess(access)) {
569
+ diagnostics.push(diagnostic("SVELTE_ATTRS_IMPORT_UNSUPPORTED", `Resolver '${resolverName}' uses an import/access form that the extraction engine cannot attribute to its exported .asClass() binding.`, originalPath, source, access.resolver));
570
+ return;
864
571
  }
865
- const srcDir = join(findPackageRoot(absEntry), "src");
866
- let fileCount = 0;
867
- let staleDist = false;
868
- if (existsSync(srcDir)) {
869
- packageDirs.push(srcDir);
870
- claimDir(srcDir, specifier);
871
- dirExtensions[srcDir] = [...extensionsSet];
872
- onPackageResolved?.(specifier, srcDir);
873
- const srcEntry = sourceEntryForSpecifier(specifier, srcDir, extensionsSet);
874
- if (srcEntry) {
875
- packageMap[specifier] = relative(rootDir, srcEntry);
876
- sourceEntries.set(specifier, srcEntry);
877
- } else packageMap[specifier] = relative(rootDir, absEntry);
878
- keyframesScanEntries.set(specifier, srcEntry ?? absEntry);
879
- if (!isAbsolute(specifier)) {
880
- const packageName = bareSpecifierPackageName(specifier);
881
- if (packageName !== specifier && !(packageName in packageMap)) {
882
- const rootEntry = sourceEntryForSpecifier(packageName, srcDir, extensionsSet);
883
- if (rootEntry) {
884
- packageMap[packageName] = relative(rootDir, rootEntry);
885
- sourceEntries.set(packageName, rootEntry);
886
- keyframesScanEntries.set(packageName, rootEntry);
887
- }
888
- }
889
- }
890
- const pkgFiles = walkPackageSources(srcDir, extensionsSet);
891
- staleDist = distEntryIsStale(absEntry, srcDir, pkgFiles);
892
- for (const pkgFile of pkgFiles) {
893
- const relPath = relative(rootDir, pkgFile);
894
- if (alreadyIngested(relPath)) {
895
- fileCount++;
896
- continue;
897
- }
898
- let source;
899
- try {
900
- source = readFileSync(pkgFile, "utf-8");
901
- } catch (err) {
902
- onUnreadable(relPath, err);
903
- continue;
904
- }
905
- onSourceRead?.(source, relPath, pkgFile);
906
- entries.push({
907
- path: relPath,
908
- source
909
- });
910
- pushed.add(relPath);
911
- fileOwners[relPath] ??= specifier;
912
- fileCount++;
913
- }
914
- } else {
915
- const outputDir = dirname(absEntry);
916
- packageDirs.push(outputDir);
917
- claimDir(outputDir, specifier);
918
- onPackageResolved?.(specifier, outputDir);
919
- packageMap[specifier] = relative(rootDir, absEntry);
920
- keyframesScanEntries.set(specifier, absEntry);
921
- const outputExtensions = new Set(extensionsSet);
922
- outputExtensions.add(extname(absEntry));
923
- dirExtensions[outputDir] = [...outputExtensions];
924
- const outputFiles = discoverFiles(outputDir, outputDir, void 0, outputExtensions).filter((file) => {
925
- const relToOutput = relative(outputDir, file);
926
- return !PACKAGE_OUTPUT_EXCLUDES.some((pattern) => relToOutput.includes(pattern));
572
+ if (nestedScopeDepth > 0) {
573
+ diagnostics.push(diagnostic("SVELTE_ATTRS_SCOPE_UNSUPPORTED", `Resolver '${callDisplay}' is inside a nested binding scope; move the call to the top-level script scope so its import identity is unambiguous.`, originalPath, source, access.resolver));
574
+ return;
575
+ }
576
+ if (site.computed || site.optional) {
577
+ diagnostics.push(diagnostic("SVELTE_ATTRS_CALLEE_UNSUPPORTED", `Resolver '${resolverName}' must be called directly as ${resolverName}(...) or ${resolverName}.attrs(...).`, originalPath, source, node));
578
+ return;
579
+ }
580
+ const argumentsList = Array.isArray(node.arguments) ? node.arguments.filter(isNode) : [];
581
+ if (argumentsList.length === 0) {
582
+ witnesses.push({
583
+ binding: access.binding,
584
+ resolver: access.resolver,
585
+ properties: []
927
586
  });
928
- if (!outputFiles.includes(absEntry)) outputFiles.unshift(absEntry);
929
- for (const outputFile of outputFiles) {
930
- const outputRelPath = relative(rootDir, outputFile);
931
- if (alreadyIngested(outputRelPath)) {
932
- fileCount++;
933
- continue;
934
- }
935
- let source;
936
- try {
937
- source = readFileSync(outputFile, "utf-8");
938
- } catch (err) {
939
- onUnreadable(outputRelPath, err);
940
- continue;
941
- }
942
- onSourceRead?.(source, outputRelPath, outputFile);
943
- entries.push({
944
- path: outputRelPath,
945
- source
946
- });
947
- pushed.add(outputRelPath);
948
- fileOwners[outputRelPath] ??= specifier;
949
- fileCount++;
950
- }
587
+ return;
951
588
  }
952
- outcomes.push({
953
- specifier,
954
- outcome: staleDist ? "stale-dist" : fileCount > 0 ? "resolved" : "empty",
955
- fileCount
589
+ if (argumentsList.length !== 1 || argumentsList[0].type !== "ObjectExpression") {
590
+ diagnostics.push(diagnostic("SVELTE_ATTRS_ARGUMENT_UNRESOLVED", `Resolver '${callDisplay}' accepts no argument or one object literal in Svelte projection.`, originalPath, source, argumentSpan(argumentsList, node)));
591
+ return;
592
+ }
593
+ const properties = readProperties(argumentsList[0], source, originalPath);
594
+ if (!Array.isArray(properties)) {
595
+ diagnostics.push(properties);
596
+ return;
597
+ }
598
+ witnesses.push({
599
+ binding: access.binding,
600
+ resolver: access.resolver,
601
+ properties
956
602
  });
603
+ });
604
+ if (diagnostics.length > 0 || witnesses.length === 0) return { diagnostics };
605
+ const witnessedBindings = new Set(witnesses.map((witness) => nodeName(witness.binding.local)));
606
+ const builder = new VirtualSourceBuilder(source);
607
+ const pendingNames = new Set(witnessedBindings);
608
+ const renderDeclarations = (declarations) => {
609
+ for (const declaration of declarations) {
610
+ if (declaration.type !== "ImportDeclaration") continue;
611
+ const rendered = renderImport(declaration, pendingNames, source);
612
+ if (!rendered) continue;
613
+ builder.append(rendered.text);
614
+ for (const local of rendered.locals) pendingNames.delete(local);
615
+ }
616
+ };
617
+ renderDeclarations(script.content.body);
618
+ if (inherited) renderDeclarations(inherited.importDeclarations);
619
+ for (const witness of witnesses) {
620
+ const resolverName = nodeName(witness.resolver);
621
+ builder.append("<");
622
+ builder.appendMapped(resolverName, "resolver", witness.resolver);
623
+ for (const property of witness.properties) {
624
+ builder.append(" ");
625
+ builder.appendMapped(property.name, "attribute", property.key);
626
+ builder.append("={");
627
+ builder.appendMapped(source.slice(property.value.start, property.value.end), "value", property.value);
628
+ builder.append("}");
629
+ }
630
+ builder.append(" />;\n");
957
631
  }
958
632
  return {
959
- entries,
960
- packageMap,
961
- sourceEntries,
962
- keyframesScanEntries,
963
- packageDirs,
964
- dirOwnerSets,
965
- dirExtensions,
966
- fileOwners,
967
- outcomes
633
+ diagnostics: [],
634
+ entry: {
635
+ scope,
636
+ path: `${originalPath}.${scope}.tsx`,
637
+ source: builder.toString(),
638
+ mappings: builder.mappings
639
+ }
968
640
  };
969
641
  }
970
- /** First-declared specifier per package dir — the single-value ownership
971
- * view correlation consumers key on, derived from `dirOwnerSets`. */
972
- function firstOwners(dirOwnerSets) {
973
- const owners = {};
974
- for (const [dir, specifiers] of Object.entries(dirOwnerSets)) if (specifiers.length > 0) owners[dir] = specifiers[0];
975
- return owners;
976
- }
977
642
  /**
978
- * Excise a set of rejected specifiers from a collection result ATOMICALLY
979
- * (openspec: external-source-watch-ingestion, design D5): the returned copy
980
- * carries no entries, package-map targets, source entries, package dirs, or
981
- * ownership for the rejected packages — a package dir is removed only when
982
- * EVERY specifier that claimed it is rejected (a dir-level rejection, such
983
- * as the cross-volume gate, always rejects them together). Outcome records
984
- * are reporting inputs, not membership, and pass through untouched.
643
+ * Project Svelte script scopes into parser-only TSX witnesses from SOURCE,
644
+ * never compiler output. Unsupported call shapes fail the whole source closed.
985
645
  */
986
- function excludeCollectedPackages(collected, rejectedSpecifiers, rootDir) {
987
- if (rejectedSpecifiers.size === 0) return collected;
988
- const rejectedDirs = Object.entries(collected.dirOwnerSets).filter(([, specs]) => specs.every((s) => rejectedSpecifiers.has(s))).map(([dir]) => dir);
989
- const underRejectedDir = (absPath) => rejectedDirs.some((dir) => isPathWithinRoot(dir, absPath));
990
- const targetRejected = (specifier, absTarget) => rejectedSpecifiers.has(specifier) || underRejectedDir(absTarget);
991
- const packageMap = {};
992
- for (const [specifier, relTarget] of Object.entries(collected.packageMap)) {
993
- if (targetRejected(specifier, resolve(rootDir, relTarget))) continue;
994
- packageMap[specifier] = relTarget;
995
- }
996
- const sourceEntries = /* @__PURE__ */ new Map();
997
- for (const [specifier, absEntry] of collected.sourceEntries) {
998
- if (targetRejected(specifier, absEntry)) continue;
999
- sourceEntries.set(specifier, absEntry);
1000
- }
1001
- const keyframesScanEntries = /* @__PURE__ */ new Map();
1002
- for (const [specifier, absEntry] of collected.keyframesScanEntries) {
1003
- if (targetRejected(specifier, absEntry)) continue;
1004
- keyframesScanEntries.set(specifier, absEntry);
1005
- }
1006
- const dirOwnerSets = {};
1007
- for (const [dir, specs] of Object.entries(collected.dirOwnerSets)) {
1008
- const kept = specs.filter((s) => !rejectedSpecifiers.has(s));
1009
- if (kept.length === 0) continue;
1010
- dirOwnerSets[dir] = kept;
1011
- }
1012
- const dirExtensions = {};
1013
- for (const [dir, exts] of Object.entries(collected.dirExtensions)) {
1014
- if (rejectedDirs.includes(dir)) continue;
1015
- dirExtensions[dir] = exts;
646
+ async function adaptSvelteSource(source, originalPath, options) {
647
+ const original = {
648
+ path: originalPath,
649
+ hash: contentHash(source)
650
+ };
651
+ const compiler = await loadCompiler();
652
+ if (!compiler) return {
653
+ kind: "missing-dep",
654
+ original,
655
+ dependency: "svelte/compiler"
656
+ };
657
+ let compilerAst;
658
+ try {
659
+ compilerAst = compiler.parse(source, {
660
+ filename: originalPath,
661
+ modern: true
662
+ });
663
+ } catch (error) {
664
+ const range = compilerParseErrorRange(error, source);
665
+ return {
666
+ kind: "error",
667
+ original,
668
+ diagnostics: [diagnostic("SVELTE_PARSE_ERROR", error instanceof Error ? error.message : String(error), originalPath, source, range?.node, range?.location)]
669
+ };
1016
670
  }
1017
- const fileOwners = {};
1018
- for (const [relPath, owner] of Object.entries(collected.fileOwners)) {
1019
- if (rejectedSpecifiers.has(owner)) continue;
1020
- fileOwners[relPath] = owner;
671
+ const ast = parseSvelteAst(compilerAst);
672
+ const projections = [];
673
+ const moduleBindings = ast.module ? importBindings(ast.module.content) : void 0;
674
+ const instanceBindings = ast.instance ? importBindings(ast.instance.content) : void 0;
675
+ const moduleScope = ast.module && moduleBindings ? {
676
+ bindings: moduleBindings,
677
+ importDeclarations: ast.module.content.body.filter((declaration) => declaration.type === "ImportDeclaration")
678
+ } : void 0;
679
+ if (ast.module) projections.push(projectScope(ast.module, "module", source, originalPath, options, void 0, moduleBindings));
680
+ if (ast.instance) projections.push(projectScope(ast.instance, "instance", source, originalPath, options, moduleScope, instanceBindings));
681
+ const diagnostics = projections.flatMap((projection) => projection.diagnostics);
682
+ if (ast.fragment) {
683
+ const scriptBindings = instanceBindings ?? /* @__PURE__ */ new Map();
684
+ const fragmentBindings = moduleScope ? new Map([...moduleScope.bindings, ...scriptBindings]) : scriptBindings;
685
+ if (fragmentBindings.size > 0) walkFragment(ast.fragment, (node) => {
686
+ const site = resolverCallSite(node, fragmentBindings);
687
+ if (!site) return;
688
+ if (options.attributeResolver(site.access.request) === "other") return;
689
+ diagnostics.push(diagnostic("SVELTE_ATTRS_TEMPLATE_UNSUPPORTED", `Resolver '${site.access.displayName}' is called inside the template; assign the result to a const in the instance script and spread that instead.`, originalPath, source, node));
690
+ });
1021
691
  }
692
+ if (diagnostics.length > 0) return {
693
+ kind: "error",
694
+ original,
695
+ diagnostics
696
+ };
1022
697
  return {
1023
- entries: collected.entries.filter((entry) => !rejectedSpecifiers.has(collected.fileOwners[entry.path])),
1024
- packageMap,
1025
- sourceEntries,
1026
- keyframesScanEntries,
1027
- packageDirs: collected.packageDirs.filter((dir) => !rejectedDirs.includes(dir)),
1028
- dirOwnerSets,
1029
- dirExtensions,
1030
- fileOwners,
1031
- outcomes: collected.outcomes
698
+ kind: "ok",
699
+ original,
700
+ entries: projections.flatMap((projection) => projection.entry ? [projection.entry] : [])
1032
701
  };
1033
702
  }
703
+ //#endregion
704
+ //#region pipeline/source-ingestion.ts
705
+ function isSerializedSourceEntry(value) {
706
+ if (!(value instanceof Object) || Array.isArray(value)) return false;
707
+ return String(value.path) === value.path && String(value.source) === value.source && (value.hash === void 0 || String(value.hash) === value.hash);
708
+ }
1034
709
  /**
1035
- * Extract external DS package names from inheritance declarations in the
1036
- * system file.
1037
- *
1038
- * Supports four forms:
1039
- * - Primary: `createSystem(...).extend(identifier)` chain calls
1040
- * (repeatable, mixable with `.from()` links; each call
1041
- * contributes one source; a library-bundle identifier
1042
- * traces its base import)
1043
- * - Deprecated chain: `createSystem(...).from(identifier)` chain calls
1044
- * - Deprecated alias: `createSystem({ includes: [identifier, ...] })`
1045
- * - Legacy: `.includes([identifier, ...])` chain method (RC
1046
- * migration fallback)
1047
- *
1048
- * For each identifier found, traces back to its import declaration and returns
1049
- * the import specifier: a bare specifier normalized to its package name, a
1050
- * relative specifier resolved against the system file's directory into an
1051
- * absolute path (so a sibling package referenced by path contributes discovery
1052
- * too). Only packages explicitly declared through one of these forms are
1053
- * treated as external DS dependencies.
1054
- *
1055
- * Falls back to empty array if no declaration is found.
1056
- */
1057
- /**
1058
- * The message for the strict/warn gate over unresolvable includes, or null
1059
- * when every declared specifier resolved (external-package-file-discovery:
1060
- * silence is never an outcome — non-strict consumers warn with this line,
1061
- * strict consumers throw it).
710
+ * Throws on a payload that is not a corpus: an empty corpus is
711
+ * indistinguishable from "no files" and would publish an empty stylesheet.
1062
712
  */
1063
- function unresolvableIncludesMessage(outcomes) {
1064
- const unresolvable = outcomes.filter((record) => record.outcome === "unresolvable").map((record) => record.specifier);
1065
- if (unresolvable.length === 0) return null;
1066
- return `[animus-extract] unresolvable include specifier(s): ${unresolvable.join(", ")}`;
713
+ function parseFilesJson(filesJson, context) {
714
+ const candidate = parseInternalWire(filesJson, `${context} filesJson`);
715
+ if (!Array.isArray(candidate) || !candidate.every(isSerializedSourceEntry)) throw new TypeError(`[${context}] analysis files JSON must be an array of {path, source} entries`);
716
+ return candidate;
1067
717
  }
1068
718
  /**
1069
- * The message for the strict/warn gate over stale dist entries
1070
- * (first-class-extension D13), or null when no declared specifier is stale.
1071
- * A stale dist silently skews merged registry content while discovery
1072
- * compiles the fresh sources, so it surfaces like an unresolvable specifier:
1073
- * non-strict consumers warn with this line, strict consumers throw it.
719
+ * Advisory diagnostics warn in every mode and never quarantine their file:
720
+ * OXC recovers parse errors the host bundler itself accepts. All else is fatal.
1074
721
  */
1075
- function staleDistIncludesMessage(outcomes) {
1076
- const stale = outcomes.filter((record) => record.outcome === "stale-dist").map((record) => record.specifier);
1077
- if (stale.length === 0) return null;
1078
- return `[animus-extract] stale dist for include specifier(s): ${stale.join(", ")} — dist entry is older than the newest src/ file; rebuild the package(s) before extracting`;
722
+ function isAdvisorySourceDiagnostic(diagnostic) {
723
+ return diagnostic.code === "SOURCE_NATIVE_PARSE_ERROR";
1079
724
  }
1080
- function extractSystemFilePackages(systemFilePath) {
1081
- let source;
1082
- try {
1083
- source = readFileSync(systemFilePath, "utf-8");
1084
- } catch {
1085
- return [];
725
+ const RELATIVE_PROBE_SUFFIXES = [
726
+ "",
727
+ ".ts",
728
+ ".tsx",
729
+ ".js",
730
+ ".jsx",
731
+ "/index.ts",
732
+ "/index.tsx",
733
+ "/index.js",
734
+ "/index.jsx"
735
+ ];
736
+ function extension(path) {
737
+ const basename = posix.basename(path);
738
+ const dot = basename.lastIndexOf(".");
739
+ return dot === -1 ? "" : basename.slice(dot).toLowerCase();
740
+ }
741
+ function canonicalResolverPath(path) {
742
+ return posix.normalize(path.replaceAll("\\", "/"));
743
+ }
744
+ const NODE_NEXT_EXTENSION_MAP = {
745
+ ".js": [
746
+ ".ts",
747
+ ".tsx",
748
+ ".jsx"
749
+ ],
750
+ ".mjs": [".mts"],
751
+ ".cjs": [".cts"]
752
+ };
753
+ function resolveRelativeSource(importerPath, specifier, files) {
754
+ if (!specifier.startsWith(".")) return null;
755
+ const base = posix.normalize(posix.join(posix.dirname(canonicalResolverPath(importerPath)), specifier));
756
+ for (const suffix of RELATIVE_PROBE_SUFFIXES) {
757
+ const candidate = `${base}${suffix}`;
758
+ const actualPath = files.get(candidate);
759
+ if (actualPath !== void 0) return actualPath;
1086
760
  }
1087
- const identifiers = /* @__PURE__ */ new Set();
1088
- const constructorRegex = /createSystem\s*\(\s*\{[^}]*?\bincludes\s*:\s*\[([^\]]*)\]/gs;
1089
- const chainRegex = /\.includes\s*\(\s*\[([^\]]*)\]\s*\)/gs;
1090
- const collectIdentifiers = (regex) => {
1091
- let match;
1092
- while ((match = regex.exec(source)) !== null) {
1093
- const inner = match[1];
1094
- for (const token of inner.split(",")) {
1095
- const id = token.trim();
1096
- if (id && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(id)) identifiers.add(id);
1097
- }
1098
- }
1099
- };
1100
- collectIdentifiers(constructorRegex);
1101
- collectIdentifiers(chainRegex);
1102
- const IDENT_START_RE = /^[a-zA-Z_$][a-zA-Z0-9_$]*/;
1103
- /** Position after any run of whitespace, line comments, block comments. */
1104
- const skipTrivia = (from) => {
1105
- let pos = from;
1106
- for (;;) {
1107
- while (pos < source.length && /\s/.test(source[pos])) pos++;
1108
- if (source.startsWith("//", pos)) {
1109
- const newline = source.indexOf("\n", pos);
1110
- pos = newline === -1 ? source.length : newline + 1;
1111
- continue;
1112
- }
1113
- if (source.startsWith("/*", pos)) {
1114
- const close = source.indexOf("*/", pos + 2);
1115
- pos = close === -1 ? source.length : close + 2;
1116
- continue;
1117
- }
1118
- return pos;
761
+ const explicitExtension = extension(base);
762
+ for (const sourceExtension of NODE_NEXT_EXTENSION_MAP[explicitExtension] ?? []) {
763
+ const candidate = `${base.slice(0, -explicitExtension.length)}${sourceExtension}`;
764
+ const actualPath = files.get(candidate);
765
+ if (actualPath !== void 0) return actualPath;
766
+ }
767
+ return null;
768
+ }
769
+ var ResolverExportIndex = class {
770
+ facts;
771
+ collisions;
772
+ files;
773
+ resolverBindings = /* @__PURE__ */ new Map();
774
+ /** Both memos live for one ingest only — the index is rebuilt per call,
775
+ * so neither needs invalidation. */
776
+ attributionMemo = /* @__PURE__ */ new Map();
777
+ resolveMemo = /* @__PURE__ */ new Map();
778
+ constructor(facts, analysisPaths) {
779
+ this.facts = facts;
780
+ const pathsByCanonical = /* @__PURE__ */ new Map();
781
+ for (const path of analysisPaths) {
782
+ const canonicalPath = canonicalResolverPath(path);
783
+ const paths = pathsByCanonical.get(canonicalPath) ?? [];
784
+ paths.push(path);
785
+ pathsByCanonical.set(canonicalPath, paths);
1119
786
  }
1120
- };
1121
- /**
1122
- * Consume consecutive extend/from links starting at `from`, collecting
1123
- * each link's base identifier; tolerates trivia between tokens and a
1124
- * trailing comma in the argument list. Returns the position after the
1125
- * last consumed link (`from` itself when none matched).
1126
- */
1127
- const consumeChainLinks = (from) => {
1128
- let pos = from;
1129
- for (;;) {
1130
- let cursor = skipTrivia(pos);
1131
- if (source[cursor] !== ".") return pos;
1132
- cursor = skipTrivia(cursor + 1);
1133
- const method = IDENT_START_RE.exec(source.slice(cursor))?.[0];
1134
- if (method !== "extend" && method !== "from") return pos;
1135
- cursor = skipTrivia(cursor + method.length);
1136
- if (source[cursor] !== "(") return pos;
1137
- cursor = skipTrivia(cursor + 1);
1138
- const base = IDENT_START_RE.exec(source.slice(cursor))?.[0];
1139
- if (!base) return pos;
1140
- cursor = skipTrivia(cursor + base.length);
1141
- while (source[cursor] === ".") {
1142
- const afterDot = skipTrivia(cursor + 1);
1143
- const segment = IDENT_START_RE.exec(source.slice(afterDot))?.[0];
1144
- if (!segment) break;
1145
- cursor = skipTrivia(afterDot + segment.length);
1146
- }
1147
- if (source[cursor] === ",") cursor = skipTrivia(cursor + 1);
1148
- if (source[cursor] !== ")") return pos;
1149
- identifiers.add(base);
1150
- pos = cursor + 1;
787
+ this.collisions = [...pathsByCanonical].filter(([, paths]) => paths.length > 1).map(([canonicalPath, paths]) => ({
788
+ canonicalPath,
789
+ paths
790
+ }));
791
+ const factPathsByCanonical = /* @__PURE__ */ new Map();
792
+ for (const path of Object.keys(facts.files)) {
793
+ const canonicalPath = canonicalResolverPath(path);
794
+ const paths = factPathsByCanonical.get(canonicalPath) ?? [];
795
+ paths.push(path);
796
+ factPathsByCanonical.set(canonicalPath, paths);
1151
797
  }
1152
- };
1153
- /** Identifier assigned directly before `index` (`const x = <here>`), if any. */
1154
- const boundIdentifierBefore = (index) => {
1155
- const match = /([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*$/.exec(source.slice(0, index));
1156
- return match ? match[1] : null;
1157
- };
1158
- const chainRootIdentifiers = /* @__PURE__ */ new Set();
1159
- const createSystemAnchor = /createSystem\s*\(/g;
1160
- let anchorMatch;
1161
- while ((anchorMatch = createSystemAnchor.exec(source)) !== null) {
1162
- let pos = anchorMatch.index + anchorMatch[0].length;
1163
- let depth = 1;
1164
- while (pos < source.length && depth > 0) {
1165
- const ch = source[pos];
1166
- if (ch === "(") depth++;
1167
- else if (ch === ")") depth--;
1168
- pos++;
798
+ const files = /* @__PURE__ */ new Map();
799
+ for (const [canonicalPath, paths] of pathsByCanonical) {
800
+ const factPaths = factPathsByCanonical.get(canonicalPath) ?? [];
801
+ if (paths.length === 1 && factPaths.length === 1) files.set(canonicalPath, factPaths[0]);
1169
802
  }
1170
- consumeChainLinks(pos);
1171
- const bound = boundIdentifierBefore(anchorMatch.index);
1172
- if (bound) chainRootIdentifiers.add(bound);
803
+ this.files = files;
804
+ for (const [path, file] of Object.entries(facts.files)) this.resolverBindings.set(path, new Set(file.chains.filter((chain) => chain.descriptor.terminal === "asClass" && chain.descriptor.extractable && chain.fatalError === null).map((chain) => chain.descriptor.binding)));
1173
805
  }
1174
- const scannedRoots = /* @__PURE__ */ new Set();
1175
- for (;;) {
1176
- const pendingRoots = [...chainRootIdentifiers].filter((root) => !scannedRoots.has(root));
1177
- if (pendingRoots.length === 0) break;
1178
- for (const root of pendingRoots) {
1179
- scannedRoots.add(root);
1180
- const rootUse = new RegExp(`(?<![a-zA-Z0-9_$])${root.replace(/\$/g, "\\$")}(?![a-zA-Z0-9_$])`, "g");
1181
- let useMatch;
1182
- while ((useMatch = rootUse.exec(source)) !== null) {
1183
- const afterIdentifier = useMatch.index + useMatch[0].length;
1184
- if (consumeChainLinks(afterIdentifier) === afterIdentifier) continue;
1185
- const bound = boundIdentifierBefore(useMatch.index);
1186
- if (bound) chainRootIdentifiers.add(bound);
1187
- }
806
+ attribute(importerPath, request) {
807
+ const memoKey = `${importerPath}\0${request.source}\0${request.imported}\0${request.access.kind}\0${request.access.importKind}`;
808
+ const memoized = this.attributionMemo.get(memoKey);
809
+ if (memoized !== void 0) return memoized;
810
+ const attribution = this.attributeUncached(importerPath, request);
811
+ this.attributionMemo.set(memoKey, attribution);
812
+ return attribution;
813
+ }
814
+ attributeUncached(importerPath, request) {
815
+ const importedFile = this.resolveSource(importerPath, request.source);
816
+ if (!importedFile) return "other";
817
+ if (this.resolveClassExport(importedFile, request.imported, /* @__PURE__ */ new Set()) === null) return "other";
818
+ return request.access.kind === "direct" && request.access.importKind === "named" ? "resolver" : "unsupported-resolver-form";
819
+ }
820
+ resolveSource(importerPath, specifier) {
821
+ const key = `${importerPath}\0${specifier}`;
822
+ let resolved = this.resolveMemo.get(key);
823
+ if (resolved === void 0) {
824
+ resolved = resolveRelativeSource(importerPath, specifier, this.files);
825
+ this.resolveMemo.set(key, resolved);
1188
826
  }
827
+ return resolved;
1189
828
  }
1190
- if (identifiers.size === 0) return [];
1191
- const importMap = /* @__PURE__ */ new Map();
1192
- const importRegex = /^\s*import\s+(?:([a-zA-Z_$][a-zA-Z0-9_$]*)\s*,\s*)?(?:\{([^}]*)\}|([a-zA-Z_$][a-zA-Z0-9_$]*))\s+from\s+['"]([^'"]+)['"]/gm;
1193
- let importMatch;
1194
- while ((importMatch = importRegex.exec(source)) !== null) {
1195
- const [, comboDefault, namedImports, defaultImport, specifier] = importMatch;
1196
- if (comboDefault) importMap.set(comboDefault, specifier);
1197
- if (defaultImport) importMap.set(defaultImport, specifier);
1198
- if (namedImports) for (const binding of namedImports.split(",")) {
1199
- const parts = binding.trim().split(/\s+as\s+/);
1200
- const localName = (parts[1] || parts[0]).trim();
1201
- if (localName) importMap.set(localName, specifier);
829
+ resolveClassExport(path, exportedName, seen) {
830
+ const identity = `${path}\0${exportedName}`;
831
+ if (seen.has(identity)) return null;
832
+ seen.add(identity);
833
+ const file = this.facts.files[path];
834
+ const exported = file?.exports.find((candidate) => candidate.exported === exportedName);
835
+ if (!file || !exported) return null;
836
+ if (exported.source === null) {
837
+ if (exported.local === null) return null;
838
+ if (this.resolverBindings.get(path)?.has(exported.local)) return exported.local;
839
+ const imported = file.imports.find((candidate) => candidate.local === exported.local);
840
+ if (!imported) return null;
841
+ const importedFile = this.resolveSource(path, imported.source);
842
+ return importedFile ? this.resolveClassExport(importedFile, imported.imported, seen) : null;
1202
843
  }
844
+ if (exported.original === null) return null;
845
+ const nextFile = this.resolveSource(path, exported.source);
846
+ return nextFile ? this.resolveClassExport(nextFile, exported.original, seen) : null;
1203
847
  }
1204
- const systemFileDir = dirname(systemFilePath);
1205
- const packages = /* @__PURE__ */ new Set();
1206
- for (const id of identifiers) {
1207
- const specifier = importMap.get(id);
1208
- if (!specifier) continue;
1209
- if (specifier.startsWith(".")) packages.add(resolve(systemFileDir, specifier));
1210
- else packages.add(specifier);
848
+ };
849
+ /**
850
+ * Native and MDX entries must establish the resolver index before any Svelte
851
+ * projection; bare-package and aliased sources stay unattributed by design.
852
+ */
853
+ async function ingestSourceEntries(rawEntries, options) {
854
+ const mdxAdapter = options.preprocessMdx ?? preprocessMdx;
855
+ const svelteAdapter = options.adaptSvelte ?? adaptSvelteSource;
856
+ const originalEntries = rawEntries.map((entry) => ({
857
+ ...entry,
858
+ hash: entry.hash ?? contentHash(entry.source)
859
+ }));
860
+ const rawOriginalPaths = new Set(originalEntries.map((entry) => entry.path));
861
+ const analysisEntries = [];
862
+ const ownership = {};
863
+ const analysisOwner = /* @__PURE__ */ new Map();
864
+ const diagnostics = [];
865
+ const svelteEntries = [];
866
+ const addAnalysisEntry = (originalPath, path, source, hash) => {
867
+ analysisEntries.push({
868
+ path,
869
+ source,
870
+ hash: hash ?? contentHash(source)
871
+ });
872
+ ownership[originalPath].analysisPaths.push(path);
873
+ analysisOwner.set(path, originalPath);
874
+ };
875
+ const addAdaptedEntries = (originalPath, entries) => {
876
+ const pendingPaths = /* @__PURE__ */ new Set();
877
+ for (const entry of entries) {
878
+ const conflictingOriginalPath = rawOriginalPaths.has(entry.path) ? entry.path : analysisOwner.get(entry.path) ?? (pendingPaths.has(entry.path) ? originalPath : null);
879
+ if (conflictingOriginalPath !== null) {
880
+ diagnostics.push({
881
+ code: "SOURCE_ANALYSIS_PATH_COLLISION",
882
+ message: `Generated analysis path '${entry.path}' collides with '${conflictingOriginalPath}'. Rename one of the source files so adapted analysis paths remain unique.`,
883
+ originalPath,
884
+ analysisPath: entry.path,
885
+ conflictingOriginalPath
886
+ });
887
+ return;
888
+ }
889
+ pendingPaths.add(entry.path);
890
+ }
891
+ for (const entry of entries) addAnalysisEntry(originalPath, entry.path, entry.source);
892
+ };
893
+ for (const original of originalEntries) {
894
+ ownership[original.path] = {
895
+ originalPath: original.path,
896
+ originalHash: original.hash,
897
+ analysisPaths: []
898
+ };
899
+ const kind = extension(original.path);
900
+ if (kind === ".svelte") {
901
+ svelteEntries.push(original);
902
+ continue;
903
+ }
904
+ if (kind !== ".mdx") {
905
+ addAnalysisEntry(original.path, original.path, original.source, original.hash);
906
+ continue;
907
+ }
908
+ const result = await mdxAdapter(original.source, original.path);
909
+ if (result.kind === "missing-dep") {
910
+ diagnostics.push({
911
+ code: "SOURCE_MDX_DEPENDENCY_MISSING",
912
+ message: "Install optional peer dependency '@mdx-js/mdx' to analyze opted-in MDX source.",
913
+ originalPath: original.path
914
+ });
915
+ continue;
916
+ }
917
+ if (result.kind === "error" || result.source === void 0) {
918
+ diagnostics.push({
919
+ code: "SOURCE_MDX_PARSE_ERROR",
920
+ message: result.error ?? "MDX preprocessing produced no source.",
921
+ originalPath: original.path
922
+ });
923
+ continue;
924
+ }
925
+ addAdaptedEntries(original.path, [{
926
+ path: `${original.path}.tsx`,
927
+ source: result.source
928
+ }]);
1211
929
  }
1212
- return Array.from(packages);
1213
- }
1214
- //#endregion
1215
- //#region pipeline/svelte-source-adapter.ts
1216
- function isNode(value) {
1217
- return typeof value === "object" && value !== null && typeof value.type === "string" && typeof value.start === "number" && typeof value.end === "number";
1218
- }
1219
- function childNode(value) {
1220
- return isNode(value) ? value : null;
1221
- }
1222
- function nodeName(node) {
1223
- return node?.type === "Identifier" && typeof node.name === "string" ? node.name : null;
1224
- }
1225
- /** Adaptation touches one source at a time, so a last-value memo makes the
1226
- * all-ASCII check O(n) once per file instead of per span endpoint. */
1227
- let asciiMemoSource = null;
1228
- let asciiMemoValue = false;
1229
- function isAsciiOnly(source) {
1230
- if (asciiMemoSource !== source) {
1231
- asciiMemoSource = source;
1232
- asciiMemoValue = Buffer.byteLength(source) === source.length;
930
+ const facts = collectFileFacts(analysisEntries, options);
931
+ for (const [analysisPath, file] of Object.entries(facts.files)) {
932
+ const originalPath = analysisOwner.get(analysisPath) ?? analysisPath;
933
+ for (const message of file.parseDiagnostics) diagnostics.push({
934
+ code: "SOURCE_NATIVE_PARSE_ERROR",
935
+ message,
936
+ originalPath,
937
+ analysisPath
938
+ });
939
+ }
940
+ const resolvers = new ResolverExportIndex(facts, analysisEntries.map((entry) => entry.path));
941
+ for (const { canonicalPath, paths } of resolvers.collisions) for (let index = 0; index < paths.length; index += 1) {
942
+ const path = paths[index];
943
+ const conflictingPath = paths[(index + 1) % paths.length];
944
+ diagnostics.push({
945
+ code: "SOURCE_RESOLVER_IDENTITY_COLLISION",
946
+ message: `Resolver lookup paths '${path}' and '${conflictingPath}' normalize to the same private identity '${canonicalPath}'. Rename one of the source files so resolver identities remain unique.`,
947
+ originalPath: analysisOwner.get(path) ?? path,
948
+ canonicalPath,
949
+ conflictingOriginalPath: analysisOwner.get(conflictingPath) ?? conflictingPath
950
+ });
951
+ }
952
+ for (const original of svelteEntries) {
953
+ const result = await svelteAdapter(original.source, original.path, { attributeResolver: (request) => resolvers.attribute(original.path, request) });
954
+ if (result.kind === "missing-dep") {
955
+ diagnostics.push({
956
+ code: "SOURCE_SVELTE_DEPENDENCY_MISSING",
957
+ message: "Install optional peer dependency 'svelte' to analyze opted-in Svelte source.",
958
+ originalPath: original.path
959
+ });
960
+ continue;
961
+ }
962
+ if (result.kind === "error") {
963
+ diagnostics.push(...result.diagnostics);
964
+ continue;
965
+ }
966
+ addAdaptedEntries(original.path, result.entries);
1233
967
  }
1234
- return asciiMemoValue;
1235
- }
1236
- function byteOffset(source, characterOffset) {
1237
- if (isAsciiOnly(source)) return characterOffset;
1238
- return Buffer.byteLength(source.slice(0, characterOffset));
1239
- }
1240
- function byteSpan(source, node) {
1241
968
  return {
1242
- start: byteOffset(source, node.start),
1243
- end: byteOffset(source, node.end)
969
+ originalEntries,
970
+ analysisEntries,
971
+ ownership,
972
+ diagnostics
1244
973
  };
1245
974
  }
1246
- function sourcePosition(source, characterOffset) {
1247
- let line = 1;
1248
- let lineStart = 0;
1249
- for (let index = 0; index < characterOffset; index += 1) {
1250
- const code = source.charCodeAt(index);
1251
- if (code === 13) {
1252
- if (index + 1 < characterOffset && source.charCodeAt(index + 1) === 10) index += 1;
1253
- line += 1;
1254
- lineStart = index + 1;
1255
- } else if (code === 10 || code === 8232 || code === 8233) {
1256
- line += 1;
1257
- lineStart = index + 1;
975
+ /**
976
+ * `parseCount` counts the parses performed by THIS call, so a fully-memoized
977
+ * pass reports zero.
978
+ */
979
+ function collectFileFacts(analysisEntries, options) {
980
+ const cache = options.factsCache;
981
+ if (!cache) return JSON.parse(options.extractFacts(JSON.stringify(analysisEntries)));
982
+ const pending = analysisEntries.filter((entry) => cache.get(entry.path)?.hash !== entry.hash);
983
+ let parseCount = 0;
984
+ if (pending.length > 0) {
985
+ const fresh = JSON.parse(options.extractFacts(JSON.stringify(pending)));
986
+ parseCount = fresh.parseCount;
987
+ for (const entry of pending) {
988
+ const facts = fresh.files[entry.path];
989
+ if (facts) cache.set(entry.path, {
990
+ hash: entry.hash,
991
+ facts
992
+ });
993
+ else cache.delete(entry.path);
1258
994
  }
1259
995
  }
996
+ const currentPaths = new Set(analysisEntries.map((entry) => entry.path));
997
+ for (const path of [...cache.keys()]) if (!currentPaths.has(path)) cache.delete(path);
998
+ const files = {};
999
+ for (const entry of analysisEntries) {
1000
+ const cached = cache.get(entry.path);
1001
+ if (cached) files[entry.path] = cached.facts;
1002
+ }
1260
1003
  return {
1261
- line,
1262
- column: characterOffset - lineStart
1004
+ files,
1005
+ parseCount
1263
1006
  };
1264
1007
  }
1265
- function sourceLocation(source, node) {
1008
+ /**
1009
+ * Drops each named original and the analysis children it owns. Every corpus
1010
+ * path applies it — skipping it on incremental passes freezes re-analysis.
1011
+ */
1012
+ function withoutInvalidOriginals(result, invalidOriginals) {
1013
+ if (invalidOriginals.size === 0) return result;
1014
+ const ownership = Object.fromEntries(Object.entries(result.ownership).filter(([originalPath]) => !invalidOriginals.has(originalPath)));
1015
+ const analysisPaths = new Set(Object.values(ownership).flatMap((owner) => owner.analysisPaths));
1266
1016
  return {
1267
- start: sourcePosition(source, node.start),
1268
- end: sourcePosition(source, node.end)
1017
+ ...result,
1018
+ originalEntries: result.originalEntries.filter((entry) => !invalidOriginals.has(entry.path)),
1019
+ analysisEntries: result.analysisEntries.filter((entry) => analysisPaths.has(entry.path)),
1020
+ ownership
1269
1021
  };
1270
1022
  }
1271
- function createsNestedBindingScope(node) {
1272
- return node.type === "BlockStatement" || node.type === "CatchClause" || node.type === "ClassBody" || node.type === "ClassDeclaration" || node.type === "ClassExpression" || node.type === "ForInStatement" || node.type === "ForOfStatement" || node.type === "ForStatement" || node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression" || node.type === "StaticBlock" || node.type === "SwitchStatement" || node.type === "TSModuleBlock";
1273
- }
1274
- function walk(node, visit, nestedScopeDepth = 0) {
1275
- visit(node, nestedScopeDepth);
1276
- const childScopeDepth = nestedScopeDepth + (createsNestedBindingScope(node) ? 1 : 0);
1277
- for (const [key, value] of Object.entries(node)) {
1278
- if (key === "loc" || key === "start" || key === "end") continue;
1279
- if (Array.isArray(value)) {
1280
- for (const item of value) if (isNode(item)) walk(item, visit, childScopeDepth);
1281
- } else if (isNode(value)) walk(value, visit, childScopeDepth);
1282
- }
1283
- }
1284
1023
  /**
1285
- * Fragment traversal: Svelte `Fragment` containers (the template root and
1286
- * every block body) carry no `start`/`end`, so the script walker's isNode
1287
- * gate would stop at each one. Descend through every plain object/array,
1288
- * visit only span-carrying nodes, and guard against metadata back-references.
1024
+ * Diagnostics name ANALYSIS paths while ownership is recorded for originals,
1025
+ * so projection is required; the result REPLACES the caller's owner map.
1289
1026
  */
1290
- function walkFragment(value, visit) {
1291
- const seen = /* @__PURE__ */ new WeakSet();
1292
- const descend = (current) => {
1293
- if (Array.isArray(current)) {
1294
- for (const item of current) descend(item);
1295
- return;
1296
- }
1297
- if (typeof current !== "object" || current === null) return;
1298
- if (seen.has(current)) return;
1299
- seen.add(current);
1300
- if (isNode(current)) visit(current);
1301
- for (const [key, child] of Object.entries(current)) {
1302
- if (key === "loc" || key === "start" || key === "end" || key === "metadata" || key === "parent") continue;
1303
- descend(child);
1304
- }
1305
- };
1306
- descend(value);
1307
- }
1308
- function importBindings(program) {
1309
- const bindings = /* @__PURE__ */ new Map();
1310
- for (const declaration of program.body) {
1311
- if (declaration.type !== "ImportDeclaration" || declaration.importKind === "type" || !Array.isArray(declaration.specifiers)) continue;
1312
- for (const value of declaration.specifiers) {
1313
- if (!isNode(value) || value.importKind === "type") continue;
1314
- const local = childNode(value.local);
1315
- const localName = nodeName(local);
1316
- if (!local || !localName) continue;
1317
- const sourceNode = childNode(declaration.source);
1318
- const importSource = sourceNode && typeof sourceNode.value === "string" ? sourceNode.value : null;
1319
- if (!importSource) continue;
1320
- const namedImported = importedName(value);
1321
- if (value.type === "ImportSpecifier" && namedImported === null) continue;
1322
- const importedNode = childNode(value.imported);
1323
- const kind = value.type === "ImportSpecifier" ? importedNode?.type === "Literal" ? "string-named" : namedImported === "default" ? "named-default" : "named" : value.type === "ImportDefaultSpecifier" ? "default" : value.type === "ImportNamespaceSpecifier" ? "namespace" : null;
1324
- if (kind) bindings.set(localName, {
1325
- kind,
1326
- source: importSource,
1327
- imported: kind === "default" || kind === "named-default" ? "default" : kind === "namespace" ? "*" : namedImported,
1328
- declaration,
1329
- specifier: value,
1330
- local
1331
- });
1332
- }
1027
+ function projectExternalFileOwners(result, rawOwners) {
1028
+ const projected = {};
1029
+ for (const owner of Object.values(result.ownership)) {
1030
+ const packageOwner = rawOwners[owner.originalPath];
1031
+ if (!packageOwner) continue;
1032
+ projected[owner.originalPath] = packageOwner;
1033
+ for (const analysisPath of owner.analysisPaths) projected[analysisPath] = packageOwner;
1333
1034
  }
1334
- return bindings;
1335
- }
1336
- function diagnostic(code, message, originalPath, source, node, location) {
1337
- return {
1338
- code,
1339
- message,
1340
- originalPath,
1341
- ...node ? {
1342
- span: byteSpan(source, node),
1343
- location: location ?? sourceLocation(source, node)
1344
- } : {}
1345
- };
1035
+ return projected;
1346
1036
  }
1347
- function compilerParseErrorRange(error, source) {
1348
- if (typeof error !== "object" || error === null) return null;
1349
- const candidate = error;
1350
- const positions = Array.isArray(candidate.position) ? candidate.position : [];
1351
- const startCharacter = typeof candidate.start?.character === "number" ? candidate.start.character : typeof positions[0] === "number" ? positions[0] : null;
1352
- const endCharacter = typeof candidate.end?.character === "number" ? candidate.end.character : typeof positions[1] === "number" ? positions[1] : startCharacter;
1353
- if (startCharacter === null || endCharacter === null || startCharacter < 0 || endCharacter < startCharacter || endCharacter > source.length) return null;
1354
- const node = {
1355
- type: "SvelteParseError",
1356
- start: startCharacter,
1357
- end: endCharacter
1358
- };
1359
- const fallback = sourceLocation(source, node);
1360
- const point = (value, fallbackPoint) => ({
1361
- line: typeof value?.line === "number" ? value.line : fallbackPoint.line,
1362
- column: typeof value?.column === "number" ? value.column : fallbackPoint.column
1363
- });
1037
+ /**
1038
+ * One ingestion policy point for every host: a host supplies only a prefix, a
1039
+ * strict flag, and a warn sink; policy and per-host memos stay here.
1040
+ */
1041
+ function createSourceIngestor(host) {
1042
+ const factsCache = /* @__PURE__ */ new Map();
1043
+ /** Non-strict dedupe: a quarantined original re-ingests on every later
1044
+ * corpus pass, so re-warning it on each save is noise. */
1045
+ const warnedByOriginal = /* @__PURE__ */ new Map();
1364
1046
  return {
1365
- node,
1366
- location: {
1367
- start: point(candidate.start, fallback.start),
1368
- end: point(candidate.end, fallback.end)
1047
+ async ingest(entries) {
1048
+ const extractFacts = host.engineApi().extractFacts;
1049
+ if (extractFacts === void 0) throw new Error(`${host.prefix} native engine does not expose extractFacts required for source adaptation`);
1050
+ return ingestSourceEntries(entries, {
1051
+ extractFacts,
1052
+ factsCache
1053
+ });
1054
+ },
1055
+ surfaceDiagnostics(diagnostics) {
1056
+ const fatal = diagnostics.filter((diagnostic) => !isAdvisorySourceDiagnostic(diagnostic));
1057
+ const invalidOriginals = new Set(fatal.map((diagnostic) => diagnostic.originalPath));
1058
+ if (diagnostics.length === 0) return invalidOriginals;
1059
+ if (host.strict() && fatal.length > 0) {
1060
+ const lines = fatal.map((diagnostic) => `${diagnostic.code} ${diagnostic.originalPath}: ${diagnostic.message}`);
1061
+ throw new Error(`${host.prefix} ${lines.join(`\n${host.prefix} `)}`);
1062
+ }
1063
+ for (const diagnostic of diagnostics) {
1064
+ const line = `${diagnostic.code} ${diagnostic.originalPath}: ${diagnostic.message}`;
1065
+ let warned = warnedByOriginal.get(diagnostic.originalPath);
1066
+ if (!warned) {
1067
+ warned = /* @__PURE__ */ new Set();
1068
+ warnedByOriginal.set(diagnostic.originalPath, warned);
1069
+ }
1070
+ if (warned.has(line)) continue;
1071
+ warned.add(line);
1072
+ host.warn(`${host.prefix} ${line}`);
1073
+ }
1074
+ return invalidOriginals;
1075
+ },
1076
+ markPublished(result) {
1077
+ for (const originalPath of Object.keys(result.ownership)) warnedByOriginal.delete(originalPath);
1369
1078
  }
1370
1079
  };
1371
1080
  }
1372
- function calleeParts(node) {
1373
- if (node.type !== "CallExpression") return null;
1374
- const callee = childNode(node.callee);
1375
- if (!callee || callee.type !== "MemberExpression") return null;
1376
- const object = childNode(callee.object);
1377
- const property = childNode(callee.property);
1378
- if (!object || !property) return null;
1379
- if ((nodeName(property) ?? (property.type === "Literal" && property.value === "attrs" ? "attrs" : null)) !== "attrs") return null;
1380
- return {
1381
- object,
1382
- property,
1383
- computed: callee.computed === true,
1384
- optional: callee.optional === true || node.optional === true
1385
- };
1386
- }
1387
- function resolverImportAccess(receiver, bindings) {
1388
- const directName = nodeName(receiver);
1389
- if (directName) {
1390
- const binding = bindings.get(directName);
1391
- if (!binding) return null;
1081
+ //#endregion
1082
+ //#region pipeline/engine-adapter.ts
1083
+ /**
1084
+ * One engine API for both plugins. Imports neither the native binding nor any
1085
+ * plugin or bundler — every such dependency arrives injected.
1086
+ */
1087
+ /**
1088
+ * Returns a function, invoked per call site, so `isV2()` and
1089
+ * `loadNativeEngine()` re-evaluate each time instead of being captured.
1090
+ */
1091
+ function createV2EngineApi(deps) {
1092
+ const { label, isV2, loadNativeEngine, store } = deps;
1093
+ return () => {
1094
+ if (!isV2()) return loadNativeEngine();
1095
+ const native = loadNativeEngine();
1392
1096
  return {
1393
- binding,
1394
- resolver: receiver,
1395
- displayName: directName,
1396
- request: {
1397
- source: binding.source,
1398
- imported: binding.imported,
1399
- local: directName,
1400
- access: {
1401
- kind: "direct",
1402
- importKind: binding.kind
1097
+ loadSystemModule: (...args) => native.loadSystemModule(...args),
1098
+ extractFacts: (filesJson) => native.extractFacts(filesJson),
1099
+ analyzeProject: (filesJsonRaw, scalesJson, variableMapJson, contextualVarsJson, propConfigJson, groupRegistryJson, packageResolutionJson, devMode, emitterConfigJson, selectorAliasesJson, _selectorOrderJson, globalStyleBlocksJson, pathAliasesJson, keyframesJson, staticCssJson, conditionAliasesJson, externalDirsJson, transformSourcesJson) => {
1100
+ const filesJson = deps.rehydrateFilesJson ? deps.rehydrateFilesJson(filesJsonRaw) : filesJsonRaw;
1101
+ const sent = /* @__PURE__ */ new Map();
1102
+ for (const entry of parseFilesJson(filesJson, label)) sent.set(entry.path, entry.source);
1103
+ store.setSentSources(sent);
1104
+ const emitterConfig = emitterConfigJson ? JSON.parse(emitterConfigJson) : {};
1105
+ store.setEngine(null);
1106
+ const config = {
1107
+ runtimeImport: emitterConfig.runtime_import ?? void 0,
1108
+ cssModuleId: emitterConfig.css_module_id ?? void 0,
1109
+ systemPropsModuleId: emitterConfig.system_props_module_id ?? void 0,
1110
+ themeJson: scalesJson,
1111
+ variableMapJson,
1112
+ contextualVarsJson: contextualVarsJson ?? void 0,
1113
+ configJson: propConfigJson,
1114
+ groupRegistryJson,
1115
+ selectorAliasesJson: selectorAliasesJson ?? void 0,
1116
+ conditionAliasesJson: conditionAliasesJson ?? void 0,
1117
+ globalStyleBlocksJson: globalStyleBlocksJson ?? void 0,
1118
+ keyframesJson: keyframesJson ?? void 0,
1119
+ packageResolutionJson: packageResolutionJson ?? void 0,
1120
+ pathAliasesJson: pathAliasesJson ?? void 0,
1121
+ staticCssJson: staticCssJson ?? void 0,
1122
+ externalDirsJson: externalDirsJson ?? void 0,
1123
+ transformSourcesJson: transformSourcesJson ?? void 0,
1124
+ devMode
1125
+ };
1126
+ const engine = new native.ExtractEngine(config);
1127
+ store.setEngine(engine);
1128
+ return engine.analyze(filesJson);
1129
+ },
1130
+ transformFile: (source, path, _manifest) => {
1131
+ const engine = store.getEngine();
1132
+ if (!engine) throw new Error(`[${label}] v2 transform before analyze — engine instance not initialized`);
1133
+ const sentMap = store.getSentSources();
1134
+ if (deps.passThroughUnknownPaths && sentMap && !sentMap.has(path)) return {
1135
+ code: source,
1136
+ hasComponents: false
1137
+ };
1138
+ const sent = sentMap?.get(path);
1139
+ if (sent !== void 0 && sent !== source && !store.getDriftWarned()) {
1140
+ store.setDriftWarned(true);
1141
+ console.warn(`[${label}] v2: transform-time source for ${path} differs from analyze-time source — an upstream transform may be reverted`);
1403
1142
  }
1143
+ return JSON.parse(engine.transformFile(path));
1144
+ },
1145
+ clearAnalysisCache: () => {
1146
+ const engine = store.getEngine();
1147
+ if (engine) engine.clearCache();
1148
+ store.setEngine(null);
1404
1149
  }
1405
1150
  };
1406
- }
1407
- if (receiver.type !== "MemberExpression" || receiver.computed === true || receiver.optional === true) return null;
1408
- const namespaceNode = childNode(receiver.object);
1409
- const memberNode = childNode(receiver.property);
1410
- const namespaceName = nodeName(namespaceNode);
1411
- const memberName = nodeName(memberNode) ?? (memberNode?.type === "Literal" && typeof memberNode.value === "string" ? memberNode.value : null);
1412
- if (!namespaceNode || !namespaceName || !memberName) return null;
1413
- const binding = bindings.get(namespaceName);
1414
- if (!binding || binding.kind !== "namespace") return null;
1415
- return {
1416
- binding,
1417
- resolver: receiver,
1418
- displayName: `${namespaceName}.${memberName}`,
1419
- request: {
1420
- source: binding.source,
1421
- imported: memberName,
1422
- local: namespaceName,
1423
- access: {
1424
- kind: "namespace-member",
1425
- importKind: "namespace"
1426
- }
1427
- }
1428
1151
  };
1429
1152
  }
1430
- function isSupportedResolverAccess(access) {
1431
- return access.binding.kind === "named" && access.request.access.kind === "direct";
1432
- }
1433
- function resolverCallSite(node, bindings) {
1434
- if (node.type !== "CallExpression") return null;
1435
- const parts = calleeParts(node);
1436
- if (parts) {
1437
- const access = resolverImportAccess(parts.object, bindings);
1438
- if (!access) return null;
1439
- return {
1440
- access,
1441
- form: "attrs",
1442
- computed: parts.computed,
1443
- optional: parts.optional
1444
- };
1445
- }
1446
- const callee = childNode(node.callee);
1447
- if (!callee) return null;
1448
- const access = resolverImportAccess(callee, bindings);
1449
- if (!access) return null;
1450
- return {
1451
- access,
1452
- form: "callable",
1453
- computed: callee.type === "MemberExpression" && callee.computed === true,
1454
- optional: node.optional === true || callee.type === "MemberExpression" && callee.optional === true
1455
- };
1153
+ //#endregion
1154
+ //#region pipeline/engine-retirement.ts
1155
+ const RETIRED_ENGINE_MESSAGE = "animus: extraction engine 'v1' was retired (openspec: retire-extract-v1) — v2 is the only engine; remove the engine option / ANIMUS_ENGINE override";
1156
+ function assertNoRetiredEngineSelection(engineOption) {
1157
+ if (engineOption === "v1" || process.env.ANIMUS_ENGINE === "v1") throw new Error(RETIRED_ENGINE_MESSAGE);
1456
1158
  }
1457
- function argumentSpan(argumentsList, fallback) {
1458
- if (argumentsList.length === 0) return fallback;
1459
- return {
1460
- type: "ArgumentRange",
1461
- start: argumentsList[0].start,
1462
- end: argumentsList[argumentsList.length - 1].end
1463
- };
1464
- }
1465
- function propertyName(node) {
1466
- const key = childNode(node.key);
1467
- if (!key) return null;
1468
- if (key.type === "Identifier" && typeof key.name === "string") return key.name;
1469
- if (key.type === "Literal" && typeof key.value === "string") return key.value;
1470
- return null;
1471
- }
1472
- function isJsxAttributeName(name) {
1473
- return /^[A-Za-z_$][A-Za-z0-9_$-]*$/.test(name);
1474
- }
1475
- function tsxIncompatibleValue(value) {
1476
- let incompatible = null;
1477
- walk(value, (node) => {
1478
- if (incompatible) return;
1479
- if (node.type === "TSTypeAssertion" || node.type === "ArrowFunctionExpression" && childNode(node.typeParameters) !== null) incompatible = node;
1480
- });
1481
- return incompatible;
1482
- }
1483
- function readProperties(argument, source, originalPath) {
1484
- if (!Array.isArray(argument.properties)) return diagnostic("SVELTE_ATTRS_ARGUMENT_UNRESOLVED", "Resolver .attrs() requires an object literal with statically named properties.", originalPath, source, argument);
1485
- const properties = [];
1486
- for (const value of argument.properties) {
1487
- if (!isNode(value)) continue;
1488
- if (value.type === "SpreadElement") return diagnostic("SVELTE_ATTRS_SPREAD_UNRESOLVED", "Resolver .attrs() object spreads are unresolved; enumerate properties explicitly.", originalPath, source, value);
1489
- if (value.type !== "Property") return diagnostic("SVELTE_ATTRS_PROPERTY_UNSUPPORTED", "Resolver .attrs() accepts ordinary object properties only.", originalPath, source, value);
1490
- if (value.computed === true) return diagnostic("SVELTE_ATTRS_COMPUTED_KEY", "Resolver .attrs() computed property names cannot be attributed safely.", originalPath, source, value);
1491
- if (value.method === true || value.kind !== "init") return diagnostic("SVELTE_ATTRS_PROPERTY_UNSUPPORTED", "Resolver .attrs() methods and accessors cannot be projected as attributes.", originalPath, source, value);
1492
- const key = childNode(value.key);
1493
- const propertyValue = childNode(value.value);
1494
- const name = propertyName(value);
1495
- if (!key || !propertyValue || !name || !isJsxAttributeName(name)) return diagnostic("SVELTE_ATTRS_PROPERTY_UNSUPPORTED", "Resolver .attrs() property names must be JSX-compatible identifier or string names.", originalPath, source, value);
1496
- const incompatibleValue = tsxIncompatibleValue(propertyValue);
1497
- if (incompatibleValue) return diagnostic("SVELTE_ATTRS_VALUE_UNSUPPORTED", "Resolver .attrs() values using angle-bracket type assertions or generic arrow parameters cannot be projected into TSX safely.", originalPath, source, incompatibleValue);
1498
- properties.push({
1499
- name,
1500
- property: value,
1501
- key,
1502
- value: propertyValue
1503
- });
1504
- }
1505
- return properties;
1506
- }
1507
- var VirtualSourceBuilder = class {
1508
- chunks = [];
1509
- bytes = 0;
1510
- mappings = [];
1511
- constructor(originalSource) {
1512
- this.originalSource = originalSource;
1513
- }
1514
- append(text) {
1515
- this.chunks.push(text);
1516
- this.bytes += Buffer.byteLength(text);
1517
- }
1518
- appendMapped(text, kind, original) {
1519
- const start = this.bytes;
1520
- this.append(text);
1521
- this.mappings.push({
1522
- kind,
1523
- generated: {
1524
- start,
1525
- end: this.bytes
1526
- },
1527
- original: byteSpan(this.originalSource, original)
1528
- });
1529
- }
1530
- toString() {
1531
- return this.chunks.join("");
1159
+ //#endregion
1160
+ //#region pipeline/core-options.ts
1161
+ const DRIVER_NAMESPACES = [
1162
+ "vite",
1163
+ "next",
1164
+ "cli"
1165
+ ];
1166
+ const DRIVER_NAMESPACE_KEYS = new Set(DRIVER_NAMESPACES);
1167
+ /**
1168
+ * Applied unconditionally: `node_modules` belongs to external-package
1169
+ * collection, and `.next`/`.animus` hold output, never source.
1170
+ */
1171
+ const STRUCTURAL_EXCLUDE = [
1172
+ "node_modules",
1173
+ ".next",
1174
+ ".animus"
1175
+ ];
1176
+ /** Defaults a user `exclude` list replaces; the structural set it cannot. */
1177
+ const REPLACEABLE_DEFAULT_EXCLUDE = [
1178
+ "dist",
1179
+ ".test.",
1180
+ ".spec."
1181
+ ];
1182
+ const DEFAULT_EXCLUDE = [...STRUCTURAL_EXCLUDE, ...REPLACEABLE_DEFAULT_EXCLUDE];
1183
+ const CORE_OPTION_KEYS = /* @__PURE__ */ new Set([
1184
+ "system",
1185
+ "exclude",
1186
+ "extensions",
1187
+ "strict",
1188
+ "verbose",
1189
+ "prefix",
1190
+ "targets",
1191
+ "minify",
1192
+ "staticCss",
1193
+ "layers",
1194
+ "engine",
1195
+ "mode",
1196
+ "root"
1197
+ ]);
1198
+ /** Carries a stable `name` so drivers can map it to their own config-error
1199
+ * surface (the CLI's exit code 2). */
1200
+ var AnimusConfigError = class extends Error {
1201
+ constructor(message) {
1202
+ super(message);
1203
+ this.name = "AnimusConfigError";
1532
1204
  }
1533
1205
  };
1534
- function importedName(specifier) {
1535
- const imported = childNode(specifier.imported);
1536
- if (!imported) return null;
1537
- if (imported.type === "Identifier" && typeof imported.name === "string") return imported.name;
1538
- if (imported.type === "Literal" && typeof imported.value === "string") return imported.value;
1539
- return null;
1540
- }
1541
- function renderImport(declaration, witnessedBindings, source) {
1542
- if (!Array.isArray(declaration.specifiers)) return null;
1543
- const names = [];
1544
- const locals = [];
1545
- for (const value of declaration.specifiers) {
1546
- if (!isNode(value) || value.type !== "ImportSpecifier") continue;
1547
- const localName = nodeName(childNode(value.local));
1548
- if (!localName || !witnessedBindings.has(localName)) continue;
1549
- names.push(source.slice(value.start, value.end));
1550
- locals.push(localName);
1551
- }
1552
- if (names.length === 0) return null;
1553
- const sourceNode = childNode(declaration.source);
1554
- const sourceText = sourceNode && typeof sourceNode.raw === "string" ? sourceNode.raw : sourceNode && typeof sourceNode.value === "string" ? JSON.stringify(sourceNode.value) : null;
1555
- return sourceText ? {
1556
- text: `import { ${names.join(", ")} } from ${sourceText};\n`,
1557
- locals
1558
- } : null;
1559
- }
1560
- /** Memoizes success only: a missing optional peer retries per file, so
1561
- * installing `svelte` mid-session recovers without a process restart. */
1562
- let compilerMemo = null;
1563
- async function loadCompiler() {
1564
- if (compilerMemo) return compilerMemo;
1565
- compilerMemo = await import("svelte/compiler").catch(() => null);
1566
- return compilerMemo;
1567
- }
1568
- function projectScope(script, scope, source, originalPath, options, inherited, precomputedOwnBindings) {
1569
- const ownBindings = precomputedOwnBindings ?? importBindings(script.content);
1570
- const bindings = inherited ? new Map([...inherited.bindings, ...ownBindings]) : ownBindings;
1571
- const witnesses = [];
1572
- const diagnostics = [];
1573
- if (bindings.size > 0) walk(script.content, (node, nestedScopeDepth) => {
1574
- const site = resolverCallSite(node, bindings);
1575
- if (!site) return;
1576
- const { access, form } = site;
1577
- const attribution = options.attributeResolver(access.request);
1578
- if (attribution === "other") return;
1579
- const resolverName = access.displayName;
1580
- const callDisplay = form === "attrs" ? `${resolverName}.attrs()` : `${resolverName}()`;
1581
- if (attribution === "unsupported-resolver-form" || !isSupportedResolverAccess(access)) {
1582
- diagnostics.push(diagnostic("SVELTE_ATTRS_IMPORT_UNSUPPORTED", `Resolver '${resolverName}' uses an import/access form that the extraction engine cannot attribute to its exported .asClass() binding.`, originalPath, source, access.resolver));
1583
- return;
1584
- }
1585
- if (nestedScopeDepth > 0) {
1586
- diagnostics.push(diagnostic("SVELTE_ATTRS_SCOPE_UNSUPPORTED", `Resolver '${callDisplay}' is inside a nested binding scope; move the call to the top-level script scope so its import identity is unambiguous.`, originalPath, source, access.resolver));
1587
- return;
1588
- }
1589
- if (site.computed || site.optional) {
1590
- diagnostics.push(diagnostic("SVELTE_ATTRS_CALLEE_UNSUPPORTED", `Resolver '${resolverName}' must be called directly as ${resolverName}(...) or ${resolverName}.attrs(...).`, originalPath, source, node));
1591
- return;
1592
- }
1593
- const argumentsList = Array.isArray(node.arguments) ? node.arguments.filter(isNode) : [];
1594
- if (argumentsList.length === 0) {
1595
- witnesses.push({
1596
- binding: access.binding,
1597
- resolver: access.resolver,
1598
- properties: []
1599
- });
1600
- return;
1601
- }
1602
- if (argumentsList.length !== 1 || argumentsList[0].type !== "ObjectExpression") {
1603
- diagnostics.push(diagnostic("SVELTE_ATTRS_ARGUMENT_UNRESOLVED", `Resolver '${callDisplay}' accepts no argument or one object literal in Svelte projection.`, originalPath, source, argumentSpan(argumentsList, node)));
1604
- return;
1605
- }
1606
- const properties = readProperties(argumentsList[0], source, originalPath);
1607
- if (!Array.isArray(properties)) {
1608
- diagnostics.push(properties);
1206
+ /**
1207
+ * `ownKeys` is the caller's own top-level surface; `rejectKeys` names core
1208
+ * keys it does not honor, which must fail rather than be silently ignored.
1209
+ */
1210
+ function assertKnownOptionKeys(raw, ownKeys = [], rejectKeys = [], opts = {}) {
1211
+ const surface = (message) => {
1212
+ if (opts.onUnknownKey === "warn") {
1213
+ opts.warn(message);
1609
1214
  return;
1610
1215
  }
1611
- witnesses.push({
1612
- binding: access.binding,
1613
- resolver: access.resolver,
1614
- properties
1615
- });
1616
- });
1617
- if (diagnostics.length > 0 || witnesses.length === 0) return { diagnostics };
1618
- const witnessedBindings = new Set(witnesses.map((witness) => nodeName(witness.binding.local)));
1619
- const builder = new VirtualSourceBuilder(source);
1620
- const pendingNames = new Set(witnessedBindings);
1621
- const renderDeclarations = (declarations) => {
1622
- for (const declaration of declarations) {
1623
- if (declaration.type !== "ImportDeclaration") continue;
1624
- const rendered = renderImport(declaration, pendingNames, source);
1625
- if (!rendered) continue;
1626
- builder.append(rendered.text);
1627
- for (const local of rendered.locals) pendingNames.delete(local);
1628
- }
1216
+ throw new AnimusConfigError(message);
1629
1217
  };
1630
- renderDeclarations(script.content.body);
1631
- if (inherited) renderDeclarations(inherited.importDeclarations);
1632
- for (const witness of witnesses) {
1633
- const resolverName = nodeName(witness.resolver);
1634
- builder.append("<");
1635
- builder.appendMapped(resolverName, "resolver", witness.resolver);
1636
- for (const property of witness.properties) {
1637
- builder.append(" ");
1638
- builder.appendMapped(property.name, "attribute", property.key);
1639
- builder.append("={");
1640
- builder.appendMapped(source.slice(property.value.start, property.value.end), "value", property.value);
1641
- builder.append("}");
1218
+ const own = new Set(ownKeys);
1219
+ for (const key of Object.keys(raw)) {
1220
+ if (raw[key] === void 0) continue;
1221
+ const rejected = rejectKeys.find((entry) => entry.key === key);
1222
+ if (rejected) {
1223
+ surface(`Option "${key}" is not supported by this driver: ${rejected.reason}`);
1224
+ continue;
1225
+ }
1226
+ if (CORE_OPTION_KEYS.has(key) || DRIVER_NAMESPACE_KEYS.has(key) || own.has(key)) continue;
1227
+ const nearest = suggestNearest(key);
1228
+ surface(`Unknown option "${key}".${nearest ? ` Did you mean "${nearest}"?` : ""} Core keys: ${[...CORE_OPTION_KEYS].join(", ")}. Driver namespaces: ${DRIVER_NAMESPACES.join(", ")}.`);
1229
+ }
1230
+ const mode = raw["mode"];
1231
+ if (mode !== void 0 && mode !== "development" && mode !== "production") throw new AnimusConfigError(`Invalid mode "${String(mode)}" — expected "development" or "production".`);
1232
+ for (const { key, ok, expected } of CORE_VALUE_GATES) {
1233
+ const value = raw[key];
1234
+ if (value === void 0 || ok(value)) continue;
1235
+ throw new AnimusConfigError(`Invalid value for "${key}" — expected ${expected}, got ${JSON.stringify(value)}.`);
1236
+ }
1237
+ }
1238
+ /**
1239
+ * Options are foreign JS, so a boxed `String`/`Boolean` must not pass: a
1240
+ * boxed value never behaves as the primitive its consumers expect.
1241
+ */
1242
+ const isString = (value) => Object(value) !== value && Object.prototype.toString.call(value) === "[object String]";
1243
+ const isBoolean = (value) => Object(value) !== value && Object.prototype.toString.call(value) === "[object Boolean]";
1244
+ const isStringArray = (value) => Array.isArray(value) && value.every(isString);
1245
+ const isStringOrStringArray = (value) => isString(value) || isStringArray(value);
1246
+ const CORE_VALUE_GATES = [
1247
+ {
1248
+ key: "system",
1249
+ ok: isString,
1250
+ expected: "a string path"
1251
+ },
1252
+ {
1253
+ key: "exclude",
1254
+ ok: isStringArray,
1255
+ expected: "an array of string patterns"
1256
+ },
1257
+ {
1258
+ key: "extensions",
1259
+ ok: isStringArray,
1260
+ expected: "an array of string extensions"
1261
+ },
1262
+ {
1263
+ key: "strict",
1264
+ ok: isBoolean,
1265
+ expected: "a boolean"
1266
+ },
1267
+ {
1268
+ key: "verbose",
1269
+ ok: isBoolean,
1270
+ expected: "a boolean"
1271
+ },
1272
+ {
1273
+ key: "minify",
1274
+ ok: isBoolean,
1275
+ expected: "a boolean"
1276
+ },
1277
+ {
1278
+ key: "prefix",
1279
+ ok: isString,
1280
+ expected: "a string"
1281
+ },
1282
+ {
1283
+ key: "targets",
1284
+ ok: isStringOrStringArray,
1285
+ expected: "a string or an array of strings"
1286
+ },
1287
+ {
1288
+ key: "layers",
1289
+ ok: isStringArray,
1290
+ expected: "an array of layer names"
1291
+ }
1292
+ ];
1293
+ function suggestNearest(key) {
1294
+ const lower = key.toLowerCase();
1295
+ let best = null;
1296
+ let bestScore = 3;
1297
+ for (const candidate of CORE_OPTION_KEYS) {
1298
+ const score = editDistance(lower, candidate.toLowerCase());
1299
+ if (score < bestScore) {
1300
+ bestScore = score;
1301
+ best = candidate;
1642
1302
  }
1643
- builder.append(" />;\n");
1644
1303
  }
1645
- return {
1646
- diagnostics: [],
1647
- entry: {
1648
- scope,
1649
- path: `${originalPath}.${scope}.tsx`,
1650
- source: builder.toString(),
1651
- mappings: builder.mappings
1652
- }
1304
+ return best;
1305
+ }
1306
+ function editDistance(a, b) {
1307
+ const dp = Array.from({ length: a.length + 1 }, (_, i) => [i]);
1308
+ for (let j = 1; j <= b.length; j++) dp[0][j] = j;
1309
+ for (let i = 1; i <= a.length; i++) for (let j = 1; j <= b.length; j++) dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
1310
+ return dp[a.length][b.length];
1311
+ }
1312
+ const GLOB_META = /[*?]/;
1313
+ /**
1314
+ * Globs compile to a RegExp over the root-relative path, matching the path or
1315
+ * any ancestor directory; plain patterns are substrings. `./` is stripped.
1316
+ */
1317
+ function compilePattern(raw) {
1318
+ let pattern = raw.split("\\").join("/");
1319
+ while (pattern.startsWith("./")) pattern = pattern.slice(2);
1320
+ if (pattern === "") return () => false;
1321
+ if (!GLOB_META.test(pattern)) return (full, rel) => full.includes(pattern) || rel.includes(pattern);
1322
+ const regex = globToRegExp(pattern);
1323
+ return (_full, rel) => {
1324
+ const normalized = rel.split("\\").join("/");
1325
+ if (regex.test(normalized)) return true;
1326
+ for (let slash = normalized.indexOf("/"); slash !== -1; slash = normalized.indexOf("/", slash + 1)) if (regex.test(normalized.slice(0, slash))) return true;
1327
+ return false;
1653
1328
  };
1654
1329
  }
1330
+ /** Minimal glob → RegExp: `**` spans segments, `*`/`?` stay within one. */
1331
+ function globToRegExp(glob) {
1332
+ let out = "";
1333
+ let i = 0;
1334
+ const normalized = glob.split("\\").join("/");
1335
+ while (i < normalized.length) {
1336
+ const ch = normalized[i];
1337
+ if (ch === "*") {
1338
+ if (normalized[i + 1] === "*") {
1339
+ if (normalized[i + 2] === "/") {
1340
+ out += "(?:[^/]+/)*";
1341
+ i += 3;
1342
+ } else {
1343
+ out += ".*";
1344
+ i += 2;
1345
+ }
1346
+ } else {
1347
+ out += "[^/]*";
1348
+ i += 1;
1349
+ }
1350
+ } else if (ch === "?") {
1351
+ out += "[^/]";
1352
+ i += 1;
1353
+ } else {
1354
+ out += ch.replace(/[.+^${}()|[\]\\]/g, "\\$&");
1355
+ i += 1;
1356
+ }
1357
+ }
1358
+ return new RegExp(`^${out}$`);
1359
+ }
1655
1360
  /**
1656
- * Project native Svelte script scopes into parser-only TSX witnesses.
1657
- *
1658
- * This adapter parses source, never compiler-generated runtime JavaScript.
1659
- * Module and instance scripts project separately (module bindings are in
1660
- * scope for the instance script, per Svelte semantics) and contribute only
1661
- * witnessed named imports plus synthetic JSX attributes for direct resolver
1662
- * `.attrs()` and callable-string calls. Caller-owned metadata attributes
1663
- * candidate imports; unsupported call shapes — including resolver calls
1664
- * written in the template fragment — fail the whole source closed.
1361
+ * A user list replaces only the replaceable defaults. Driver-owned
1362
+ * invariants belong in `extraStructural`; a user list would replace those.
1665
1363
  */
1666
- async function adaptSvelteSource(source, originalPath, options) {
1667
- const original = {
1668
- path: originalPath,
1669
- hash: contentHash(source)
1364
+ function createExcludeMatcher(userPatterns, extraStructural = []) {
1365
+ const merged = [...STRUCTURAL_EXCLUDE];
1366
+ for (const pattern of extraStructural) if (!merged.includes(pattern)) merged.push(pattern);
1367
+ for (const pattern of userPatterns ?? REPLACEABLE_DEFAULT_EXCLUDE) if (!merged.includes(pattern)) merged.push(pattern);
1368
+ const compiled = merged.map((pattern) => [pattern, compilePattern(pattern)]);
1369
+ const hits = new Map(merged.map((p) => [p, 0]));
1370
+ const explain = (full, rel) => {
1371
+ for (const [pattern, test] of compiled) if (test(full, rel)) {
1372
+ hits.set(pattern, (hits.get(pattern) ?? 0) + 1);
1373
+ return pattern;
1374
+ }
1375
+ return null;
1670
1376
  };
1671
- const compiler = await loadCompiler();
1672
- if (!compiler) return {
1673
- kind: "missing-dep",
1674
- original,
1675
- dependency: "svelte/compiler"
1377
+ return {
1378
+ patterns: merged,
1379
+ matches: (full, rel) => explain(full, rel) !== null,
1380
+ explain,
1381
+ stats: () => hits
1676
1382
  };
1677
- let ast;
1678
- try {
1679
- ast = compiler.parse(source, {
1680
- filename: originalPath,
1681
- modern: true
1682
- });
1683
- } catch (error) {
1684
- const range = compilerParseErrorRange(error, source);
1685
- return {
1686
- kind: "error",
1687
- original,
1688
- diagnostics: [diagnostic("SVELTE_PARSE_ERROR", error instanceof Error ? error.message : String(error), originalPath, source, range?.node, range?.location)]
1689
- };
1690
- }
1691
- const projections = [];
1692
- const moduleBindings = ast.module ? importBindings(ast.module.content) : void 0;
1693
- const instanceBindings = ast.instance ? importBindings(ast.instance.content) : void 0;
1694
- const moduleScope = ast.module && moduleBindings ? {
1695
- bindings: moduleBindings,
1696
- importDeclarations: ast.module.content.body.filter((declaration) => declaration.type === "ImportDeclaration")
1697
- } : void 0;
1698
- if (ast.module) projections.push(projectScope(ast.module, "module", source, originalPath, options, void 0, moduleBindings));
1699
- if (ast.instance) projections.push(projectScope(ast.instance, "instance", source, originalPath, options, moduleScope, instanceBindings));
1700
- const diagnostics = projections.flatMap((projection) => projection.diagnostics);
1701
- if (ast.fragment) {
1702
- const scriptBindings = instanceBindings ?? /* @__PURE__ */ new Map();
1703
- const fragmentBindings = moduleScope ? new Map([...moduleScope.bindings, ...scriptBindings]) : scriptBindings;
1704
- if (fragmentBindings.size > 0) walkFragment(ast.fragment, (node) => {
1705
- const site = resolverCallSite(node, fragmentBindings);
1706
- if (!site) return;
1707
- if (options.attributeResolver(site.access.request) === "other") return;
1708
- diagnostics.push(diagnostic("SVELTE_ATTRS_TEMPLATE_UNSUPPORTED", `Resolver '${site.access.displayName}' is called inside the template; assign the result to a const in the instance script and spread that instead.`, originalPath, source, node));
1709
- });
1710
- }
1711
- if (diagnostics.length > 0) return {
1712
- kind: "error",
1713
- original,
1714
- diagnostics
1383
+ }
1384
+ function resolveMode(explicit, driverDefault) {
1385
+ if (explicit === "development" || explicit === "production") return {
1386
+ mode: explicit,
1387
+ provenance: "explicit"
1715
1388
  };
1716
1389
  return {
1717
- kind: "ok",
1718
- original,
1719
- entries: projections.flatMap((projection) => projection.entry ? [projection.entry] : [])
1390
+ mode: driverDefault(),
1391
+ provenance: "driver-default"
1720
1392
  };
1721
1393
  }
1722
1394
  //#endregion
1723
- //#region pipeline/mdx-preprocessor.ts
1395
+ //#region pipeline/discover-files.ts
1396
+ function pathCharRank(code) {
1397
+ return code === 47 || code === 92 ? -1 : code;
1398
+ }
1399
+ function compareDiscoveryOrder(a, b) {
1400
+ const shared = Math.min(a.length, b.length);
1401
+ for (let i = 0; i < shared; i++) {
1402
+ const left = pathCharRank(a.charCodeAt(i));
1403
+ const right = pathCharRank(b.charCodeAt(i));
1404
+ if (left !== right) return left - right;
1405
+ }
1406
+ return a.length - b.length;
1407
+ }
1408
+ function discoverFiles(dir, rootDir, exclude, extensionsSet) {
1409
+ return walkFiles(dir, rootDir, exclude, extensionsSet).sort(compareDiscoveryOrder);
1410
+ }
1411
+ function walkFiles(dir, rootDir, exclude, extensionsSet) {
1412
+ const results = [];
1413
+ let entries;
1414
+ try {
1415
+ entries = readdirSync(dir, { encoding: "utf8" });
1416
+ } catch {
1417
+ return results;
1418
+ }
1419
+ const isExcluded = exclude ? (full, rel) => exclude.matches(full, rel) : () => false;
1420
+ for (const entry of entries) {
1421
+ const fullPath = join$1(dir, entry);
1422
+ if (isExcluded(fullPath, relative(rootDir, fullPath))) continue;
1423
+ let stat;
1424
+ try {
1425
+ stat = statSync(fullPath);
1426
+ } catch {
1427
+ continue;
1428
+ }
1429
+ if (stat.isDirectory()) results.push(...walkFiles(fullPath, rootDir, exclude, extensionsSet));
1430
+ else if (extensionsSet.has(extname(entry))) results.push(fullPath);
1431
+ }
1432
+ return results;
1433
+ }
1434
+ //#endregion
1435
+ //#region pipeline/source-identity.ts
1724
1436
  /**
1725
- * MDX source preprocessor for the extraction pipeline.
1726
- *
1727
- * MDX files are first-class consumers of ds-built components but are not
1728
- * directly parseable by OXC (which the scanner uses for .tsx/.jsx). This
1729
- * module compiles MDX sources to scanner-consumable JSX using @mdx-js/mdx.
1730
- *
1731
- * @mdx-js/mdx is declared as `peerDependenciesMeta.optional` on the
1732
- * plugin packages that import this module. The dynamic import with
1733
- * .catch() ensures non-MDX consumers (who configure `extensions` to
1734
- * exclude .mdx) never trigger the resolution — zero install-footprint
1735
- * cost for them.
1736
- *
1737
- * The `DEFAULT_EXTENSIONS` constant is the shared source of truth for
1738
- * both `@animus-ui/vite-plugin` and `@animus-ui/next-plugin`. Each plugin
1739
- * imports it directly; independent redeclaration of default extensions
1740
- * is considered a regression.
1437
+ * One derivation of source identity for discovery, watch ingestion, ownership
1438
+ * and deletion: event paths are lookup INPUTS only and never fork identity.
1741
1439
  */
1742
- const DEFAULT_EXTENSIONS = [
1743
- ".ts",
1744
- ".tsx",
1745
- ".js",
1746
- ".jsx",
1747
- ".mdx"
1748
- ];
1749
1440
  /**
1750
- * Extensions the ENGINE TRANSFORM may rewrite (distinct from
1751
- * `DEFAULT_EXTENSIONS`, the discovery set: `.mjs` is here so dist-entry
1752
- * kits reach the engine, `.mdx` is not — MDX is preprocessed to `.tsx`
1753
- * before the engine sees it). The single source for the Turbopack rule
1754
- * glob and the unplugin transform gate; independent redeclaration of this
1755
- * set is considered a regression — a missed copy silently skips a whole
1756
- * file class on one bundler family.
1441
+ * True when `target` is `root` or a descendant, decided by `relative()`
1442
+ * shape: string prefixing would let `/ui` claim `/ui-old`.
1757
1443
  */
1758
- const ENGINE_TRANSFORM_EXTENSIONS = [
1759
- "ts",
1760
- "tsx",
1761
- "js",
1762
- "jsx",
1763
- "mjs"
1764
- ];
1444
+ function isPathWithinRoot(root, target, pathApi = path) {
1445
+ const rel = pathApi.relative(root, target);
1446
+ return rel === "" || !pathApi.isAbsolute(rel) && rel !== ".." && !rel.startsWith(".." + pathApi.sep);
1447
+ }
1765
1448
  /**
1766
- * Preprocess an MDX source string into scanner-consumable JSX.
1767
- *
1768
- * - Returns `{ kind: 'ok', source }` with JSX-compiled output on success.
1769
- * - Returns `{ kind: 'missing-dep' }` if @mdx-js/mdx is not resolvable
1770
- * (consumer needs to install it).
1771
- * - Returns `{ kind: 'error', error }` on compile failure (e.g. malformed
1772
- * MDX syntax). Plugins SHALL warn + skip affected files; the build
1773
- * continues with remaining files.
1449
+ * True when two paths share a platform root (win32 drive letter or UNC
1450
+ * share; always true on posix). Drive letters compare case-insensitively.
1774
1451
  */
1775
- async function preprocessMdx(source, filename) {
1776
- const mdxMod = await import("@mdx-js/mdx").catch(() => null);
1777
- if (mdxMod === null) return { kind: "missing-dep" };
1452
+ function sharesVolumeRoot(a, b, pathApi = path) {
1453
+ const volumeOf = (p) => pathApi.parse(pathApi.resolve(p)).root.toLowerCase();
1454
+ return volumeOf(a) === volumeOf(b);
1455
+ }
1456
+ function canonicalize(p) {
1778
1457
  try {
1779
- const vfile = await mdxMod.compile(source, {
1780
- outputFormat: "program",
1781
- development: false,
1782
- jsx: true
1783
- });
1784
- return {
1785
- kind: "ok",
1786
- source: `/* @mdx-source: ${filename} */\n${String(vfile)}`
1787
- };
1788
- } catch (err) {
1789
- return {
1790
- kind: "error",
1791
- error: err instanceof Error ? err.message : String(err)
1792
- };
1458
+ return realpathSync$1(p);
1459
+ } catch {
1460
+ return null;
1793
1461
  }
1794
1462
  }
1463
+ function createSourceIdentity(rootDir) {
1464
+ const normalizedRootDir = path.normalize(rootDir);
1465
+ const canonicalRootDir = canonicalize(normalizedRootDir) ?? normalizedRootDir;
1466
+ const roots = [];
1467
+ const aliases = /* @__PURE__ */ new Map();
1468
+ const owningRecord = (canonicalPath) => {
1469
+ let owner = null;
1470
+ for (const record of roots) {
1471
+ if (!isPathWithinRoot(record.canonical, canonicalPath)) continue;
1472
+ if (!owner || record.canonical.length > owner.canonical.length) owner = record;
1473
+ }
1474
+ return owner;
1475
+ };
1476
+ return {
1477
+ rootDir: normalizedRootDir,
1478
+ canonicalRootDir,
1479
+ registerExternalRoot(root) {
1480
+ const asRegistered = path.normalize(root);
1481
+ const canonical = canonicalize(asRegistered) ?? asRegistered;
1482
+ const existing = roots.find((record) => record.canonical === canonical);
1483
+ if (existing) return existing.canonical;
1484
+ roots.push({
1485
+ asRegistered,
1486
+ canonical
1487
+ });
1488
+ return canonical;
1489
+ },
1490
+ externalRoots() {
1491
+ return roots.map((record) => record.canonical);
1492
+ },
1493
+ resolveSourceId(inputPath) {
1494
+ const lexical = path.normalize(inputPath);
1495
+ const canonical = canonicalize(lexical);
1496
+ if (canonical === null) return null;
1497
+ const owner = owningRecord(canonical);
1498
+ const ownedBy = (record) => {
1499
+ const pathInRoot = path.relative(record.canonical, canonical);
1500
+ return {
1501
+ sourceKey: path.relative(normalizedRootDir, path.join(record.asRegistered, pathInRoot)),
1502
+ owningRoot: record.canonical,
1503
+ pathInRoot
1504
+ };
1505
+ };
1506
+ const ownerIsMoreSpecific = owner !== null && owner.canonical.length > canonicalRootDir.length;
1507
+ let resolved;
1508
+ if (!ownerIsMoreSpecific && isPathWithinRoot(canonicalRootDir, canonical)) {
1509
+ const key = path.relative(canonicalRootDir, canonical);
1510
+ resolved = {
1511
+ sourceKey: key,
1512
+ owningRoot: null,
1513
+ pathInRoot: key
1514
+ };
1515
+ } else if (owner) resolved = ownedBy(owner);
1516
+ else return null;
1517
+ aliases.set(lexical, resolved);
1518
+ aliases.set(canonical, resolved);
1519
+ return resolved;
1520
+ },
1521
+ resolveDeletedSourceId(inputPath) {
1522
+ return aliases.get(path.normalize(inputPath)) ?? null;
1523
+ },
1524
+ containingExternalRoot(inputPath) {
1525
+ const lexical = path.normalize(inputPath);
1526
+ let owner = null;
1527
+ for (const record of roots) {
1528
+ if (!(isPathWithinRoot(record.canonical, lexical) || isPathWithinRoot(record.asRegistered, lexical))) continue;
1529
+ if (!owner || record.canonical.length > owner.canonical.length) owner = record;
1530
+ }
1531
+ if (owner) return owner.canonical;
1532
+ const canonical = canonicalize(lexical);
1533
+ if (canonical === null) return null;
1534
+ for (const record of roots) {
1535
+ if (!isPathWithinRoot(record.canonical, canonical)) continue;
1536
+ if (!owner || record.canonical.length > owner.canonical.length) owner = record;
1537
+ }
1538
+ return owner?.canonical ?? null;
1539
+ }
1540
+ };
1541
+ }
1795
1542
  //#endregion
1796
- //#region pipeline/source-ingestion.ts
1797
- /**
1798
- * Advisory diagnostics surface as warnings in EVERY mode and never
1799
- * quarantine their file. OXC reports recovered parse diagnostics for
1800
- * sources the consumer's own toolchain may accept (`.js` now parses
1801
- * JSX-enabled, so genuinely malformed code is the remaining producer),
1802
- * and the engine still analyzes whatever the recovered AST carries —
1803
- * failing the build for that would make extraction stricter than the
1804
- * host bundler, which surfaces its own error for the same file. Everything
1805
- * else stays fatal: strict throws, non-strict warns and quarantines.
1806
- */
1807
- function isAdvisorySourceDiagnostic(diagnostic) {
1808
- return diagnostic.code === "SOURCE_NATIVE_PARSE_ERROR";
1543
+ //#region pipeline/discover-packages.ts
1544
+ function findPackageRoot(absEntryPath) {
1545
+ let pkgRoot = dirname$1(absEntryPath);
1546
+ while (pkgRoot !== dirname$1(pkgRoot) && !existsSync(join$1(pkgRoot, "package.json"))) pkgRoot = dirname$1(pkgRoot);
1547
+ return pkgRoot;
1809
1548
  }
1810
- const RELATIVE_PROBE_SUFFIXES = [
1811
- "",
1812
- ".ts",
1813
- ".tsx",
1814
- ".js",
1815
- ".jsx",
1816
- "/index.ts",
1817
- "/index.tsx",
1818
- "/index.js",
1819
- "/index.jsx"
1549
+ const PACKAGE_SRC_EXCLUDES = [
1550
+ "dist",
1551
+ "node_modules",
1552
+ ".test.",
1553
+ ".spec."
1820
1554
  ];
1821
- function extension(path) {
1822
- const basename = posix.basename(path);
1823
- const dot = basename.lastIndexOf(".");
1824
- return dot === -1 ? "" : basename.slice(dot).toLowerCase();
1555
+ function isExcludedPackageRelativePath(rel) {
1556
+ return PACKAGE_SRC_EXCLUDES.some((pattern) => rel.includes(pattern));
1825
1557
  }
1826
- function canonicalResolverPath(path) {
1827
- return posix.normalize(path.replaceAll("\\", "/"));
1558
+ function walkPackageSources(packageDir, extensionsSet) {
1559
+ return discoverFiles(packageDir, packageDir, void 0, extensionsSet).filter((absPath) => !isExcludedPackageRelativePath(relative(packageDir, absPath)));
1828
1560
  }
1829
- /** NodeNext-style relative specifiers carry the EMITTED extension
1830
- * (`./definition.js` for `definition.ts`); map each back to its source
1831
- * forms. The exact spelling is probed first by the suffix loop's empty
1832
- * suffix, so a literal `.js` neighbor still wins. */
1833
- const NODE_NEXT_EXTENSION_MAP = {
1834
- ".js": [
1835
- ".ts",
1836
- ".tsx",
1837
- ".jsx"
1838
- ],
1839
- ".mjs": [".mts"],
1840
- ".cjs": [".cts"]
1561
+ const PACKAGE_OUTPUT_EXCLUDES = [
1562
+ "node_modules",
1563
+ ".test.",
1564
+ ".spec.",
1565
+ ".d.ts",
1566
+ ".map"
1567
+ ];
1568
+ const isFile = (path) => {
1569
+ try {
1570
+ return statSync(path).isFile();
1571
+ } catch {
1572
+ return false;
1573
+ }
1841
1574
  };
1842
- function resolveRelativeSource(importerPath, specifier, files) {
1843
- if (!specifier.startsWith(".")) return null;
1844
- const base = posix.normalize(posix.join(posix.dirname(canonicalResolverPath(importerPath)), specifier));
1845
- for (const suffix of RELATIVE_PROBE_SUFFIXES) {
1846
- const candidate = `${base}${suffix}`;
1847
- const actualPath = files.get(candidate);
1848
- if (actualPath !== void 0) return actualPath;
1575
+ function resolveAbsolutePathSpecifier(absSpecifier, extensionsSet) {
1576
+ return [
1577
+ absSpecifier,
1578
+ ...Array.from(extensionsSet, (ext) => absSpecifier + ext),
1579
+ ...Array.from(extensionsSet, (ext) => join$1(absSpecifier, `index${ext}`))
1580
+ ].find(isFile) ?? null;
1581
+ }
1582
+ function bareSpecifierPackageName(specifier) {
1583
+ const segments = specifier.split("/");
1584
+ return specifier.startsWith("@") ? segments.slice(0, 2).join("/") : segments[0];
1585
+ }
1586
+ function sourceEntryForSpecifier(specifier, srcDir, extensionsSet) {
1587
+ if (isAbsolute(specifier)) {
1588
+ const resolved = resolveAbsolutePathSpecifier(specifier, extensionsSet);
1589
+ if (!resolved) return null;
1590
+ return isPathWithinRoot(srcDir, resolved) ? resolved : null;
1849
1591
  }
1850
- const explicitExtension = extension(base);
1851
- for (const sourceExtension of NODE_NEXT_EXTENSION_MAP[explicitExtension] ?? []) {
1852
- const candidate = `${base.slice(0, -explicitExtension.length)}${sourceExtension}`;
1853
- const actualPath = files.get(candidate);
1854
- if (actualPath !== void 0) return actualPath;
1592
+ const packageName = bareSpecifierPackageName(specifier);
1593
+ const subpath = specifier.slice(packageName.length + 1);
1594
+ return resolveAbsolutePathSpecifier(join$1(srcDir, subpath || "index"), extensionsSet);
1595
+ }
1596
+ function distEntryIsStale(absEntry, srcDir, srcFiles) {
1597
+ if (isPathWithinRoot(srcDir, absEntry)) return false;
1598
+ let distMtime;
1599
+ try {
1600
+ distMtime = statSync(absEntry).mtimeMs;
1601
+ } catch {
1602
+ return false;
1855
1603
  }
1856
- return null;
1604
+ let newestSrcMtime = -Infinity;
1605
+ for (const srcFile of srcFiles) try {
1606
+ newestSrcMtime = Math.max(newestSrcMtime, statSync(srcFile).mtimeMs);
1607
+ } catch {}
1608
+ return distMtime < newestSrcMtime;
1857
1609
  }
1858
- var ResolverExportIndex = class {
1859
- collisions;
1860
- files;
1861
- resolverBindings = /* @__PURE__ */ new Map();
1862
- /** The same (importer, request) pair repeats for every call site of one
1863
- * resolver in one file; the index is rebuilt per ingest, so neither memo
1864
- * needs invalidation. */
1865
- attributionMemo = /* @__PURE__ */ new Map();
1866
- resolveMemo = /* @__PURE__ */ new Map();
1867
- constructor(facts, analysisPaths) {
1868
- this.facts = facts;
1869
- const pathsByCanonical = /* @__PURE__ */ new Map();
1870
- for (const path of analysisPaths) {
1871
- const canonicalPath = canonicalResolverPath(path);
1872
- const paths = pathsByCanonical.get(canonicalPath) ?? [];
1873
- paths.push(path);
1874
- pathsByCanonical.set(canonicalPath, paths);
1610
+ async function collectExternalPackageSources(opts) {
1611
+ const { specifiers, resolveSpecifier, rootDir, extensionsSet, hasEntry, onSourceRead, onUnreadable, onPackageResolved } = opts;
1612
+ const entries = [];
1613
+ const pushed = /* @__PURE__ */ new Set();
1614
+ const packageMap = {};
1615
+ const sourceEntries = /* @__PURE__ */ new Map();
1616
+ const packageDirs = [];
1617
+ const dirOwnerSets = {};
1618
+ const dirExtensions = {};
1619
+ const fileOwners = {};
1620
+ const outcomes = [];
1621
+ const claimDir = (dir, specifier) => {
1622
+ (dirOwnerSets[dir] ??= []).push(specifier);
1623
+ };
1624
+ const alreadyIngested = (relPath) => hasEntry(relPath) || pushed.has(relPath);
1625
+ for (const specifier of specifiers) {
1626
+ let absEntry;
1627
+ try {
1628
+ absEntry = await resolveSpecifier(specifier);
1629
+ } catch {
1630
+ absEntry = null;
1875
1631
  }
1876
- this.collisions = [...pathsByCanonical].filter(([, paths]) => paths.length > 1).map(([canonicalPath, paths]) => ({
1877
- canonicalPath,
1878
- paths
1879
- }));
1880
- const factPathsByCanonical = /* @__PURE__ */ new Map();
1881
- for (const path of Object.keys(facts.files)) {
1882
- const canonicalPath = canonicalResolverPath(path);
1883
- const paths = factPathsByCanonical.get(canonicalPath) ?? [];
1884
- paths.push(path);
1885
- factPathsByCanonical.set(canonicalPath, paths);
1632
+ if (!absEntry && isAbsolute(specifier)) absEntry = resolveAbsolutePathSpecifier(specifier, extensionsSet);
1633
+ if (!absEntry) {
1634
+ outcomes.push({
1635
+ specifier,
1636
+ outcome: "unresolvable",
1637
+ fileCount: 0
1638
+ });
1639
+ continue;
1886
1640
  }
1887
- const files = /* @__PURE__ */ new Map();
1888
- for (const [canonicalPath, paths] of pathsByCanonical) {
1889
- const factPaths = factPathsByCanonical.get(canonicalPath) ?? [];
1890
- if (paths.length === 1 && factPaths.length === 1) files.set(canonicalPath, factPaths[0]);
1641
+ const pkgRoot = findPackageRoot(absEntry);
1642
+ const srcDir = join$1(pkgRoot, "src");
1643
+ let fileCount = 0;
1644
+ let staleDist = false;
1645
+ if (existsSync(srcDir)) {
1646
+ packageDirs.push(srcDir);
1647
+ claimDir(srcDir, specifier);
1648
+ dirExtensions[srcDir] = [...extensionsSet];
1649
+ onPackageResolved?.(specifier, srcDir);
1650
+ const srcEntry = sourceEntryForSpecifier(specifier, srcDir, extensionsSet);
1651
+ if (srcEntry) {
1652
+ packageMap[specifier] = relative(rootDir, srcEntry);
1653
+ sourceEntries.set(specifier, srcEntry);
1654
+ } else packageMap[specifier] = relative(rootDir, absEntry);
1655
+ if (!isAbsolute(specifier)) {
1656
+ const packageName = bareSpecifierPackageName(specifier);
1657
+ if (packageName !== specifier && !(packageName in packageMap)) {
1658
+ const rootEntry = sourceEntryForSpecifier(packageName, srcDir, extensionsSet);
1659
+ if (rootEntry) {
1660
+ packageMap[packageName] = relative(rootDir, rootEntry);
1661
+ sourceEntries.set(packageName, rootEntry);
1662
+ }
1663
+ }
1664
+ }
1665
+ const pkgFiles = walkPackageSources(srcDir, extensionsSet);
1666
+ staleDist = distEntryIsStale(absEntry, srcDir, pkgFiles);
1667
+ for (const pkgFile of pkgFiles) {
1668
+ const relPath = relative(rootDir, pkgFile);
1669
+ if (alreadyIngested(relPath)) {
1670
+ fileCount++;
1671
+ continue;
1672
+ }
1673
+ let source;
1674
+ try {
1675
+ source = readFileSync(pkgFile, "utf-8");
1676
+ } catch (err) {
1677
+ onUnreadable(relPath, err);
1678
+ continue;
1679
+ }
1680
+ onSourceRead?.(source, relPath, pkgFile);
1681
+ entries.push({
1682
+ path: relPath,
1683
+ source
1684
+ });
1685
+ pushed.add(relPath);
1686
+ fileOwners[relPath] ??= specifier;
1687
+ fileCount++;
1688
+ }
1689
+ } else {
1690
+ const outputDir = dirname$1(absEntry);
1691
+ packageDirs.push(outputDir);
1692
+ claimDir(outputDir, specifier);
1693
+ onPackageResolved?.(specifier, outputDir);
1694
+ packageMap[specifier] = relative(rootDir, absEntry);
1695
+ const outputExtensions = new Set(extensionsSet);
1696
+ outputExtensions.add(extname(absEntry));
1697
+ dirExtensions[outputDir] = [...outputExtensions];
1698
+ const outputFiles = discoverFiles(outputDir, outputDir, void 0, outputExtensions).filter((file) => {
1699
+ const relToOutput = relative(outputDir, file);
1700
+ return !PACKAGE_OUTPUT_EXCLUDES.some((pattern) => relToOutput.includes(pattern));
1701
+ });
1702
+ if (!outputFiles.includes(absEntry)) outputFiles.unshift(absEntry);
1703
+ for (const outputFile of outputFiles) {
1704
+ const outputRelPath = relative(rootDir, outputFile);
1705
+ if (alreadyIngested(outputRelPath)) {
1706
+ fileCount++;
1707
+ continue;
1708
+ }
1709
+ let source;
1710
+ try {
1711
+ source = readFileSync(outputFile, "utf-8");
1712
+ } catch (err) {
1713
+ onUnreadable(outputRelPath, err);
1714
+ continue;
1715
+ }
1716
+ onSourceRead?.(source, outputRelPath, outputFile);
1717
+ entries.push({
1718
+ path: outputRelPath,
1719
+ source
1720
+ });
1721
+ pushed.add(outputRelPath);
1722
+ fileOwners[outputRelPath] ??= specifier;
1723
+ fileCount++;
1724
+ }
1891
1725
  }
1892
- this.files = files;
1893
- for (const [path, file] of Object.entries(facts.files)) this.resolverBindings.set(path, new Set(file.chains.filter((chain) => chain.descriptor.terminal === "asClass" && chain.descriptor.extractable && chain.fatalError === null).map((chain) => chain.descriptor.binding)));
1726
+ outcomes.push({
1727
+ specifier,
1728
+ outcome: staleDist ? "stale-dist" : fileCount > 0 ? "resolved" : "empty",
1729
+ fileCount
1730
+ });
1894
1731
  }
1895
- attribute(importerPath, request) {
1896
- const memoKey = `${importerPath}\0${request.source}\0${request.imported}\0${request.access.kind}\0${request.access.importKind}`;
1897
- const memoized = this.attributionMemo.get(memoKey);
1898
- if (memoized !== void 0) return memoized;
1899
- const attribution = this.attributeUncached(importerPath, request);
1900
- this.attributionMemo.set(memoKey, attribution);
1901
- return attribution;
1732
+ return {
1733
+ entries,
1734
+ packageMap,
1735
+ sourceEntries,
1736
+ packageDirs,
1737
+ dirOwnerSets,
1738
+ dirExtensions,
1739
+ fileOwners,
1740
+ outcomes
1741
+ };
1742
+ }
1743
+ function firstOwners(dirOwnerSets) {
1744
+ const owners = {};
1745
+ for (const [dir, specifiers] of Object.entries(dirOwnerSets)) if (specifiers.length > 0) owners[dir] = specifiers[0];
1746
+ return owners;
1747
+ }
1748
+ function excludeCollectedPackages(collected, rejectedSpecifiers, rootDir) {
1749
+ if (rejectedSpecifiers.size === 0) return collected;
1750
+ const rejectedDirs = Object.entries(collected.dirOwnerSets).filter(([, specs]) => specs.every((s) => rejectedSpecifiers.has(s))).map(([dir]) => dir);
1751
+ const underRejectedDir = (absPath) => rejectedDirs.some((dir) => isPathWithinRoot(dir, absPath));
1752
+ const targetRejected = (specifier, absTarget) => rejectedSpecifiers.has(specifier) || underRejectedDir(absTarget);
1753
+ const packageMap = {};
1754
+ for (const [specifier, relTarget] of Object.entries(collected.packageMap)) {
1755
+ if (targetRejected(specifier, resolve(rootDir, relTarget))) continue;
1756
+ packageMap[specifier] = relTarget;
1902
1757
  }
1903
- attributeUncached(importerPath, request) {
1904
- const importedFile = this.resolveSource(importerPath, request.source);
1905
- if (!importedFile) return "other";
1906
- if (this.resolveClassExport(importedFile, request.imported, /* @__PURE__ */ new Set()) === null) return "other";
1907
- return request.access.kind === "direct" && request.access.importKind === "named" ? "resolver" : "unsupported-resolver-form";
1758
+ const sourceEntries = /* @__PURE__ */ new Map();
1759
+ for (const [specifier, absEntry] of collected.sourceEntries) {
1760
+ if (targetRejected(specifier, absEntry)) continue;
1761
+ sourceEntries.set(specifier, absEntry);
1908
1762
  }
1909
- resolveSource(importerPath, specifier) {
1910
- const key = `${importerPath}\0${specifier}`;
1911
- let resolved = this.resolveMemo.get(key);
1912
- if (resolved === void 0) {
1913
- resolved = resolveRelativeSource(importerPath, specifier, this.files);
1914
- this.resolveMemo.set(key, resolved);
1915
- }
1916
- return resolved;
1763
+ const dirOwnerSets = {};
1764
+ for (const [dir, specs] of Object.entries(collected.dirOwnerSets)) {
1765
+ const kept = specs.filter((s) => !rejectedSpecifiers.has(s));
1766
+ if (kept.length === 0) continue;
1767
+ dirOwnerSets[dir] = kept;
1917
1768
  }
1918
- resolveClassExport(path, exportedName, seen) {
1919
- const identity = `${path}\0${exportedName}`;
1920
- if (seen.has(identity)) return null;
1921
- seen.add(identity);
1922
- const file = this.facts.files[path];
1923
- const exported = file?.exports.find((candidate) => candidate.exported === exportedName);
1924
- if (!file || !exported) return null;
1925
- if (exported.source === null) {
1926
- if (exported.local === null) return null;
1927
- if (this.resolverBindings.get(path)?.has(exported.local)) return exported.local;
1928
- const imported = file.imports.find((candidate) => candidate.local === exported.local);
1929
- if (!imported) return null;
1930
- const importedFile = this.resolveSource(path, imported.source);
1931
- return importedFile ? this.resolveClassExport(importedFile, imported.imported, seen) : null;
1932
- }
1933
- if (exported.original === null) return null;
1934
- const nextFile = this.resolveSource(path, exported.source);
1935
- return nextFile ? this.resolveClassExport(nextFile, exported.original, seen) : null;
1769
+ const dirExtensions = {};
1770
+ for (const [dir, exts] of Object.entries(collected.dirExtensions)) {
1771
+ if (rejectedDirs.includes(dir)) continue;
1772
+ dirExtensions[dir] = exts;
1936
1773
  }
1937
- };
1938
- /**
1939
- * Convert raw source identities into parser-ready analysis entries once.
1940
- *
1941
- * Native and MDX entries establish the resolver-export index through the
1942
- * native fact collector before any Svelte projection. Svelte calls are then
1943
- * attributed by exact relative import/export identity and parsed once. Bare
1944
- * package and configured-alias sources deliberately remain `other`; callers
1945
- * must not guess those identities.
1946
- */
1947
- async function ingestSourceEntries(rawEntries, options) {
1948
- const mdxAdapter = options.preprocessMdx ?? preprocessMdx;
1949
- const svelteAdapter = options.adaptSvelte ?? adaptSvelteSource;
1950
- const originalEntries = rawEntries.map((entry) => ({
1951
- ...entry,
1952
- hash: entry.hash ?? contentHash(entry.source)
1953
- }));
1954
- const rawOriginalPaths = new Set(originalEntries.map((entry) => entry.path));
1955
- const analysisEntries = [];
1956
- const ownership = {};
1957
- const analysisOwner = /* @__PURE__ */ new Map();
1958
- const diagnostics = [];
1959
- const svelteEntries = [];
1960
- const addAnalysisEntry = (originalPath, path, source, hash) => {
1961
- analysisEntries.push({
1962
- path,
1963
- source,
1964
- hash: hash ?? contentHash(source)
1965
- });
1966
- ownership[originalPath].analysisPaths.push(path);
1967
- analysisOwner.set(path, originalPath);
1774
+ const fileOwners = {};
1775
+ for (const [relPath, owner] of Object.entries(collected.fileOwners)) {
1776
+ if (rejectedSpecifiers.has(owner)) continue;
1777
+ fileOwners[relPath] = owner;
1778
+ }
1779
+ return {
1780
+ entries: collected.entries.filter((entry) => !rejectedSpecifiers.has(collected.fileOwners[entry.path])),
1781
+ packageMap,
1782
+ sourceEntries,
1783
+ packageDirs: collected.packageDirs.filter((dir) => !rejectedDirs.includes(dir)),
1784
+ dirOwnerSets,
1785
+ dirExtensions,
1786
+ fileOwners,
1787
+ outcomes: collected.outcomes
1968
1788
  };
1969
- const addAdaptedEntries = (originalPath, entries) => {
1970
- const pendingPaths = /* @__PURE__ */ new Set();
1971
- for (const entry of entries) {
1972
- const conflictingOriginalPath = rawOriginalPaths.has(entry.path) ? entry.path : analysisOwner.get(entry.path) ?? (pendingPaths.has(entry.path) ? originalPath : null);
1973
- if (conflictingOriginalPath !== null) {
1974
- diagnostics.push({
1975
- code: "SOURCE_ANALYSIS_PATH_COLLISION",
1976
- message: `Generated analysis path '${entry.path}' collides with '${conflictingOriginalPath}'. Rename one of the source files so adapted analysis paths remain unique.`,
1977
- originalPath,
1978
- analysisPath: entry.path,
1979
- conflictingOriginalPath
1980
- });
1981
- return;
1789
+ }
1790
+ function unresolvableIncludesMessage(outcomes) {
1791
+ const unresolvable = outcomes.filter((record) => record.outcome === "unresolvable").map((record) => record.specifier);
1792
+ if (unresolvable.length === 0) return null;
1793
+ return `[animus-extract] unresolvable include specifier(s): ${unresolvable.join(", ")}`;
1794
+ }
1795
+ /** A stale dist silently skews merged registry content while discovery
1796
+ * compiles the fresh sources; null when no specifier is stale. */
1797
+ function staleDistIncludesMessage(outcomes) {
1798
+ const stale = outcomes.filter((record) => record.outcome === "stale-dist").map((record) => record.specifier);
1799
+ if (stale.length === 0) return null;
1800
+ return `[animus-extract] stale dist for include specifier(s): ${stale.join(", ")} — dist entry is older than the newest src/ file; rebuild the package(s) before extracting`;
1801
+ }
1802
+ function extractSystemFilePackages(systemFilePath) {
1803
+ let source;
1804
+ try {
1805
+ source = readFileSync(systemFilePath, "utf-8");
1806
+ } catch {
1807
+ return [];
1808
+ }
1809
+ const identifiers = /* @__PURE__ */ new Set();
1810
+ const constructorRegex = /createSystem\s*\(\s*\{[^}]*?\bincludes\s*:\s*\[([^\]]*)\]/gs;
1811
+ const chainRegex = /\.includes\s*\(\s*\[([^\]]*)\]\s*\)/gs;
1812
+ const collectIdentifiers = (regex) => {
1813
+ let match;
1814
+ while ((match = regex.exec(source)) !== null) {
1815
+ const inner = match[1];
1816
+ for (const token of inner.split(",")) {
1817
+ const id = token.trim();
1818
+ if (id && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(id)) identifiers.add(id);
1982
1819
  }
1983
- pendingPaths.add(entry.path);
1984
1820
  }
1985
- for (const entry of entries) addAnalysisEntry(originalPath, entry.path, entry.source);
1986
1821
  };
1987
- for (const original of originalEntries) {
1988
- ownership[original.path] = {
1989
- originalPath: original.path,
1990
- originalHash: original.hash,
1991
- analysisPaths: []
1992
- };
1993
- const kind = extension(original.path);
1994
- if (kind === ".svelte") {
1995
- svelteEntries.push(original);
1996
- continue;
1997
- }
1998
- if (kind !== ".mdx") {
1999
- addAnalysisEntry(original.path, original.path, original.source, original.hash);
2000
- continue;
2001
- }
2002
- const result = await mdxAdapter(original.source, original.path);
2003
- if (result.kind === "missing-dep") {
2004
- diagnostics.push({
2005
- code: "SOURCE_MDX_DEPENDENCY_MISSING",
2006
- message: "Install optional peer dependency '@mdx-js/mdx' to analyze opted-in MDX source.",
2007
- originalPath: original.path
2008
- });
2009
- continue;
2010
- }
2011
- if (result.kind === "error" || result.source === void 0) {
2012
- diagnostics.push({
2013
- code: "SOURCE_MDX_PARSE_ERROR",
2014
- message: result.error ?? "MDX preprocessing produced no source.",
2015
- originalPath: original.path
2016
- });
2017
- continue;
1822
+ collectIdentifiers(constructorRegex);
1823
+ collectIdentifiers(chainRegex);
1824
+ const IDENT_START_RE = /^[a-zA-Z_$][a-zA-Z0-9_$]*/;
1825
+ const skipTrivia = (from) => {
1826
+ let pos = from;
1827
+ for (;;) {
1828
+ while (pos < source.length && /\s/.test(source[pos])) pos++;
1829
+ if (source.startsWith("//", pos)) {
1830
+ const newline = source.indexOf("\n", pos);
1831
+ pos = newline === -1 ? source.length : newline + 1;
1832
+ continue;
1833
+ }
1834
+ if (source.startsWith("/*", pos)) {
1835
+ const close = source.indexOf("*/", pos + 2);
1836
+ pos = close === -1 ? source.length : close + 2;
1837
+ continue;
1838
+ }
1839
+ return pos;
2018
1840
  }
2019
- addAdaptedEntries(original.path, [{
2020
- path: `${original.path}.tsx`,
2021
- source: result.source
2022
- }]);
2023
- }
2024
- const facts = collectFileFacts(analysisEntries, options);
2025
- for (const [analysisPath, file] of Object.entries(facts.files)) {
2026
- const originalPath = analysisOwner.get(analysisPath) ?? analysisPath;
2027
- for (const message of file.parseDiagnostics) diagnostics.push({
2028
- code: "SOURCE_NATIVE_PARSE_ERROR",
2029
- message,
2030
- originalPath,
2031
- analysisPath
2032
- });
2033
- }
2034
- const resolvers = new ResolverExportIndex(facts, analysisEntries.map((entry) => entry.path));
2035
- for (const { canonicalPath, paths } of resolvers.collisions) for (let index = 0; index < paths.length; index += 1) {
2036
- const path = paths[index];
2037
- const conflictingPath = paths[(index + 1) % paths.length];
2038
- diagnostics.push({
2039
- code: "SOURCE_RESOLVER_IDENTITY_COLLISION",
2040
- message: `Resolver lookup paths '${path}' and '${conflictingPath}' normalize to the same private identity '${canonicalPath}'. Rename one of the source files so resolver identities remain unique.`,
2041
- originalPath: analysisOwner.get(path) ?? path,
2042
- canonicalPath,
2043
- conflictingOriginalPath: analysisOwner.get(conflictingPath) ?? conflictingPath
2044
- });
2045
- }
2046
- for (const original of svelteEntries) {
2047
- const result = await svelteAdapter(original.source, original.path, { attributeResolver: (request) => resolvers.attribute(original.path, request) });
2048
- if (result.kind === "missing-dep") {
2049
- diagnostics.push({
2050
- code: "SOURCE_SVELTE_DEPENDENCY_MISSING",
2051
- message: "Install optional peer dependency 'svelte' to analyze opted-in Svelte source.",
2052
- originalPath: original.path
2053
- });
2054
- continue;
1841
+ };
1842
+ const consumeChainLinks = (from) => {
1843
+ let pos = from;
1844
+ for (;;) {
1845
+ let cursor = skipTrivia(pos);
1846
+ if (source[cursor] !== ".") return pos;
1847
+ cursor = skipTrivia(cursor + 1);
1848
+ const method = IDENT_START_RE.exec(source.slice(cursor))?.[0];
1849
+ if (method !== "extend" && method !== "from") return pos;
1850
+ cursor = skipTrivia(cursor + method.length);
1851
+ if (source[cursor] !== "(") return pos;
1852
+ cursor = skipTrivia(cursor + 1);
1853
+ const base = IDENT_START_RE.exec(source.slice(cursor))?.[0];
1854
+ if (!base) return pos;
1855
+ cursor = skipTrivia(cursor + base.length);
1856
+ while (source[cursor] === ".") {
1857
+ const afterDot = skipTrivia(cursor + 1);
1858
+ const segment = IDENT_START_RE.exec(source.slice(afterDot))?.[0];
1859
+ if (!segment) break;
1860
+ cursor = skipTrivia(afterDot + segment.length);
1861
+ }
1862
+ if (source[cursor] === ",") cursor = skipTrivia(cursor + 1);
1863
+ if (source[cursor] !== ")") return pos;
1864
+ identifiers.add(base);
1865
+ pos = cursor + 1;
2055
1866
  }
2056
- if (result.kind === "error") {
2057
- diagnostics.push(...result.diagnostics);
2058
- continue;
1867
+ };
1868
+ const boundIdentifierBefore = (index) => {
1869
+ const match = /([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*$/.exec(source.slice(0, index));
1870
+ return match ? match[1] : null;
1871
+ };
1872
+ const chainRootIdentifiers = /* @__PURE__ */ new Set();
1873
+ const createSystemAnchor = /createSystem\s*\(/g;
1874
+ let anchorMatch;
1875
+ while ((anchorMatch = createSystemAnchor.exec(source)) !== null) {
1876
+ let pos = anchorMatch.index + anchorMatch[0].length;
1877
+ let depth = 1;
1878
+ while (pos < source.length && depth > 0) {
1879
+ const ch = source[pos];
1880
+ if (ch === "(") depth++;
1881
+ else if (ch === ")") depth--;
1882
+ pos++;
2059
1883
  }
2060
- addAdaptedEntries(original.path, result.entries);
1884
+ consumeChainLinks(pos);
1885
+ const bound = boundIdentifierBefore(anchorMatch.index);
1886
+ if (bound) chainRootIdentifiers.add(bound);
2061
1887
  }
2062
- return {
2063
- originalEntries,
2064
- analysisEntries,
2065
- ownership,
2066
- diagnostics
2067
- };
2068
- }
2069
- /**
2070
- * Run the native fact collector, re-extracting only changed entries when the
2071
- * caller supplies a `factsCache`. `parseCount` keeps its meaning — parses
2072
- * performed by THIS call — so a fully-memoized pass reports zero.
2073
- */
2074
- function collectFileFacts(analysisEntries, options) {
2075
- const cache = options.factsCache;
2076
- if (!cache) return JSON.parse(options.extractFacts(JSON.stringify(analysisEntries)));
2077
- const pending = analysisEntries.filter((entry) => cache.get(entry.path)?.hash !== entry.hash);
2078
- let parseCount = 0;
2079
- if (pending.length > 0) {
2080
- const fresh = JSON.parse(options.extractFacts(JSON.stringify(pending)));
2081
- parseCount = fresh.parseCount;
2082
- for (const entry of pending) {
2083
- const facts = fresh.files[entry.path];
2084
- if (facts) cache.set(entry.path, {
2085
- hash: entry.hash,
2086
- facts
2087
- });
2088
- else cache.delete(entry.path);
1888
+ const scannedRoots = /* @__PURE__ */ new Set();
1889
+ for (;;) {
1890
+ const pendingRoots = [...chainRootIdentifiers].filter((root) => !scannedRoots.has(root));
1891
+ if (pendingRoots.length === 0) break;
1892
+ for (const root of pendingRoots) {
1893
+ scannedRoots.add(root);
1894
+ const rootUse = new RegExp(`(?<![a-zA-Z0-9_$])${root.replace(/\$/g, "\\$")}(?![a-zA-Z0-9_$])`, "g");
1895
+ let useMatch;
1896
+ while ((useMatch = rootUse.exec(source)) !== null) {
1897
+ const afterIdentifier = useMatch.index + useMatch[0].length;
1898
+ if (consumeChainLinks(afterIdentifier) === afterIdentifier) continue;
1899
+ const bound = boundIdentifierBefore(useMatch.index);
1900
+ if (bound) chainRootIdentifiers.add(bound);
1901
+ }
2089
1902
  }
2090
1903
  }
2091
- const currentPaths = new Set(analysisEntries.map((entry) => entry.path));
2092
- for (const path of [...cache.keys()]) if (!currentPaths.has(path)) cache.delete(path);
2093
- const files = {};
2094
- for (const entry of analysisEntries) {
2095
- const cached = cache.get(entry.path);
2096
- if (cached) files[entry.path] = cached.facts;
1904
+ if (identifiers.size === 0) return [];
1905
+ const importMap = /* @__PURE__ */ new Map();
1906
+ const importRegex = /^\s*import\s+(?:([a-zA-Z_$][a-zA-Z0-9_$]*)\s*,\s*)?(?:\{([^}]*)\}|([a-zA-Z_$][a-zA-Z0-9_$]*))\s+from\s+['"]([^'"]+)['"]/gm;
1907
+ let importMatch;
1908
+ while ((importMatch = importRegex.exec(source)) !== null) {
1909
+ const [, comboDefault, namedImports, defaultImport, specifier] = importMatch;
1910
+ if (comboDefault) importMap.set(comboDefault, specifier);
1911
+ if (defaultImport) importMap.set(defaultImport, specifier);
1912
+ if (namedImports) for (const binding of namedImports.split(",")) {
1913
+ const parts = binding.trim().split(/\s+as\s+/);
1914
+ const localName = (parts[1] || parts[0]).trim();
1915
+ if (localName) importMap.set(localName, specifier);
1916
+ }
2097
1917
  }
2098
- return {
2099
- files,
2100
- parseCount
2101
- };
1918
+ const systemFileDir = dirname$1(systemFilePath);
1919
+ const packages = /* @__PURE__ */ new Set();
1920
+ for (const id of identifiers) {
1921
+ const specifier = importMap.get(id);
1922
+ if (!specifier) continue;
1923
+ if (specifier.startsWith(".")) packages.add(resolve(systemFileDir, specifier));
1924
+ else packages.add(specifier);
1925
+ }
1926
+ return Array.from(packages);
2102
1927
  }
2103
- /**
2104
- * The per-file quarantine: drop every original a diagnostic named — and the
2105
- * analysis children it owns — so one invalid source never aborts the rest
2106
- * of the corpus. Shared by buildStart AND every incremental path in both
2107
- * hosts; asymmetry here is how a permanently-diagnosable file (an `.mdx`
2108
- * with the optional peer absent, an unsupported `.svelte` shape) froze all
2109
- * re-analysis for the life of the dev server.
2110
- */
2111
- function withoutInvalidOriginals(result, invalidOriginals) {
2112
- if (invalidOriginals.size === 0) return result;
2113
- const ownership = Object.fromEntries(Object.entries(result.ownership).filter(([originalPath]) => !invalidOriginals.has(originalPath)));
2114
- const analysisPaths = new Set(Object.values(ownership).flatMap((owner) => owner.analysisPaths));
2115
- return {
2116
- ...result,
2117
- originalEntries: result.originalEntries.filter((entry) => !invalidOriginals.has(entry.path)),
2118
- analysisEntries: result.analysisEntries.filter((entry) => analysisPaths.has(entry.path)),
2119
- ownership
2120
- };
1928
+ //#endregion
1929
+ //#region pipeline/source-corpus.ts
1930
+ function assembleRawEntries({ fileCache, externalFileOwners }) {
1931
+ const project = [];
1932
+ const external = [];
1933
+ for (const [path, { hash, source }] of fileCache) (externalFileOwners[path] === void 0 ? project : external).push({
1934
+ path,
1935
+ source,
1936
+ hash
1937
+ });
1938
+ project.sort((a, b) => compareDiscoveryOrder(a.path, b.path));
1939
+ return [...project, ...external];
2121
1940
  }
2122
- /**
2123
- * The one source-ingestion policy point shared by every host (vite-plugin,
2124
- * next-plugin, cli, unplugin). Hosts hold exactly their prefix, strict flag,
2125
- * and warn sink; the capability guard, facts memo, and warn-dedupe lifecycle
2126
- * live here so the plugins cannot fork (precedent: pkg-collection
2127
- * divergences; see also `enforceExternalTokenContracts`).
2128
- */
2129
- function createSourceIngestor(host) {
2130
- const factsCache = /* @__PURE__ */ new Map();
2131
- /** Non-strict warn dedupe: a quarantined-but-retained original re-ingests
2132
- * on every later corpus pass, and re-warning each save is noise. Keyed by
2133
- * original path; cleared when that original publishes clean again. */
2134
- const warnedByOriginal = /* @__PURE__ */ new Map();
1941
+ function createSourceCorpus(host, ingestor = createSourceIngestor(host)) {
1942
+ let published = {
1943
+ analysisEntries: /* @__PURE__ */ new Map(),
1944
+ ownership: {}
1945
+ };
2135
1946
  return {
2136
- async ingest(entries) {
2137
- const extractFacts = host.engineApi().extractFacts;
2138
- if (typeof extractFacts !== "function") throw new Error(`${host.prefix} native engine does not expose extractFacts required for source adaptation`);
2139
- return ingestSourceEntries(entries, {
2140
- extractFacts,
2141
- factsCache
2142
- });
1947
+ async prepare(input) {
1948
+ const rawEntries = "fileCache" in input ? assembleRawEntries(input) : input;
1949
+ const ingested = await ingestor.ingest(rawEntries);
1950
+ return withoutInvalidOriginals(ingested, ingestor.surfaceDiagnostics(ingested.diagnostics));
2143
1951
  },
2144
- surfaceDiagnostics(diagnostics) {
2145
- const fatal = diagnostics.filter((diagnostic) => !isAdvisorySourceDiagnostic(diagnostic));
2146
- const invalidOriginals = new Set(fatal.map((diagnostic) => diagnostic.originalPath));
2147
- if (diagnostics.length === 0) return invalidOriginals;
2148
- if (host.strict() && fatal.length > 0) {
2149
- const lines = fatal.map((diagnostic) => `${diagnostic.code} ${diagnostic.originalPath}: ${diagnostic.message}`);
2150
- throw new Error(`${host.prefix} ${lines.join(`\n${host.prefix} `)}`);
2151
- }
2152
- for (const diagnostic of diagnostics) {
2153
- const line = `${diagnostic.code} ${diagnostic.originalPath}: ${diagnostic.message}`;
2154
- let warned = warnedByOriginal.get(diagnostic.originalPath);
2155
- if (!warned) {
2156
- warned = /* @__PURE__ */ new Set();
2157
- warnedByOriginal.set(diagnostic.originalPath, warned);
2158
- }
2159
- if (warned.has(line)) continue;
2160
- warned.add(line);
2161
- host.warn(`${host.prefix} ${line}`);
2162
- }
2163
- return invalidOriginals;
1952
+ publish(accepted) {
1953
+ ingestor.markPublished(accepted);
1954
+ published = {
1955
+ analysisEntries: new Map(accepted.analysisEntries.map((entry) => [entry.path, {
1956
+ hash: entry.hash,
1957
+ source: entry.source
1958
+ }])),
1959
+ ownership: accepted.ownership
1960
+ };
2164
1961
  },
2165
- markPublished(result) {
2166
- for (const originalPath of Object.keys(result.ownership)) warnedByOriginal.delete(originalPath);
1962
+ get published() {
1963
+ return published;
2167
1964
  }
2168
1965
  };
2169
1966
  }
2170
1967
  //#endregion
2171
1968
  //#region pipeline/asset-placeholders.ts
2172
- /**
2173
- * Host-side handling of `asset()` placeholders (global-styles-system):
2174
- * `animus-asset:<specifier>` markers ride through the sandbox and the
2175
- * emitter byte-exact; each HOST plugin substitutes them with its bundler's
2176
- * resolved asset URL after extraction. This module owns the shared scanning
2177
- * and replacement mechanics — resolution and strict gating stay at the
2178
- * plugin call sites.
2179
- */
2180
- /** Mirrors the producer-side constant in `@animus-ui/system`'s `asset.ts`
2181
- * (extract must not take a runtime dependency on system); the pairing is
2182
- * pinned by a contract test in `tests/asset-placeholders.test.ts`. */
2183
1969
  const ASSET_PLACEHOLDER_PREFIX = "animus-asset:";
2184
1970
  const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2185
1971
  const PREFIX_RE = escapeRegExp(ASSET_PLACEHOLDER_PREFIX);
2186
- /**
2187
- * Quoted `url('animus-asset:<specifier>')` form — the emitter always quotes
2188
- * `url()` values, so a quoted match carries the FULL specifier, including
2189
- * whitespace and parentheses (D5 pins the placeholder as specifier-verbatim,
2190
- * so scanning fidelity is the only place such characters can be honored).
2191
- */
2192
1972
  const QUOTED_PLACEHOLDER_RE = new RegExp(`(['"])${PREFIX_RE}([^'"]*?)\\1`, "g");
2193
- /** Bare-form fallback: a placeholder up to CSS url()/quote/whitespace. */
2194
1973
  const BARE_PLACEHOLDER_RE = new RegExp(`${PREFIX_RE}([^'")\\s]+)`, "g");
2195
- /** Unique asset specifiers referenced by placeholders, in appearance order. */
2196
1974
  function findAssetSpecifiers(css) {
2197
1975
  if (!css.includes("animus-asset:")) return [];
2198
1976
  const seen = /* @__PURE__ */ new Set();
@@ -2203,19 +1981,6 @@ function findAssetSpecifiers(css) {
2203
1981
  for (const match of remainder.matchAll(BARE_PLACEHOLDER_RE)) seen.add(match[1]);
2204
1982
  return [...seen];
2205
1983
  }
2206
- /**
2207
- * Replace each specifier's placeholder with its substitution. Specifiers
2208
- * absent from the map keep their placeholder — the caller decides whether
2209
- * that is a strict failure or a warn-and-emit-literally. A substitution may
2210
- * itself be a bundler marker (e.g. Vite's `__VITE_ASSET__<ref>__`) that the
2211
- * host's own asset pipeline resolves to a hashed file name later.
2212
- *
2213
- * Each replacement is anchored to a following delimiter (quote, `)`,
2214
- * whitespace, or end of input) so a specifier that textually prefixes a
2215
- * longer one can never clobber the longer placeholder; longest-first
2216
- * ordering additionally lets the longer of two mapped specifiers claim its
2217
- * occurrences before the shorter runs.
2218
- */
2219
1984
  function substituteAssetPlaceholders(css, urlBySpecifier) {
2220
1985
  if (urlBySpecifier.size === 0) return css;
2221
1986
  if (!css.includes("animus-asset:")) return css;
@@ -2230,51 +1995,42 @@ function substituteAssetPlaceholders(css, urlBySpecifier) {
2230
1995
  //#endregion
2231
1996
  //#region pipeline/resolve-asset.ts
2232
1997
  /**
2233
- * Node-side asset specifier resolution shared by the host plugins (the
2234
- * asset() contract, global-styles-system): host path aliases apply FIRST —
2235
- * an alias such as `@fonts` works in application modules, so
2236
- * `asset('@fonts/inter.woff2')` must resolve identically — then direct Node
2237
- * resolution, then the package-root fallback (exports maps rarely list
2238
- * asset subpaths). Bundler-native resolution (Vite's `this.resolve`) is
2239
- * still preferred where available; this resolver covers the paths that have
2240
- * no bundler hook (Next's session, Vite's dev re-analysis).
1998
+ * Asset resolution for hosts with no bundler hook. Host path aliases apply
1999
+ * FIRST, so `asset('@fonts/x')` resolves as the alias does in app modules.
2241
2000
  */
2242
2001
  const requireByRoot = /* @__PURE__ */ new Map();
2243
2002
  function requireAnchoredAt(rootDir) {
2244
2003
  let req = requireByRoot.get(rootDir);
2245
2004
  if (!req) {
2246
- req = createRequire$1(join(rootDir, "package.json"));
2005
+ req = createRequire$1(join$1(rootDir, "package.json"));
2247
2006
  requireByRoot.set(rootDir, req);
2248
2007
  }
2249
2008
  return req;
2250
2009
  }
2251
2010
  function packageRootFromEntry(entry) {
2252
- let current = dirname(entry);
2011
+ let current = dirname$1(entry);
2253
2012
  while (true) {
2254
- if (existsSync(join(current, "package.json"))) return current;
2255
- const parent = dirname(current);
2013
+ if (existsSync(join$1(current, "package.json"))) return current;
2014
+ const parent = dirname$1(current);
2256
2015
  if (parent === current) return null;
2257
2016
  current = parent;
2258
2017
  }
2259
2018
  }
2260
2019
  const aliasTableCache = /* @__PURE__ */ new Map();
2020
+ /**
2021
+ * A parse failure throws: an empty table would disable every alias-based
2022
+ * `asset()` and ship dangling `url()`s as a successful build.
2023
+ */
2261
2024
  function parseAliasTable(pathAliasesJson) {
2262
2025
  const cached = aliasTableCache.get(pathAliasesJson);
2263
2026
  if (cached) return cached;
2264
- let aliases;
2265
- try {
2266
- aliases = JSON.parse(pathAliasesJson).aliases ?? [];
2267
- } catch {
2268
- aliases = [];
2269
- }
2027
+ const aliases = parseInternalWire(pathAliasesJson, "pathAliasesJson (the host alias table from buildPathAliasesJson)").aliases ?? [];
2270
2028
  aliasTableCache.set(pathAliasesJson, aliases);
2271
2029
  return aliases;
2272
2030
  }
2273
2031
  /**
2274
- * Map a specifier through the harvested host alias table (the same
2275
- * `pathAliasesJson` wire the engine consumes — entries pre-sorted longest
2276
- * pattern first by `buildPathAliasesJson`). Returns an absolute path to an
2277
- * EXISTING file, or null when no alias claims the specifier.
2032
+ * Map a specifier through the host alias table, whose entries arrive sorted
2033
+ * longest-pattern-first. Returns an absolute path to an EXISTING file.
2278
2034
  */
2279
2035
  function resolveThroughPathAliases(specifier, rootDir, pathAliasesJson) {
2280
2036
  if (!pathAliasesJson) return null;
@@ -2285,15 +2041,14 @@ function resolveThroughPathAliases(specifier, rootDir, pathAliasesJson) {
2285
2041
  if (alias.pattern === specifier) mapped = alias.replacement;
2286
2042
  } else if (specifier.startsWith(alias.pattern)) mapped = alias.replacement + specifier.slice(alias.pattern.length);
2287
2043
  if (mapped === null) continue;
2288
- const absolute = isAbsolute(mapped) ? mapped : join(rootDir, mapped);
2044
+ const absolute = isAbsolute(mapped) ? mapped : join$1(rootDir, mapped);
2289
2045
  if (existsSync(absolute)) return absolute;
2290
2046
  }
2291
2047
  return null;
2292
2048
  }
2293
2049
  /**
2294
- * Resolve an asset specifier to an absolute file: host aliases, then direct
2295
- * Node resolution anchored at `rootDir`, then the package-root fallback.
2296
- * Returns null when nothing matches — strict gating stays at the caller.
2050
+ * Resolve an asset specifier: host aliases, then Node resolution anchored at
2051
+ * `rootDir`, then the package root. Strict gating stays at the caller.
2297
2052
  */
2298
2053
  function resolveAssetFile(specifier, rootDir, pathAliasesJson) {
2299
2054
  const aliased = resolveThroughPathAliases(specifier, rootDir, pathAliasesJson);
@@ -2307,15 +2062,15 @@ function resolveAssetFile(specifier, rootDir, pathAliasesJson) {
2307
2062
  const subpath = specifier.slice(packageName.length + 1);
2308
2063
  if (!subpath) return null;
2309
2064
  for (const modulesDir of requireFromRoot.resolve.paths(packageName) ?? []) {
2310
- const packageRoot = join(modulesDir, packageName);
2311
- if (!existsSync(join(packageRoot, "package.json"))) continue;
2312
- const candidate = join(packageRoot, subpath);
2065
+ const packageRoot = join$1(modulesDir, packageName);
2066
+ if (!existsSync(join$1(packageRoot, "package.json"))) continue;
2067
+ const candidate = join$1(packageRoot, subpath);
2313
2068
  if (existsSync(candidate)) return candidate;
2314
2069
  }
2315
2070
  try {
2316
2071
  const packageRoot = packageRootFromEntry(requireFromRoot.resolve(packageName));
2317
2072
  if (!packageRoot) return null;
2318
- const candidate = join(packageRoot, subpath);
2073
+ const candidate = join$1(packageRoot, subpath);
2319
2074
  return existsSync(candidate) ? candidate : null;
2320
2075
  } catch {
2321
2076
  return null;
@@ -2324,20 +2079,13 @@ function resolveAssetFile(specifier, rootDir, pathAliasesJson) {
2324
2079
  //#endregion
2325
2080
  //#region pipeline/correlate-external-tokens.ts
2326
2081
  /**
2327
- * Index the loader-captured theme manifests by owning specifier. Module paths
2328
- * are canonical absolute paths (symlinks resolved), so each package dir is
2329
- * realpath'd before prefix-matching; a module outside every known package dir
2330
- * (typically the consumer's own theme) contributes nothing.
2082
+ * Index the loader-captured theme manifests by owning specifier. An
2083
+ * unparseable capture throws: an empty index reads as "no source defines it".
2331
2084
  */
2332
2085
  function buildSourceTokenIndex(opts) {
2333
2086
  const index = /* @__PURE__ */ new Map();
2334
2087
  if (!opts.sourceThemeManifestsJson) return index;
2335
- let manifests;
2336
- try {
2337
- manifests = JSON.parse(opts.sourceThemeManifestsJson);
2338
- } catch {
2339
- return index;
2340
- }
2088
+ const manifests = parseInternalWire(opts.sourceThemeManifestsJson, "sourceThemeManifestsJson (the system loader's built-theme capture)");
2341
2089
  const realDirOwners = [];
2342
2090
  const addOwnerDir = (dir, specifier) => {
2343
2091
  if (!realDirOwners.some((entry) => entry.dir === dir)) realDirOwners.push({
@@ -2352,8 +2100,8 @@ function buildSourceTokenIndex(opts) {
2352
2100
  } catch {}
2353
2101
  addOwnerDir(real, specifier);
2354
2102
  let packageRoot = real;
2355
- while (packageRoot !== dirname(packageRoot) && !existsSync(join(packageRoot, "package.json"))) packageRoot = dirname(packageRoot);
2356
- if (existsSync(join(packageRoot, "package.json"))) addOwnerDir(packageRoot, specifier);
2103
+ while (packageRoot !== dirname$1(packageRoot) && !existsSync(join$1(packageRoot, "package.json"))) packageRoot = dirname$1(packageRoot);
2104
+ if (existsSync(join$1(packageRoot, "package.json"))) addOwnerDir(packageRoot, specifier);
2357
2105
  }
2358
2106
  for (const [modulePath, exports] of Object.entries(manifests)) {
2359
2107
  const owner = realDirOwners.find(({ dir }) => isPathWithinRoot(dir, modulePath));
@@ -2368,11 +2116,8 @@ function buildSourceTokenIndex(opts) {
2368
2116
  return index;
2369
2117
  }
2370
2118
  /**
2371
- * The correlation join. Returns one teaching-error message per distinct
2372
- * (component, token, specifier) whose file belongs to a discovered source AND
2373
- * whose token that source's manifest defines. The messages follow the
2374
- * standard severity routing at the CALLER (throw under `strict`, warn
2375
- * otherwise) — this join never reports on its own.
2119
+ * One message per distinct (component, token, specifier) whose source
2120
+ * defines the token. Severity routing stays at the caller.
2376
2121
  */
2377
2122
  function correlateExternalTokenDiagnostics(opts) {
2378
2123
  const messages = [];
@@ -2391,11 +2136,8 @@ function correlateExternalTokenDiagnostics(opts) {
2391
2136
  }
2392
2137
  const indexCache = /* @__PURE__ */ new WeakMap();
2393
2138
  /**
2394
- * The complete cross-source token-contract gate both plugins share
2395
- * (extraction-diagnostics): index the loader-captured source manifests,
2396
- * join them against the engine's candidates, and route the resulting
2397
- * teaching errors — throw under `strict`, warn otherwise. One entry point
2398
- * so the two hosts cannot drift on wiring or severity semantics.
2139
+ * The one cross-source token-contract gate both plugins share, so the hosts
2140
+ * cannot drift on wiring or severity.
2399
2141
  */
2400
2142
  function enforceExternalTokenContracts(opts) {
2401
2143
  const manifestsJson = opts.sourceThemeManifestsJson ?? "";
@@ -2423,12 +2165,6 @@ function enforceExternalTokenContracts(opts) {
2423
2165
  }
2424
2166
  //#endregion
2425
2167
  //#region pipeline/path-aliases.ts
2426
- /**
2427
- * Normalize alias pairs: strip a `rootDir` prefix from targets, classify
2428
- * exact (file) vs prefix (directory) matches, force trailing slashes on
2429
- * prefix entries, and sort longest pattern first for correct matching
2430
- * priority. Returns null when no entries survive.
2431
- */
2432
2168
  function buildPathAliasesJson(pairs, rootDir) {
2433
2169
  const entries = [];
2434
2170
  for (const { pattern, target, kind } of pairs) {
@@ -2453,17 +2189,6 @@ function buildPathAliasesJson(pairs, rootDir) {
2453
2189
  }
2454
2190
  //#endregion
2455
2191
  //#region pipeline/post-process-css.ts
2456
- /**
2457
- * CSS post-processing (spec: css-post-processing) — Lightning CSS
2458
- * autoprefixing, syntax lowering, and optional minification against
2459
- * browserslist targets. The single implementation consumed by both the
2460
- * Vite plugin (virtual-module serve time) and the Next.js plugin
2461
- * (ExtractionSession artifact emission).
2462
- */
2463
- /**
2464
- * Resolve browser targets for Lightning CSS.
2465
- * Priority: explicit config → project browserslist → 'defaults' fallback.
2466
- */
2467
2192
  function resolveLightningTargets(explicitTargets, rootDir) {
2468
2193
  let queries;
2469
2194
  if (explicitTargets) queries = Array.isArray(explicitTargets) ? explicitTargets : [explicitTargets];
@@ -2471,12 +2196,9 @@ function resolveLightningTargets(explicitTargets, rootDir) {
2471
2196
  const detected = browserslist(void 0, { path: rootDir });
2472
2197
  queries = detected.length > 0 ? detected : browserslist("defaults");
2473
2198
  }
2474
- return browserslistToTargets(Array.isArray(queries) && typeof queries[0] === "string" && queries[0].includes(" ") ? browserslist(queries) : queries);
2199
+ const [firstQuery] = queries;
2200
+ return browserslistToTargets(firstQuery !== void 0 && firstQuery.includes(" ") ? browserslist(queries) : queries);
2475
2201
  }
2476
- /**
2477
- * Post-process CSS with Lightning CSS: autoprefixing + optional minification.
2478
- * On failure, returns the original CSS and logs a warning.
2479
- */
2480
2202
  function postProcessCss(css, opts) {
2481
2203
  if (!css) return css;
2482
2204
  try {
@@ -2494,28 +2216,20 @@ function postProcessCss(css, opts) {
2494
2216
  }
2495
2217
  //#endregion
2496
2218
  //#region pipeline/manifest-diagnostics.ts
2497
- /** Stable code for selector forms with no substitutable subject.
2498
- * Mirrors the Rust constant in `extract-v2/src/eval.rs`. */
2219
+ /** Stable code for selector forms with no substitutable subject; the
2220
+ * extraction engine mints the same string. */
2499
2221
  const SELECTOR_UNSUPPORTED_SUBJECT = "animus.selector.unsupported-subject";
2500
- /** Unresolved-parent chain-drop message (mirrors the Rust diagnostic in
2501
- * `extract-v2` — the ONE encoding of this message shape; consumers match
2502
- * through the helpers below, never their own regex copies). */
2222
+ /** The one matcher for the engine's unresolved-parent chain-drop message;
2223
+ * consumers match through the exported helpers, never their own copy. */
2503
2224
  const UNRESOLVED_PARENT_RE = /chain dropped: could not resolve parent component '([^']+)'/;
2504
- /** True when the diagnostic reports a chain dropped for an unresolved
2505
- * parent component. */
2506
2225
  function isUnresolvedParentDrop(diagnostic) {
2507
- return typeof diagnostic?.message === "string" && UNRESOLVED_PARENT_RE.test(diagnostic.message);
2226
+ return UNRESOLVED_PARENT_RE.test(diagnostic.message);
2508
2227
  }
2509
- /** The parent binding named by an unresolved-parent drop, or null when the
2510
- * diagnostic is not one. */
2511
2228
  function unresolvedParentName(diagnostic) {
2512
- if (typeof diagnostic?.message !== "string") return null;
2513
2229
  return UNRESOLVED_PARENT_RE.exec(diagnostic.message)?.[1] ?? null;
2514
2230
  }
2515
- /** True when the selector string carries at least one substitutable `&`
2516
- * subject outside quoted text (mirrors the Rust `selector_subject` walk —
2517
- * ancestor, leading, and repeated subjects all count; a `&` inside a
2518
- * quoted attribute value does not). */
2231
+ /** True when the selector carries at least one substitutable `&` subject
2232
+ * outside quoted text. */
2519
2233
  function hasSelectorSubject(value) {
2520
2234
  let quote = null;
2521
2235
  let escaped = false;
@@ -2536,44 +2250,91 @@ function hasSelectorSubject(value) {
2536
2250
  return false;
2537
2251
  }
2538
2252
  /**
2539
- * Synthesize coded diagnostics for registered selector-alias values that
2540
- * look selector-shaped (`&` present) but carry no substitutable subject —
2541
- * every `&` sits inside quoted text, so there is nothing to anchor the
2542
- * class to. Ancestor-prefixed and repeated subjects are supported and pass
2543
- * validation. The system-config boundary is where these must fail loud
2544
- * (alias values never reach the evaluator's key guard).
2253
+ * Coded diagnostics for selector-shaped alias values with no substitutable
2254
+ * subject; alias values never reach the evaluator's own key guard.
2545
2255
  */
2546
2256
  function collectSelectorAliasDiagnostics(selectorAliasesJson) {
2547
2257
  if (!selectorAliasesJson) return [];
2548
- let aliases;
2549
- try {
2550
- aliases = JSON.parse(selectorAliasesJson);
2551
- } catch {
2552
- return [];
2553
- }
2258
+ const aliases = parseInternalWire(selectorAliasesJson, "selectorAliasesJson (the system loader's selector-alias registry)");
2554
2259
  const diagnostics = [];
2555
- for (const [name, value] of Object.entries(aliases)) {
2556
- if (typeof value !== "string") continue;
2557
- if (value.includes("&") && !hasSelectorSubject(value)) diagnostics.push({
2558
- file: "system",
2559
- component: name,
2560
- kind: "warn",
2561
- message: `selector alias '${name}' value '${value}' has no substitutable '&' subject outside quoted text (${SELECTOR_UNSUPPORTED_SUBJECT})`,
2562
- code: SELECTOR_UNSUPPORTED_SUBJECT,
2563
- severity: "error"
2564
- });
2565
- }
2260
+ for (const [name, value] of Object.entries(aliases)) if (value.includes("&") && !hasSelectorSubject(value)) diagnostics.push({
2261
+ file: "system",
2262
+ component: name,
2263
+ kind: "warn",
2264
+ message: `selector alias '${name}' value '${value}' has no substitutable '&' subject outside quoted text (${SELECTOR_UNSUPPORTED_SUBJECT})`,
2265
+ code: SELECTOR_UNSUPPORTED_SUBJECT,
2266
+ severity: severityFor(SELECTOR_UNSUPPORTED_SUBJECT)
2267
+ });
2268
+ return diagnostics;
2269
+ }
2270
+ const UNREADABLE_SOURCE_FILE = "animus.ingestion.unreadable-source-file";
2271
+ /**
2272
+ * A configured source file that could not be read is a LOST INPUT: the
2273
+ * emitted artifacts silently lack whatever that file declared.
2274
+ */
2275
+ function unreadableSourceDiagnostic(relPath, error) {
2276
+ return {
2277
+ file: relPath,
2278
+ component: "source",
2279
+ kind: "warn",
2280
+ message: `configured source file ${relPath} could not be read and was skipped: ${String(error)}`,
2281
+ code: UNREADABLE_SOURCE_FILE,
2282
+ severity: severityFor(UNREADABLE_SOURCE_FILE)
2283
+ };
2284
+ }
2285
+ /** The collision entry code minted by the system package's merge. */
2286
+ const VOCABULARY_COLLISION = "animus.vocabulary.collision";
2287
+ /** Minted when a sealed kit with registered vocabulary is consumed through
2288
+ * the deprecated `from()`/`includes:` verbs. */
2289
+ const VOCABULARY_LEGACY_VERB = "animus.vocabulary.legacy-verb";
2290
+ const DIAGNOSTIC_SEVERITY = /* @__PURE__ */ new Map([
2291
+ [SELECTOR_UNSUPPORTED_SUBJECT, "error"],
2292
+ [UNREADABLE_SOURCE_FILE, "error"],
2293
+ [VOCABULARY_COLLISION, "warn"],
2294
+ [VOCABULARY_LEGACY_VERB, "warn"]
2295
+ ]);
2296
+ /** An unlisted code is `warn`: a witness kind from a newer system package
2297
+ * must not fail the strict build of a host that predates its writer. */
2298
+ function severityFor(code) {
2299
+ return (code === void 0 ? void 0 : DIAGNOSTIC_SEVERITY.get(code)) ?? "warn";
2300
+ }
2301
+ /**
2302
+ * The one mapper of a sealed system's witness entries for every host: the
2303
+ * loader's evaluation host shims `console`, so the record is the channel.
2304
+ */
2305
+ function vocabularyWitnessDiagnostics(vocabularyWitnessesJson) {
2306
+ if (!vocabularyWitnessesJson) return [];
2307
+ const entries = parseInternalWire(vocabularyWitnessesJson, "vocabularyWitnessesJson (the sealed system's vocabulary witness record)");
2308
+ const diagnostics = [];
2309
+ for (const entry of entries) if (entry.code === "animus.vocabulary.collision") diagnostics.push({
2310
+ file: "system",
2311
+ component: entry.name ?? "keyframes",
2312
+ kind: "warn",
2313
+ message: `keyframes vocabulary "${entry.name}" is registered by both ${entry.loser} and ${entry.winner} — ${entry.winner} wins; rename one collection (${entry.code})`,
2314
+ code: entry.code,
2315
+ severity: severityFor(entry.code)
2316
+ });
2317
+ else if (entry.code === "animus.vocabulary.legacy-verb") diagnostics.push({
2318
+ file: "system",
2319
+ component: "keyframes",
2320
+ kind: "warn",
2321
+ message: `a sealed system (${entry.source ?? `'${entry.verb}' source`}) with registered vocabulary [${(entry.names ?? []).join(", ")}] was consumed through the deprecated '${entry.verb}' verb, which cannot carry it — those collections do NOT reach this consumer; use createSystem().extend(source) (${entry.code})`,
2322
+ code: entry.code,
2323
+ severity: severityFor(entry.code)
2324
+ });
2325
+ else diagnostics.push({
2326
+ file: "system",
2327
+ component: "vocabulary",
2328
+ kind: "warn",
2329
+ message: `unrecognized vocabulary witness entry ${JSON.stringify(entry)} — a newer @animus-ui/system may have recorded a witness kind this host predates${entry.code ? ` (${entry.code})` : ""}`,
2330
+ code: entry.code,
2331
+ severity: severityFor(entry.code)
2332
+ });
2566
2333
  return diagnostics;
2567
2334
  }
2568
2335
  /**
2569
- * Surface extraction-manifest diagnostics through a plugin's warn channel.
2570
- *
2571
- * Single authoritative copy for both extraction plugins — and the single
2572
- * strict-escalation policy point: error-severity diagnostics throw one
2573
- * Error naming every offender when `policy.strict`, and print as warnings
2574
- * otherwise. Surfaces `bail` (component not extracted), `skip` (component
2575
- * skipped), and `warn` kinds; unknown kinds stay silent. Printed lines
2576
- * include the diagnostic code when the message doesn't already carry it.
2336
+ * The single strict-escalation policy point for both extraction plugins:
2337
+ * error-severity diagnostics throw together under `strict`, else warn.
2577
2338
  */
2578
2339
  function surfaceManifestDiagnostics(manifest, warn, policy = {}) {
2579
2340
  const errors = [];
@@ -2595,11 +2356,6 @@ function surfaceManifestDiagnostics(manifest, warn, policy = {}) {
2595
2356
  }
2596
2357
  //#endregion
2597
2358
  //#region pipeline/unit-fallback.ts
2598
- /**
2599
- * Append `px` to bare numeric values in CSS declarations for properties
2600
- * that expect length units. Unitless properties are preserved as-is.
2601
- * Numbers inside CSS function calls (cubic-bezier, rgb, calc, etc.) are skipped.
2602
- */
2603
2359
  function applyUnitFallback(css) {
2604
2360
  return css.replace(/([a-z-]+)\s*:\s*([^;{}]+);/g, (match, prop, value) => {
2605
2361
  if (isUnitlessProperty(prop)) return match;
@@ -2635,13 +2391,14 @@ function applyUnitFallback(css) {
2635
2391
  }
2636
2392
  //#endregion
2637
2393
  //#region pipeline/run-analysis.ts
2638
- /**
2639
- * Build the named `analyzeProject` input set from analysis options. Also
2640
- * the persistence shape for `.animus/analysis-inputs.json` — an isolated
2641
- * process can replay the analysis from exactly this object
2642
- * (spec: next-turbopack-integration).
2643
- */
2394
+ /** The named `analyzeProject` input set, and the persistence shape an
2395
+ * isolated process replays the analysis from. */
2644
2396
  function buildAnalysisInputs(opts) {
2397
+ const emitterConfig = {
2398
+ runtime_import: opts.emitter.runtimeImport,
2399
+ css_module_id: opts.emitter.cssModuleId
2400
+ };
2401
+ if (opts.emitter.systemPropsModuleId) emitterConfig.system_props_module_id = opts.emitter.systemPropsModuleId;
2645
2402
  return {
2646
2403
  filesJson: JSON.stringify(opts.fileEntries),
2647
2404
  scalesJson: opts.system.scalesJson,
@@ -2651,11 +2408,7 @@ function buildAnalysisInputs(opts) {
2651
2408
  groupRegistryJson: opts.system.groupRegistryJson,
2652
2409
  packageResolutionJson: JSON.stringify(opts.packageMap),
2653
2410
  devMode: opts.devMode,
2654
- emitterConfigJson: JSON.stringify({
2655
- runtime_import: opts.emitter.runtimeImport,
2656
- css_module_id: opts.emitter.cssModuleId,
2657
- ...opts.emitter.systemPropsModuleId ? { system_props_module_id: opts.emitter.systemPropsModuleId } : {}
2658
- }),
2411
+ emitterConfigJson: JSON.stringify(emitterConfig),
2659
2412
  selectorAliasesJson: opts.system.selectorAliasesJson,
2660
2413
  globalStyleBlocksJson: opts.system.globalStyleBlocksJson,
2661
2414
  pathAliasesJson: opts.pathAliasesJson,
@@ -2666,17 +2419,13 @@ function buildAnalysisInputs(opts) {
2666
2419
  externalDirsJson: opts.externalDirs?.length && hasSourceThemeManifests(opts.system) ? JSON.stringify(opts.externalDirs) : null
2667
2420
  };
2668
2421
  }
2669
- /** Whether the loader captured at least one source built-theme manifest. */
2670
2422
  function hasSourceThemeManifests(system) {
2671
- const json = system.sourceThemeManifestsJson;
2672
- return typeof json === "string" && json.length > 0 && json !== "{}";
2423
+ const json = system.sourceThemeManifestsJson ?? "";
2424
+ return json.length > 0 && json !== "{}";
2673
2425
  }
2674
2426
  /**
2675
- * The one analysis invocation both plugins share: build the emitter
2676
- * config, serialize inputs, call the NAPI `analyzeProject`, parse the
2677
- * manifest, surface its diagnostics, and resolve the CSS outputs.
2678
- *
2679
- * Error handling stays at the call site (strict-mode throw vs warn).
2427
+ * The one analysis invocation both plugins share. Error handling stays at
2428
+ * the call site (strict-mode throw vs warn).
2680
2429
  */
2681
2430
  function runProjectAnalysis(engineApi, opts) {
2682
2431
  const { analyzeProject } = engineApi();
@@ -2696,8 +2445,8 @@ function runProjectAnalysis(engineApi, opts) {
2696
2445
  return {
2697
2446
  manifest,
2698
2447
  manifestJson,
2699
- globalCss: manifest?.sheets?.global || "",
2700
- componentCss: applyUnitFallback(manifest?.css || ""),
2448
+ globalCss: manifest.sheets.global,
2449
+ componentCss: applyUnitFallback(manifest.css),
2701
2450
  inputs,
2702
2451
  timings: {
2703
2452
  serializeMs,
@@ -2707,9 +2456,8 @@ function runProjectAnalysis(engineApi, opts) {
2707
2456
  };
2708
2457
  }
2709
2458
  /**
2710
- * Clear the engine's per-file analysis cache so stale results from a prior
2711
- * build never bleed into a fresh run. Tolerates engines without the
2712
- * capability (older builds) — the probe is benign.
2459
+ * Clear the engine's per-file analysis cache so stale results never bleed
2460
+ * into a fresh run. Engines without the capability are tolerated.
2713
2461
  */
2714
2462
  function clearEngineCache(engineApi) {
2715
2463
  try {
@@ -2718,15 +2466,32 @@ function clearEngineCache(engineApi) {
2718
2466
  } catch {}
2719
2467
  }
2720
2468
  //#endregion
2721
- //#region pipeline/static-css.ts
2722
- /** Key-sorted stringify so semantically identical configs serialize
2723
- * identically (stable analysis-inputs hashing). */
2469
+ //#region pipeline/utils.ts
2470
+ function camelToKebab(str) {
2471
+ return str.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
2472
+ }
2724
2473
  function stableStringify(value) {
2725
2474
  if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
2726
- if (value !== null && typeof value === "object") return `{${Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(",")}}`;
2475
+ if (isKeyedReference(value)) return `{${Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(",")}}`;
2727
2476
  return JSON.stringify(value);
2728
2477
  }
2729
- /** Serialize a staticCss option for the engine; null when absent/empty. */
2478
+ /** Decided by an intrinsic only functions accept, so a spoofed
2479
+ * `Symbol.toStringTag` cannot pass a plain object off as a function. */
2480
+ function isCallable(value) {
2481
+ try {
2482
+ Function.prototype.toString.call(value);
2483
+ return true;
2484
+ } catch {
2485
+ return false;
2486
+ }
2487
+ }
2488
+ function isKeyedReference(value) {
2489
+ return Object(value) === value && !isCallable(value);
2490
+ }
2491
+ //#endregion
2492
+ //#region pipeline/static-css.ts
2493
+ /** The `staticCss` option declares emission the JSX scanner cannot observe
2494
+ * (config-driven variants, spread props); the engine applies it as usage. */
2730
2495
  function serializeStaticCss(config) {
2731
2496
  if (!config) return null;
2732
2497
  const json = stableStringify(config);
@@ -2734,14 +2499,6 @@ function serializeStaticCss(config) {
2734
2499
  }
2735
2500
  //#endregion
2736
2501
  //#region pipeline/prefix.ts
2737
- /**
2738
- * Apply namespace prefix to a variable map, CSS variable declarations, and theme JSON.
2739
- *
2740
- * Variable map: `{ "colors.ember": "--color-ember" }` -> `{ "colors.ember": "--prefix-color-ember" }`
2741
- * Variable CSS: `--color-ember: #FF2800` -> `--prefix-color-ember: #FF2800`
2742
- * `var(--color-ember)` -> `var(--prefix-color-ember)`
2743
- * Theme JSON: `{ "colors.ember": "var(--color-ember)" }` -> `{ "colors.ember": "var(--prefix-color-ember)" }`
2744
- */
2745
2502
  function applyPrefix(prefix, variableMapJson, variableCss, themeJson, contextualVarsJson) {
2746
2503
  if (!prefix) return {
2747
2504
  variableMapJson,
@@ -2772,14 +2529,8 @@ function applyPrefix(prefix, variableMapJson, variableCss, themeJson, contextual
2772
2529
  //#endregion
2773
2530
  //#region pipeline/system-config.ts
2774
2531
  /**
2775
- * Load a SystemInstance via the engine's `loadSystemModule` and normalize
2776
- * it into a SystemConfig. When `prefix` is set, CSS variable names in the
2777
- * variable map/css (and theme + contextual vars when affected) are
2778
- * namespaced via the shared `applyPrefix`.
2779
- *
2780
- * Error handling stays at the call site — the Vite plugin warns (or throws
2781
- * in strict mode) and keeps its previous config, the Next plugin lets the
2782
- * failure propagate.
2532
+ * Load and normalize a SystemInstance; `prefix` namespaces every CSS
2533
+ * variable name. Error handling stays at the call site.
2783
2534
  */
2784
2535
  function loadSystemConfig(engineApi, opts) {
2785
2536
  const { loadSystemModule } = engineApi();
@@ -2807,23 +2558,13 @@ function loadSystemConfig(engineApi, opts) {
2807
2558
  transformSourcesJson: config.transformSources || null,
2808
2559
  globalStyleBlocksJson: config.globalStyleBlocks || null,
2809
2560
  keyframesJson: config.keyframesBlocks || null,
2561
+ vocabularyWitnessesJson: config.vocabularyWitnesses || null,
2810
2562
  dependencies: config.dependencies ?? [],
2811
2563
  sourceThemeManifestsJson: config.sourceThemeManifests || null
2812
2564
  };
2813
2565
  }
2814
2566
  //#endregion
2815
2567
  //#region pipeline/structural-self-check.ts
2816
- /**
2817
- * Structural self-check over an emitted extraction result — the affirmative
2818
- * post-condition against silent-empty output (openspec:
2819
- * standalone-extraction-cli, "Silent-empty success is impossible").
2820
- *
2821
- * One shared implementation for every driver: the Vite plugin's `verify`
2822
- * option and the standalone CLI (default-on) both call this; the caller
2823
- * owns the failure POLICY (warn vs throw vs exit code), never the checks.
2824
- */
2825
- /** Run every structural check; returns human-readable failure lines
2826
- * (empty = pass). */
2827
2568
  function runStructuralSelfCheck(input) {
2828
2569
  const failures = [];
2829
2570
  if (input.componentCount === 0) failures.push("No component CSS produced — check the system file and its includes list");
@@ -2865,33 +2606,12 @@ function buildDynamicPropConfig(dynamicProps) {
2865
2606
  }
2866
2607
  //#endregion
2867
2608
  //#region pipeline/system-props-module.ts
2868
- /**
2869
- * Build the source of the runtime system-props module — served as
2870
- * `virtual:animus/system-props` by the Vite plugin and written as the
2871
- * session-scoped `system-props.js` artifact by the extraction session
2872
- * (`@animus-ui/extract/session`). One generator so the runtimes can never
2873
- * drift.
2874
- *
2875
- * Runtime transform functions for dynamic props are not yet supported
2876
- * (transforms are resolved at extraction time via boa_engine in Rust), so
2877
- * `transforms` defaults to an empty object literal.
2878
- *
2879
- * `dynamicPropConfig` entries carry the CSS property the slot class declares
2880
- * — the runtime's unit-fallback decision. Their field order and omission
2881
- * rules belong to buildDynamicPropConfig.
2882
- */
2883
2609
  function buildSystemPropsModule(opts) {
2884
2610
  const dynamicPropConfig = buildDynamicPropConfig(opts.dynamicProps);
2885
2611
  return `export const systemPropMap = ${opts.systemPropMapJson};\nexport const systemPropGroups = ${opts.groupRegistryJson};\nexport const dynamicPropConfig = ${JSON.stringify(dynamicPropConfig)};\nexport const transforms = ${opts.transformsSource ?? "{}"};\n`;
2886
2612
  }
2887
2613
  //#endregion
2888
2614
  //#region pipeline/timing-waterfall.ts
2889
- /**
2890
- * Rust-side timing phases as reported in `manifest.timing`, in waterfall
2891
- * order, with their display labels. Mirrors the timing struct in the
2892
- * extract-v2 crate — a renamed or added Rust phase is updated here once,
2893
- * not per plugin.
2894
- */
2895
2615
  const RUST_TIMING_PHASES = [
2896
2616
  ["parseAndWalk", "parse+walk"],
2897
2617
  ["importResolution", "imports"],
@@ -2905,11 +2625,6 @@ const RUST_TIMING_PHASES = [
2905
2625
  ["cssGeneration", "css-gen"],
2906
2626
  ["manifestSerialization", "serialize"]
2907
2627
  ];
2908
- /**
2909
- * Format the Rust timing waterfall as log lines. `indent` and `labelWidth`
2910
- * let each plugin keep its established column layout; the phase table and
2911
- * the `(N files, M cached)` suffix are shared.
2912
- */
2913
2628
  function formatRustTimingWaterfall(timing, opts) {
2914
2629
  const lines = [];
2915
2630
  for (const [key, label] of RUST_TIMING_PHASES) {
@@ -2923,21 +2638,9 @@ function formatRustTimingWaterfall(timing, opts) {
2923
2638
  //#endregion
2924
2639
  //#region pipeline/tsconfig-paths.ts
2925
2640
  /**
2926
- * tsconfig `paths` → alias pairs for the Rust `pathAliasesJson` contract
2927
- * (via `buildPathAliasesJson`). Used by drivers that cannot harvest aliases
2928
- * from a live bundler config — Turbopack exposes none.
2929
- *
2930
- * Semantics (deliberately the TypeScript subset that matters for import
2931
- * provenance): JSONC tolerated (comments + trailing commas); `extends`
2932
- * chains followed (string or array; missing parents and cycles skipped);
2933
- * the NEAREST config declaring `paths` wins wholesale (TS replaces the
2934
- * object, never merges); targets resolve against the nearest `baseUrl`
2935
- * (resolved from its declaring config's directory) or, absent one, the
2936
- * directory of the config declaring `paths`; first target per pattern.
2937
- * `x/*` patterns become prefix pairs, non-wildcard patterns exact pairs,
2938
- * bare `*` catch-alls and mid-pattern wildcards are skipped.
2641
+ * tsconfig `paths` → alias pairs, for drivers with no live bundler config.
2642
+ * The nearest config declaring `paths` wins wholesale — TS never merges.
2939
2643
  */
2940
- /** String-aware JSONC → JSON: strips // and block comments + trailing commas. */
2941
2644
  function stripJsonc(text) {
2942
2645
  let out = "";
2943
2646
  let inString = false;
@@ -2987,6 +2690,35 @@ function stripJsonc(text) {
2987
2690
  }
2988
2691
  return out.replace(/,(\s*[}\]])/g, "$1");
2989
2692
  }
2693
+ /** `Object(value) === value` holds for exactly the blocks and lists
2694
+ * `JSON.parse` produces; the `[object Object]` tag separates the two. */
2695
+ function isJsonBlock(value) {
2696
+ return Object(value) === value && Object.prototype.toString.call(value) === "[object Object]";
2697
+ }
2698
+ /** A block or a list. A declared `paths` OWNS the setting and blocks its
2699
+ * parents' even when it contributes nothing usable. */
2700
+ function isJsonKeyed(value) {
2701
+ return Object(value) === value;
2702
+ }
2703
+ /** A JSON string, excluding the boxed `String` object, which no path join
2704
+ * accepts. */
2705
+ function isJsonString(value) {
2706
+ return Object(value) !== value && Object.prototype.toString.call(value) === "[object String]";
2707
+ }
2708
+ function decodePaths(value) {
2709
+ if (!isJsonKeyed(value)) return null;
2710
+ const decoded = /* @__PURE__ */ new Map();
2711
+ for (const [pattern, targets] of Object.entries(value)) {
2712
+ const [first] = Array.isArray(targets) ? targets : [];
2713
+ if (isJsonString(first)) decoded.set(pattern, first);
2714
+ }
2715
+ return decoded;
2716
+ }
2717
+ /** TypeScript accepts one specifier or an array; a non-string member names
2718
+ * no config and is dropped here. */
2719
+ function decodeExtends(value) {
2720
+ return (Array.isArray(value) ? value : [value]).filter(isJsonString);
2721
+ }
2990
2722
  function readConfig(path) {
2991
2723
  let raw;
2992
2724
  try {
@@ -2994,16 +2726,20 @@ function readConfig(path) {
2994
2726
  } catch {
2995
2727
  return null;
2996
2728
  }
2729
+ let parsed;
2997
2730
  try {
2998
- const json = JSON.parse(stripJsonc(raw));
2999
- return {
3000
- dir: dirname(path),
3001
- compilerOptions: json.compilerOptions ?? {},
3002
- extends: json.extends
3003
- };
2731
+ parsed = JSON.parse(stripJsonc(raw));
3004
2732
  } catch {
3005
2733
  return null;
3006
2734
  }
2735
+ const root = isJsonBlock(parsed) ? parsed : {};
2736
+ const compilerOptions = isJsonBlock(root.compilerOptions) ? root.compilerOptions : {};
2737
+ return {
2738
+ dir: dirname$1(path),
2739
+ baseUrl: isJsonString(compilerOptions.baseUrl) ? compilerOptions.baseUrl : null,
2740
+ paths: decodePaths(compilerOptions.paths),
2741
+ extends: decodeExtends(root.extends)
2742
+ };
3007
2743
  }
3008
2744
  function resolveExtendsTarget(specifier, fromDir) {
3009
2745
  if (specifier.startsWith(".") || isAbsolute(specifier)) {
@@ -3011,7 +2747,7 @@ function resolveExtendsTarget(specifier, fromDir) {
3011
2747
  for (const candidate of [
3012
2748
  base,
3013
2749
  `${base}.json`,
3014
- join(base, "tsconfig.json")
2750
+ join$1(base, "tsconfig.json")
3015
2751
  ]) if (existsSync(candidate)) return candidate;
3016
2752
  return null;
3017
2753
  }
@@ -3031,15 +2767,14 @@ function loadChain(entryPath) {
3031
2767
  const visited = /* @__PURE__ */ new Set();
3032
2768
  const queue = [entryPath];
3033
2769
  while (queue.length > 0) {
3034
- const key = resolve(queue.shift());
2770
+ const path = queue.shift();
2771
+ const key = resolve(path);
3035
2772
  if (visited.has(key)) continue;
3036
2773
  visited.add(key);
3037
2774
  const node = readConfig(key);
3038
2775
  if (!node) continue;
3039
2776
  chain.push(node);
3040
- const parents = Array.isArray(node.extends) ? node.extends : node.extends !== void 0 ? [node.extends] : [];
3041
- for (const parent of parents) {
3042
- if (typeof parent !== "string") continue;
2777
+ for (const parent of node.extends) {
3043
2778
  const resolved = resolveExtendsTarget(parent, node.dir);
3044
2779
  if (resolved) queue.push(resolved);
3045
2780
  }
@@ -3047,22 +2782,20 @@ function loadChain(entryPath) {
3047
2782
  return chain;
3048
2783
  }
3049
2784
  /**
3050
- * Read the project's tsconfig path aliases as pairs consumable by
3051
- * `buildPathAliasesJson`. Returns an empty array when no readable tsconfig
3052
- * (or no usable `paths`) exists.
2785
+ * The project's tsconfig path aliases as `buildPathAliasesJson` pairs; empty
2786
+ * when no readable tsconfig, or no usable `paths`, exists.
3053
2787
  */
3054
2788
  function readTsconfigAliasPairs(rootDir) {
3055
- const chain = loadChain(join(rootDir, "tsconfig.json"));
2789
+ const chain = loadChain(join$1(rootDir, "tsconfig.json"));
3056
2790
  if (chain.length === 0) return [];
3057
- const pathsOwner = chain.find((node) => node.compilerOptions.paths && typeof node.compilerOptions.paths === "object");
3058
- if (!pathsOwner) return [];
3059
- const paths = pathsOwner.compilerOptions.paths;
3060
- const baseOwner = chain.find((node) => typeof node.compilerOptions.baseUrl === "string");
3061
- const base = baseOwner ? resolve(baseOwner.dir, baseOwner.compilerOptions.baseUrl) : pathsOwner.dir;
2791
+ const pathsOwner = chain.find((node) => node.paths !== null);
2792
+ const paths = pathsOwner?.paths ?? null;
2793
+ if (pathsOwner === void 0 || paths === null) return [];
2794
+ const baseOwner = chain.find((node) => node.baseUrl !== null);
2795
+ const baseUrl = baseOwner?.baseUrl ?? null;
2796
+ const base = baseOwner !== void 0 && baseUrl !== null ? resolve(baseOwner.dir, baseUrl) : pathsOwner.dir;
3062
2797
  const pairs = [];
3063
- for (const [pattern, targets] of Object.entries(paths)) {
3064
- const target = Array.isArray(targets) ? targets[0] : void 0;
3065
- if (typeof target !== "string") continue;
2798
+ for (const [pattern, target] of paths) {
3066
2799
  const patternStars = pattern.split("*").length - 1;
3067
2800
  const targetStars = target.split("*").length - 1;
3068
2801
  if (pattern === "*" || patternStars > 1 || targetStars > 1) continue;
@@ -3086,14 +2819,6 @@ function readTsconfigAliasPairs(rootDir) {
3086
2819
  }
3087
2820
  //#endregion
3088
2821
  //#region pipeline/error-diagnostics.ts
3089
- /**
3090
- * Throw when the diagnostics carry any `kind: "error"` entry — one
3091
- * aggregated `Error` naming EVERY offender (component, file, message — one
3092
- * line per entry, each prefixed `[animus]`). Warning-only diagnostics
3093
- * (`bail`/`skip`/`warn`, any severity) never trip this gate; their routing
3094
- * stays with `surfaceManifestDiagnostics`. Callers accept the manifest only
3095
- * past this point, so no stylesheet from a failing analysis is published.
3096
- */
3097
2822
  function assertNoErrorDiagnostics(diagnostics) {
3098
2823
  const errors = (diagnostics ?? []).filter((diagnostic) => diagnostic.kind === "error");
3099
2824
  if (errors.length === 0) return;
@@ -3103,33 +2828,29 @@ function assertNoErrorDiagnostics(diagnostics) {
3103
2828
  //#endregion
3104
2829
  //#region pipeline/replacement-plans.ts
3105
2830
  const snapshotByManifest = /* @__PURE__ */ new WeakMap();
2831
+ /** `null` (a context before its first analysis) snapshots to the empty plan
2832
+ * set — the same state as an empty universe. */
3106
2833
  function snapshotFilePlans(manifest) {
3107
- const memoKey = manifest !== null && typeof manifest === "object" ? manifest : null;
3108
- if (memoKey) {
3109
- const cached = snapshotByManifest.get(memoKey);
3110
- if (cached) return cached;
3111
- }
2834
+ if (manifest === null) return /* @__PURE__ */ new Map();
2835
+ const cached = snapshotByManifest.get(manifest);
2836
+ if (cached) return cached;
3112
2837
  const entriesByFile = /* @__PURE__ */ new Map();
3113
- for (const [id, desc] of Object.entries(manifest?.components ?? {})) {
3114
- const file = desc.file;
3115
- if (!file) continue;
3116
- const list = entriesByFile.get(file) ?? [];
3117
- list.push([id, desc.replacement ?? ""]);
3118
- entriesByFile.set(file, list);
2838
+ for (const [id, desc] of Object.entries(manifest.components)) {
2839
+ const list = entriesByFile.get(desc.file) ?? [];
2840
+ list.push([id, desc.replacement]);
2841
+ entriesByFile.set(desc.file, list);
3119
2842
  }
3120
2843
  const snapshot = /* @__PURE__ */ new Map();
3121
2844
  for (const [file, entries] of entriesByFile) {
3122
2845
  entries.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
3123
2846
  snapshot.set(file, JSON.stringify(entries));
3124
2847
  }
3125
- if (memoKey) snapshotByManifest.set(memoKey, snapshot);
2848
+ snapshotByManifest.set(manifest, snapshot);
3126
2849
  return snapshot;
3127
2850
  }
3128
2851
  /**
3129
- * Rel paths whose plan changed between two snapshots: absent→present,
3130
- * present→absent, and any change to membership or replacement content.
3131
- * `exclude` drops one path (the changed file itself on the edit path — its
3132
- * own modules are already in the update set).
2852
+ * Paths whose plan changed: absent↔present, membership, or content.
2853
+ * `exclude` drops one path whose own modules are already being updated.
3133
2854
  */
3134
2855
  function diffFilePlans(prev, next, opts) {
3135
2856
  const changed = /* @__PURE__ */ new Set();
@@ -3138,33 +2859,15 @@ function diffFilePlans(prev, next, opts) {
3138
2859
  if (opts?.exclude) changed.delete(opts.exclude);
3139
2860
  return [...changed].sort();
3140
2861
  }
3141
- /** Domain prefix keeping the epoch hash disjoint from every other
3142
- * contentHash use in the pipeline (design D5). */
2862
+ /** Domain prefix keeping this hash disjoint from every other `contentHash`
2863
+ * use in the pipeline. */
3143
2864
  const REPLACEMENT_PLANS_DOMAIN = "animus-replacement-plans-v1\0";
3144
- /** Section marker for the served-dependency witness. Leads with NUL: every
3145
- * plan entry ends `…\0` and neither file paths nor plan JSON contain NUL,
3146
- * so `\0\0` is unreachable from entry serialization alone — a snapshot can
3147
- * never forge the witness section. */
2865
+ /** Leads with NUL: entries end with `\0` and contain none, so `\0\0` is
2866
+ * unreachable from entries alone and the section cannot be forged. */
3148
2867
  const SERVED_DEPENDENCY_DOMAIN = "\0animus-served-dependency-v1\0";
3149
2868
  /**
3150
- * Canonical replacement epoch: contentHash over the domain-prefixed, sorted
3151
- * canonical serialization of a file-plan snapshot — each entry contributes
3152
- * `file + '\0' + plan + '\0'` (unambiguous: neither file paths nor the
3153
- * JSON plan strings contain NUL). Byte-identical plans → identical epoch;
3154
- * any membership/replacement/absent↔present change moves it.
3155
- *
3156
- * `servedDependencyWitness` extends the epoch to content the plans cannot
3157
- * see but restored modules depend on: transformed modules import the
3158
- * session's served system-props artifact, so a consumer passes that
3159
- * artifact's content here and an offline change to it moves the epoch —
3160
- * invalidating persistent-cache snapshots that reference the old epoch
3161
- * artifact. Absence and empty-string are distinct values.
3162
- *
3163
- * This is THE epoch definition — integrations consume this hash (or the
3164
- * snapshot diff) and never define their own. Epoch values compare only for
3165
- * equality (a moved value invalidates), so the serialization is free to
3166
- * evolve with a domain bump: sessions running different serializations
3167
- * simply disagree once and rebuild.
2869
+ * THE replacement epoch: integrations consume this hash, never their own.
2870
+ * `servedDependencyWitness` folds in content the plans cannot see.
3168
2871
  */
3169
2872
  function hashReplacementPlans(snapshot, servedDependencyWitness) {
3170
2873
  const entries = [...snapshot.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
@@ -3174,40 +2877,14 @@ function hashReplacementPlans(snapshot, servedDependencyWitness) {
3174
2877
  return contentHash(serialized);
3175
2878
  }
3176
2879
  //#endregion
3177
- //#region pipeline/utils.ts
3178
- /** Convert camelCase CSS property names to kebab-case. */
3179
- function camelToKebab(str) {
3180
- return str.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
3181
- }
3182
- //#endregion
3183
2880
  //#region pipeline/watch-keys.ts
3184
- /**
3185
- * Membership keys for a filesystem path in the system-dependency watch set.
3186
- *
3187
- * The loader reports symlink-resolved canonical paths; watcher events carry
3188
- * lexical absolute paths that may traverse symlinks (bun workspace links) and
3189
- * — for deletions — may no longer exist, so `realpathSync` on the event path
3190
- * alone cannot produce the canonical form. Both insertion and lookup go
3191
- * through this one normalizer: the lexical form plus a canonical form
3192
- * reconstructed through the nearest existing ancestor.
3193
- */
3194
2881
  function toWatchKeys(input) {
3195
2882
  const lexical = normalize(input);
3196
2883
  const canonical = realpathThroughNearestExistingAncestor(lexical);
3197
2884
  return canonical === lexical ? [lexical] : [lexical, canonical];
3198
2885
  }
3199
- /**
3200
- * Canonicalize a path that may not exist: walk up to the nearest existing
3201
- * ancestor, realpath it, and re-append the missing suffix. Handles deleted
3202
- * files under still-existing (possibly symlinked) directories and atomic
3203
- * unlink/rename saves. Falls back to the input when no ancestor resolves.
3204
- *
3205
- * Known limit: a symlink retargeted AFTER the system load canonicalizes to
3206
- * the new target, which cannot match the recorded (old-target) dependency —
3207
- * the membership test then fails open (no reset), identical to not having
3208
- * this reconstruction at all. Retarget sensitivity is a resolution-input
3209
- * concern, deliberately out of scope for the watch set.
3210
- */
2886
+ /** Canonicalizes a path that may not exist, through its nearest existing
2887
+ * ancestor. A symlink retargeted after load fails open: no match. */
3211
2888
  function realpathThroughNearestExistingAncestor(path) {
3212
2889
  try {
3213
2890
  return realpathSync$1(path);
@@ -3215,14 +2892,14 @@ function realpathThroughNearestExistingAncestor(path) {
3215
2892
  const suffix = [];
3216
2893
  let current = path;
3217
2894
  for (;;) {
3218
- const parent = dirname$1(current);
2895
+ const parent = dirname(current);
3219
2896
  if (parent === current) return path;
3220
- suffix.unshift(basename$1(current));
2897
+ suffix.unshift(basename(current));
3221
2898
  current = parent;
3222
2899
  try {
3223
- return join$1(realpathSync$1(current), ...suffix);
2900
+ return join(realpathSync$1(current), ...suffix);
3224
2901
  } catch {}
3225
2902
  }
3226
2903
  }
3227
2904
  //#endregion
3228
- export { CORE_OPTION_KEYS as $, createSourceIngestor as A, extractSystemFilePackages as B, resolveLightningTargets as C, resolveThroughPathAliases as D, resolveAssetFile as E, ENGINE_TRANSFORM_EXTENSIONS as F, staleDistIncludesMessage as G, firstOwners as H, preprocessMdx as I, createSourceIdentity as J, unresolvableIncludesMessage as K, adaptSvelteSource as L, isAdvisorySourceDiagnostic as M, withoutInvalidOriginals as N, findAssetSpecifiers as O, DEFAULT_EXTENSIONS as P, AnimusConfigError as Q, collectExternalPackageSources as R, postProcessCss as S, enforceExternalTokenContracts as T, isExcludedPackageRelativePath as U, findPackageRoot as V, resolveAbsolutePathSpecifier as W, sharesVolumeRoot as X, isPathWithinRoot as Y, discoverFiles as Z, runProjectAnalysis as _, stripLeadingLayerDeclaration as _t, snapshotFilePlans as a, createExcludeMatcher as at, surfaceManifestDiagnostics as b, formatRustTimingWaterfall as c, KEYFRAMES_EXTERNAL_ENTRY_FAILED as ct, runStructuralSelfCheck as d, RETIRED_ENGINE_MESSAGE as dt, DEFAULT_EXCLUDE as et, loadSystemConfig as f, assertNoRetiredEngineSelection as ft, clearEngineCache as g, assembleStylesheet as gt, buildAnalysisInputs as h, ANIMUS_LAYERS as ht, hashReplacementPlans as i, assertKnownOptionKeys as it, ingestSourceEntries as j, substituteAssetPlaceholders as k, buildSystemPropsModule as l, mergeExternalKeyframes as lt, serializeStaticCss as m, buildAnalyzeProjectArgs as mt, camelToKebab as n, REPLACEABLE_DEFAULT_EXCLUDE as nt, assertNoErrorDiagnostics as o, resolveMode as ot, applyPrefix as p, createV2EngineApi as pt, walkPackageSources as q, diffFilePlans as r, STRUCTURAL_EXCLUDE as rt, readTsconfigAliasPairs as s, KEYFRAMES_EXPORT_COLLISION as st, toWatchKeys as t, DRIVER_NAMESPACES as tt, buildDynamicPropConfig as u, contentHash as ut, applyUnitFallback as v, validateLayerOrder as vt, buildPathAliasesJson as w, unresolvedParentName as x, isUnresolvedParentDrop as y, __require as yt, excludeCollectedPackages as z };
2905
+ export { DEFAULT_EXCLUDE as $, enforceExternalTokenContracts as A, firstOwners as B, surfaceManifestDiagnostics as C, validateLayerOrder as Ct, postProcessCss as D, vocabularyWitnessDiagnostics as E, createSourceCorpus as F, walkPackageSources as G, resolveAbsolutePathSpecifier as H, collectExternalPackageSources as I, sharesVolumeRoot as J, createSourceIdentity as K, excludeCollectedPackages as L, resolveThroughPathAliases as M, findAssetSpecifiers as N, resolveLightningTargets as O, substituteAssetPlaceholders as P, CORE_OPTION_KEYS as Q, extractSystemFilePackages as R, isUnresolvedParentDrop as S, stripLeadingLayerDeclaration as St, unresolvedParentName as T, staleDistIncludesMessage as U, isExcludedPackageRelativePath as V, unresolvableIncludesMessage as W, discoverFiles as X, compareDiscoveryOrder as Y, AnimusConfigError as Z, clearEngineCache as _, preprocessMdx as _t, assertNoErrorDiagnostics as a, resolveMode as at, VOCABULARY_COLLISION as b, ANIMUS_LAYERS as bt, buildSystemPropsModule as c, createV2EngineApi as ct, loadSystemConfig as d, parseFilesJson as dt, DRIVER_NAMESPACES as et, applyPrefix as f, projectExternalFileOwners as ft, buildAnalysisInputs as g, isEngineTransformExtension as gt, stableStringify as h, ENGINE_TRANSFORM_EXTENSIONS as ht, snapshotFilePlans as i, createExcludeMatcher as it, resolveAssetFile as j, buildPathAliasesJson as k, buildDynamicPropConfig as l, ingestSourceEntries as lt, camelToKebab as m, DEFAULT_EXTENSIONS as mt, diffFilePlans as n, STRUCTURAL_EXCLUDE as nt, readTsconfigAliasPairs as o, RETIRED_ENGINE_MESSAGE as ot, serializeStaticCss as p, adaptSvelteSource as pt, isPathWithinRoot as q, hashReplacementPlans as r, assertKnownOptionKeys as rt, formatRustTimingWaterfall as s, assertNoRetiredEngineSelection as st, toWatchKeys as t, REPLACEABLE_DEFAULT_EXCLUDE as tt, runStructuralSelfCheck as u, isAdvisorySourceDiagnostic as ut, runProjectAnalysis as v, contentHash as vt, unreadableSourceDiagnostic as w, __require as wt, VOCABULARY_LEGACY_VERB as x, assembleStylesheet as xt, applyUnitFallback as y, buildAnalyzeProjectArgs as yt, findPackageRoot as z };