@animus-ui/extract 0.1.7 → 0.1.9

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.
package/dist/index.mjs CHANGED
@@ -1,13 +1,13 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { createHash } from "crypto";
3
3
  import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "fs";
4
- import { dirname, extname, isAbsolute, join, relative, resolve, sep } from "path";
4
+ import { dirname, extname, isAbsolute, join, relative, resolve } from "path";
5
+ import { realpathSync as realpathSync$1 } from "node:fs";
6
+ import path, { basename, dirname as dirname$1, join as join$1, normalize } from "node:path";
5
7
  import { createRequire as createRequire$1 } from "module";
6
8
  import browserslist from "browserslist";
7
9
  import { browserslistToTargets, transform } from "lightningcss";
8
10
  import { isUnitlessProperty } from "@animus-ui/properties";
9
- import { realpathSync as realpathSync$1 } from "node:fs";
10
- import { basename, dirname as dirname$1, join as join$1, normalize } from "node:path";
11
11
  //#region \0rolldown/runtime.js
12
12
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
13
13
  //#endregion
@@ -103,7 +103,8 @@ function buildAnalyzeProjectArgs(inputs) {
103
103
  inputs.keyframesJson,
104
104
  inputs.staticCssJson,
105
105
  inputs.conditionAliasesJson,
106
- inputs.externalDirsJson
106
+ inputs.externalDirsJson,
107
+ inputs.transformSourcesJson
107
108
  ];
108
109
  }
109
110
  //#endregion
@@ -122,7 +123,7 @@ function createV2EngineApi(deps) {
122
123
  return {
123
124
  loadSystemModule: (...args) => native.loadSystemModule(...args),
124
125
  scanKeyframesExports: (entryPath, rootDir) => native.scanKeyframesExports(entryPath, rootDir) ?? null,
125
- analyzeProject: (filesJsonRaw, scalesJson, variableMapJson, contextualVarsJson, propConfigJson, groupRegistryJson, packageResolutionJson, devMode, emitterConfigJson, selectorAliasesJson, _selectorOrderJson, globalStyleBlocksJson, pathAliasesJson, keyframesJson, staticCssJson, conditionAliasesJson, externalDirsJson) => {
126
+ analyzeProject: (filesJsonRaw, scalesJson, variableMapJson, contextualVarsJson, propConfigJson, groupRegistryJson, packageResolutionJson, devMode, emitterConfigJson, selectorAliasesJson, _selectorOrderJson, globalStyleBlocksJson, pathAliasesJson, keyframesJson, staticCssJson, conditionAliasesJson, externalDirsJson, transformSourcesJson) => {
126
127
  const filesJson = deps.rehydrateFilesJson ? deps.rehydrateFilesJson(filesJsonRaw) : filesJsonRaw;
127
128
  const sent = /* @__PURE__ */ new Map();
128
129
  for (const entry of JSON.parse(filesJson)) sent.set(entry.path, entry.source);
@@ -146,6 +147,7 @@ function createV2EngineApi(deps) {
146
147
  pathAliasesJson: pathAliasesJson ?? void 0,
147
148
  staticCssJson: staticCssJson ?? void 0,
148
149
  externalDirsJson: externalDirsJson ?? void 0,
150
+ transformSourcesJson: transformSourcesJson ?? void 0,
149
151
  devMode
150
152
  };
151
153
  const engine = new native.ExtractEngine(config);
@@ -317,6 +319,126 @@ function discoverFiles(dir, rootDir, excludePatterns, extensionsSet) {
317
319
  return results;
318
320
  }
319
321
  //#endregion
322
+ //#region pipeline/source-identity.ts
323
+ /**
324
+ * SourceId derivation authority + allowlist membership (openspec:
325
+ * external-source-watch-ingestion, design D1/D2/D5).
326
+ *
327
+ * One shared derivation produces the canonical source identity consumed by
328
+ * discovery, watch ingestion, ownership, deletion pruning, and diagnostics
329
+ * — raw event paths are lookup INPUTS only, so event spelling (symlink
330
+ * alias vs canonical, lexical vs realpath) can never fork identity.
331
+ *
332
+ * The containment and volume helpers are pure and path-API-injectable so
333
+ * Windows semantics are unit-testable via `path.win32` on any host; runtime
334
+ * callers use the ambient platform implementation.
335
+ */
336
+ /**
337
+ * Structural containment: `target` is the root itself or a descendant of it,
338
+ * decided by `relative()` shape — never by string prefixing, so `/ui` can
339
+ * never claim `/ui-old`, and a cross-drive win32 target (whose relative()
340
+ * result is absolute) is never contained.
341
+ */
342
+ function isPathWithinRoot(root, target, pathApi = path) {
343
+ const rel = pathApi.relative(root, target);
344
+ return rel === "" || !pathApi.isAbsolute(rel) && rel !== ".." && !rel.startsWith(".." + pathApi.sep);
345
+ }
346
+ /**
347
+ * Cross-volume gate (design D5): two paths share a platform path root
348
+ * (win32 drive letter or UNC share; always true on posix). Compared
349
+ * case-insensitively — win32 drive letters are case-insensitive.
350
+ */
351
+ function sharesVolumeRoot(a, b, pathApi = path) {
352
+ const volumeOf = (p) => pathApi.parse(pathApi.resolve(p)).root.toLowerCase();
353
+ return volumeOf(a) === volumeOf(b);
354
+ }
355
+ function canonicalize(p) {
356
+ try {
357
+ return realpathSync$1(p);
358
+ } catch {
359
+ return null;
360
+ }
361
+ }
362
+ function createSourceIdentity(rootDir) {
363
+ const normalizedRootDir = path.normalize(rootDir);
364
+ const canonicalRootDir = canonicalize(normalizedRootDir) ?? normalizedRootDir;
365
+ const roots = [];
366
+ const aliases = /* @__PURE__ */ new Map();
367
+ const owningRecord = (canonicalPath) => {
368
+ let owner = null;
369
+ for (const record of roots) {
370
+ if (!isPathWithinRoot(record.canonical, canonicalPath)) continue;
371
+ if (!owner || record.canonical.length > owner.canonical.length) owner = record;
372
+ }
373
+ return owner;
374
+ };
375
+ return {
376
+ rootDir: normalizedRootDir,
377
+ canonicalRootDir,
378
+ registerExternalRoot(root) {
379
+ const asRegistered = path.normalize(root);
380
+ const canonical = canonicalize(asRegistered) ?? asRegistered;
381
+ const existing = roots.find((record) => record.canonical === canonical);
382
+ if (existing) return existing.canonical;
383
+ roots.push({
384
+ asRegistered,
385
+ canonical
386
+ });
387
+ return canonical;
388
+ },
389
+ externalRoots() {
390
+ return roots.map((record) => record.canonical);
391
+ },
392
+ resolveSourceId(inputPath) {
393
+ const lexical = path.normalize(inputPath);
394
+ const canonical = canonicalize(lexical);
395
+ if (canonical === null) return null;
396
+ const owner = owningRecord(canonical);
397
+ const ownedBy = (record) => {
398
+ const pathInRoot = path.relative(record.canonical, canonical);
399
+ return {
400
+ sourceKey: path.relative(normalizedRootDir, path.join(record.asRegistered, pathInRoot)),
401
+ owningRoot: record.canonical,
402
+ pathInRoot
403
+ };
404
+ };
405
+ const ownerIsMoreSpecific = owner !== null && owner.canonical.length > canonicalRootDir.length;
406
+ let resolved;
407
+ if (!ownerIsMoreSpecific && isPathWithinRoot(canonicalRootDir, canonical)) {
408
+ const key = path.relative(canonicalRootDir, canonical);
409
+ resolved = {
410
+ sourceKey: key,
411
+ owningRoot: null,
412
+ pathInRoot: key
413
+ };
414
+ } else if (owner) resolved = ownedBy(owner);
415
+ else return null;
416
+ aliases.set(lexical, resolved);
417
+ aliases.set(canonical, resolved);
418
+ return resolved;
419
+ },
420
+ resolveDeletedSourceId(inputPath) {
421
+ return aliases.get(path.normalize(inputPath)) ?? null;
422
+ },
423
+ containingExternalRoot(inputPath) {
424
+ const lexical = path.normalize(inputPath);
425
+ let owner = null;
426
+ for (const record of roots) {
427
+ if (!(isPathWithinRoot(record.canonical, lexical) || isPathWithinRoot(record.asRegistered, lexical))) continue;
428
+ if (!owner || record.canonical.length > owner.canonical.length) owner = record;
429
+ }
430
+ if (owner) return owner.canonical;
431
+ const canonical = canonicalize(lexical);
432
+ if (canonical === null) return null;
433
+ for (const record of roots) {
434
+ if (!isPathWithinRoot(record.canonical, canonical)) continue;
435
+ if (!owner || record.canonical.length > owner.canonical.length) owner = record;
436
+ }
437
+ return owner?.canonical ?? null;
438
+ }
439
+ };
440
+ }
441
+ //#endregion
320
442
  //#region pipeline/discover-packages.ts
321
443
  /**
322
444
  * Walk up from a resolved package entry file to the nearest directory
@@ -341,6 +463,20 @@ const PACKAGE_SRC_EXCLUDES = [
341
463
  ".test.",
342
464
  ".spec."
343
465
  ];
466
+ /** True when a PACKAGE-RELATIVE path matches a package-source exclusion
467
+ * fragment (the "within the package" filter — see PACKAGE_SRC_EXCLUDES). */
468
+ function isExcludedPackageRelativePath(rel) {
469
+ return PACKAGE_SRC_EXCLUDES.some((pattern) => rel.includes(pattern));
470
+ }
471
+ /**
472
+ * THE package-source walk (guardrail G1 — one policy): shared discovery with
473
+ * no consumer patterns, then exclusion by package-relative path so fragments
474
+ * in the package's own location can't blank out its sources. Used by the
475
+ * collector, hosts' dirty-root rewalks, and watch-path classification alike.
476
+ */
477
+ function walkPackageSources(packageDir, extensionsSet) {
478
+ return discoverFiles(packageDir, packageDir, [], extensionsSet).filter((absPath) => !isExcludedPackageRelativePath(relative(packageDir, absPath)));
479
+ }
344
480
  /** Exclusions for the compiled-output fallback when a package does not ship src/. */
345
481
  const PACKAGE_OUTPUT_EXCLUDES = [
346
482
  "node_modules",
@@ -379,7 +515,7 @@ function sourceEntryForSpecifier(specifier, srcDir, extensionsSet) {
379
515
  if (isAbsolute(specifier)) {
380
516
  const resolved = resolveAbsolutePathSpecifier(specifier, extensionsSet);
381
517
  if (!resolved) return null;
382
- return !relative(srcDir, resolved).startsWith("..") ? resolved : null;
518
+ return isPathWithinRoot(srcDir, resolved) ? resolved : null;
383
519
  }
384
520
  const packageName = bareSpecifierPackageName(specifier);
385
521
  return resolveAbsolutePathSpecifier(join(srcDir, specifier.slice(packageName.length + 1) || "index"), extensionsSet);
@@ -393,7 +529,7 @@ function sourceEntryForSpecifier(specifier, srcDir, extensionsSet) {
393
529
  * file's mtime is readable. Detection only — reporting is the caller's policy.
394
530
  */
395
531
  function distEntryIsStale(absEntry, srcDir, srcFiles) {
396
- if (!relative(srcDir, absEntry).startsWith("..")) return false;
532
+ if (isPathWithinRoot(srcDir, absEntry)) return false;
397
533
  let distMtime;
398
534
  try {
399
535
  distMtime = statSync(absEntry).mtimeMs;
@@ -425,15 +561,20 @@ function distEntryIsStale(absEntry, srcDir, srcFiles) {
425
561
  * them — any warning or gate is the caller's policy.
426
562
  */
427
563
  async function collectExternalPackageSources(opts) {
428
- const { specifiers, resolveSpecifier, rootDir, extensionsSet, hasEntry, preprocessFile, onUnreadable } = opts;
564
+ const { specifiers, resolveSpecifier, rootDir, extensionsSet, hasEntry, preprocessFile, onUnreadable, onPackageResolved } = opts;
429
565
  const entries = [];
430
566
  const pushed = /* @__PURE__ */ new Set();
431
567
  const packageMap = {};
432
568
  const sourceEntries = /* @__PURE__ */ new Map();
569
+ const keyframesScanEntries = /* @__PURE__ */ new Map();
433
570
  const packageDirs = [];
434
- const dirOwners = {};
571
+ const dirOwnerSets = {};
572
+ const dirExtensions = {};
435
573
  const fileOwners = {};
436
574
  const outcomes = [];
575
+ const claimDir = (dir, specifier) => {
576
+ (dirOwnerSets[dir] ??= []).push(specifier);
577
+ };
437
578
  const alreadyIngested = (relPath) => hasEntry(relPath) || pushed.has(relPath);
438
579
  for (const specifier of specifiers) {
439
580
  let absEntry;
@@ -456,12 +597,15 @@ async function collectExternalPackageSources(opts) {
456
597
  let staleDist = false;
457
598
  if (existsSync(srcDir)) {
458
599
  packageDirs.push(srcDir);
459
- dirOwners[srcDir] ??= specifier;
600
+ claimDir(srcDir, specifier);
601
+ dirExtensions[srcDir] = [...extensionsSet];
602
+ onPackageResolved?.(specifier, srcDir);
460
603
  const srcEntry = sourceEntryForSpecifier(specifier, srcDir, extensionsSet);
461
604
  if (srcEntry) {
462
605
  packageMap[specifier] = relative(rootDir, srcEntry);
463
606
  sourceEntries.set(specifier, srcEntry);
464
607
  } else packageMap[specifier] = relative(rootDir, absEntry);
608
+ keyframesScanEntries.set(specifier, srcEntry ?? absEntry);
465
609
  if (!isAbsolute(specifier)) {
466
610
  const packageName = bareSpecifierPackageName(specifier);
467
611
  if (packageName !== specifier && !(packageName in packageMap)) {
@@ -469,13 +613,11 @@ async function collectExternalPackageSources(opts) {
469
613
  if (rootEntry) {
470
614
  packageMap[packageName] = relative(rootDir, rootEntry);
471
615
  sourceEntries.set(packageName, rootEntry);
616
+ keyframesScanEntries.set(packageName, rootEntry);
472
617
  }
473
618
  }
474
619
  }
475
- const pkgFiles = discoverFiles(srcDir, srcDir, [], extensionsSet).filter((absPath) => {
476
- const inPkg = relative(srcDir, absPath);
477
- return !PACKAGE_SRC_EXCLUDES.some((pattern) => inPkg.includes(pattern));
478
- });
620
+ const pkgFiles = walkPackageSources(srcDir, extensionsSet);
479
621
  staleDist = distEntryIsStale(absEntry, srcDir, pkgFiles);
480
622
  for (const pkgFile of pkgFiles) {
481
623
  const relPath = relative(rootDir, pkgFile);
@@ -503,10 +645,13 @@ async function collectExternalPackageSources(opts) {
503
645
  } else {
504
646
  const outputDir = dirname(absEntry);
505
647
  packageDirs.push(outputDir);
506
- dirOwners[outputDir] ??= specifier;
648
+ claimDir(outputDir, specifier);
649
+ onPackageResolved?.(specifier, outputDir);
507
650
  packageMap[specifier] = relative(rootDir, absEntry);
651
+ keyframesScanEntries.set(specifier, absEntry);
508
652
  const outputExtensions = new Set(extensionsSet);
509
653
  outputExtensions.add(extname(absEntry));
654
+ dirExtensions[outputDir] = [...outputExtensions];
510
655
  const outputFiles = discoverFiles(outputDir, outputDir, [], outputExtensions).filter((file) => {
511
656
  const relToOutput = relative(outputDir, file);
512
657
  return !PACKAGE_OUTPUT_EXCLUDES.some((pattern) => relToOutput.includes(pattern));
@@ -546,12 +691,78 @@ async function collectExternalPackageSources(opts) {
546
691
  entries,
547
692
  packageMap,
548
693
  sourceEntries,
694
+ keyframesScanEntries,
549
695
  packageDirs,
550
- dirOwners,
696
+ dirOwnerSets,
697
+ dirExtensions,
551
698
  fileOwners,
552
699
  outcomes
553
700
  };
554
701
  }
702
+ /** First-declared specifier per package dir — the single-value ownership
703
+ * view correlation consumers key on, derived from `dirOwnerSets`. */
704
+ function firstOwners(dirOwnerSets) {
705
+ const owners = {};
706
+ for (const [dir, specifiers] of Object.entries(dirOwnerSets)) if (specifiers.length > 0) owners[dir] = specifiers[0];
707
+ return owners;
708
+ }
709
+ /**
710
+ * Excise a set of rejected specifiers from a collection result ATOMICALLY
711
+ * (openspec: external-source-watch-ingestion, design D5): the returned copy
712
+ * carries no entries, package-map targets, source entries, package dirs, or
713
+ * ownership for the rejected packages — a package dir is removed only when
714
+ * EVERY specifier that claimed it is rejected (a dir-level rejection, such
715
+ * as the cross-volume gate, always rejects them together). Outcome records
716
+ * are reporting inputs, not membership, and pass through untouched.
717
+ */
718
+ function excludeCollectedPackages(collected, rejectedSpecifiers, rootDir) {
719
+ if (rejectedSpecifiers.size === 0) return collected;
720
+ const rejectedDirs = Object.entries(collected.dirOwnerSets).filter(([, specs]) => specs.every((s) => rejectedSpecifiers.has(s))).map(([dir]) => dir);
721
+ const underRejectedDir = (absPath) => rejectedDirs.some((dir) => isPathWithinRoot(dir, absPath));
722
+ const targetRejected = (specifier, absTarget) => rejectedSpecifiers.has(specifier) || underRejectedDir(absTarget);
723
+ const packageMap = {};
724
+ for (const [specifier, relTarget] of Object.entries(collected.packageMap)) {
725
+ if (targetRejected(specifier, resolve(rootDir, relTarget))) continue;
726
+ packageMap[specifier] = relTarget;
727
+ }
728
+ const sourceEntries = /* @__PURE__ */ new Map();
729
+ for (const [specifier, absEntry] of collected.sourceEntries) {
730
+ if (targetRejected(specifier, absEntry)) continue;
731
+ sourceEntries.set(specifier, absEntry);
732
+ }
733
+ const keyframesScanEntries = /* @__PURE__ */ new Map();
734
+ for (const [specifier, absEntry] of collected.keyframesScanEntries) {
735
+ if (targetRejected(specifier, absEntry)) continue;
736
+ keyframesScanEntries.set(specifier, absEntry);
737
+ }
738
+ const dirOwnerSets = {};
739
+ for (const [dir, specs] of Object.entries(collected.dirOwnerSets)) {
740
+ const kept = specs.filter((s) => !rejectedSpecifiers.has(s));
741
+ if (kept.length === 0) continue;
742
+ dirOwnerSets[dir] = kept;
743
+ }
744
+ const dirExtensions = {};
745
+ for (const [dir, exts] of Object.entries(collected.dirExtensions)) {
746
+ if (rejectedDirs.includes(dir)) continue;
747
+ dirExtensions[dir] = exts;
748
+ }
749
+ const fileOwners = {};
750
+ for (const [relPath, owner] of Object.entries(collected.fileOwners)) {
751
+ if (rejectedSpecifiers.has(owner)) continue;
752
+ fileOwners[relPath] = owner;
753
+ }
754
+ return {
755
+ entries: collected.entries.filter((entry) => !rejectedSpecifiers.has(collected.fileOwners[entry.path])),
756
+ packageMap,
757
+ sourceEntries,
758
+ keyframesScanEntries,
759
+ packageDirs: collected.packageDirs.filter((dir) => !rejectedDirs.includes(dir)),
760
+ dirOwnerSets,
761
+ dirExtensions,
762
+ fileOwners,
763
+ outcomes: collected.outcomes
764
+ };
765
+ }
555
766
  /**
556
767
  * Extract external DS package names from inheritance declarations in the
557
768
  * system file.
@@ -921,7 +1132,7 @@ function buildSourceTokenIndex(opts) {
921
1132
  if (existsSync(join(packageRoot, "package.json"))) addOwnerDir(packageRoot, specifier);
922
1133
  }
923
1134
  for (const [modulePath, exports] of Object.entries(manifests)) {
924
- const owner = realDirOwners.find(({ dir }) => modulePath === dir || modulePath.startsWith(dir + sep));
1135
+ const owner = realDirOwners.find(({ dir }) => isPathWithinRoot(dir, modulePath));
925
1136
  if (!owner) continue;
926
1137
  let tokens = index.get(owner.specifier);
927
1138
  if (!tokens) {
@@ -1062,16 +1273,42 @@ function postProcessCss(css, opts) {
1062
1273
  /** Stable code for selector forms with no substitutable subject.
1063
1274
  * Mirrors the Rust constant in `extract-v2/src/eval.rs`. */
1064
1275
  const SELECTOR_UNSUPPORTED_SUBJECT = "animus.selector.unsupported-subject";
1276
+ /** Unresolved-parent chain-drop message (mirrors the Rust diagnostic in
1277
+ * `extract-v2` — the ONE encoding of this message shape; consumers match
1278
+ * through the helpers below, never their own regex copies). */
1279
+ const UNRESOLVED_PARENT_RE = /chain dropped: could not resolve parent component '([^']+)'/;
1280
+ /** True when the diagnostic reports a chain dropped for an unresolved
1281
+ * parent component. */
1282
+ function isUnresolvedParentDrop(diagnostic) {
1283
+ return typeof diagnostic?.message === "string" && UNRESOLVED_PARENT_RE.test(diagnostic.message);
1284
+ }
1285
+ /** The parent binding named by an unresolved-parent drop, or null when the
1286
+ * diagnostic is not one. */
1287
+ function unresolvedParentName(diagnostic) {
1288
+ if (typeof diagnostic?.message !== "string") return null;
1289
+ return UNRESOLVED_PARENT_RE.exec(diagnostic.message)?.[1] ?? null;
1290
+ }
1065
1291
  /** True when the selector string carries at least one substitutable `&`
1066
1292
  * subject outside quoted text (mirrors the Rust `selector_subject` walk —
1067
1293
  * ancestor, leading, and repeated subjects all count; a `&` inside a
1068
1294
  * quoted attribute value does not). */
1069
1295
  function hasSelectorSubject(value) {
1070
1296
  let quote = null;
1071
- for (const c of value) if (quote !== null) {
1072
- if (c === quote) quote = null;
1073
- } else if (c === "\"" || c === "'") quote = c;
1074
- else if (c === "&") return true;
1297
+ let escaped = false;
1298
+ for (const c of value) {
1299
+ if (escaped) {
1300
+ escaped = false;
1301
+ continue;
1302
+ }
1303
+ if (c === "\\") {
1304
+ escaped = true;
1305
+ continue;
1306
+ }
1307
+ if (quote !== null) {
1308
+ if (c === quote) quote = null;
1309
+ } else if (c === "\"" || c === "'") quote = c;
1310
+ else if (c === "&") return true;
1311
+ }
1075
1312
  return false;
1076
1313
  }
1077
1314
  /**
@@ -1201,6 +1438,7 @@ function buildAnalysisInputs(opts) {
1201
1438
  keyframesJson: opts.system.keyframesJson,
1202
1439
  staticCssJson: opts.staticCssJson ?? null,
1203
1440
  conditionAliasesJson: opts.system.conditionAliasesJson ?? null,
1441
+ transformSourcesJson: opts.system.transformSourcesJson ?? null,
1204
1442
  externalDirsJson: opts.externalDirs?.length && hasSourceThemeManifests(opts.system) ? JSON.stringify(opts.externalDirs) : null
1205
1443
  };
1206
1444
  }
@@ -1342,6 +1580,7 @@ function loadSystemConfig(engineApi, opts) {
1342
1580
  contextualVarsJson,
1343
1581
  selectorAliasesJson: config.selectorAliases || null,
1344
1582
  conditionAliasesJson: config.conditionAliases || null,
1583
+ transformSourcesJson: config.transformSources || null,
1345
1584
  globalStyleBlocksJson: config.globalStyleBlocks || null,
1346
1585
  keyframesJson: config.keyframesBlocks || null,
1347
1586
  dependencies: config.dependencies ?? [],
@@ -1587,6 +1826,22 @@ function readTsconfigAliasPairs(rootDir) {
1587
1826
  return pairs;
1588
1827
  }
1589
1828
  //#endregion
1829
+ //#region pipeline/error-diagnostics.ts
1830
+ /**
1831
+ * Throw when the diagnostics carry any `kind: "error"` entry — one
1832
+ * aggregated `Error` naming EVERY offender (component, file, message — one
1833
+ * line per entry, each prefixed `[animus]`). Warning-only diagnostics
1834
+ * (`bail`/`skip`/`warn`, any severity) never trip this gate; their routing
1835
+ * stays with `surfaceManifestDiagnostics`. Callers accept the manifest only
1836
+ * past this point, so no stylesheet from a failing analysis is published.
1837
+ */
1838
+ function assertNoErrorDiagnostics(diagnostics) {
1839
+ const errors = (diagnostics ?? []).filter((diagnostic) => diagnostic.kind === "error");
1840
+ if (errors.length === 0) return;
1841
+ const lines = [...new Set(errors.map((diagnostic) => `[animus] ${diagnostic.component || "<unknown component>"} (${diagnostic.file || "<unknown file>"}): ${diagnostic.message || "<no message>"}`))];
1842
+ throw new Error(lines.join("\n"));
1843
+ }
1844
+ //#endregion
1590
1845
  //#region pipeline/mdx-preprocessor.ts
1591
1846
  /**
1592
1847
  * MDX source preprocessor for the extraction pipeline.
@@ -1644,6 +1899,79 @@ async function preprocessMdx(source, filename) {
1644
1899
  }
1645
1900
  }
1646
1901
  //#endregion
1902
+ //#region pipeline/replacement-plans.ts
1903
+ const snapshotByManifest = /* @__PURE__ */ new WeakMap();
1904
+ function snapshotFilePlans(manifest) {
1905
+ const memoKey = manifest !== null && typeof manifest === "object" ? manifest : null;
1906
+ if (memoKey) {
1907
+ const cached = snapshotByManifest.get(memoKey);
1908
+ if (cached) return cached;
1909
+ }
1910
+ const entriesByFile = /* @__PURE__ */ new Map();
1911
+ for (const [id, desc] of Object.entries(manifest?.components ?? {})) {
1912
+ const file = desc.file;
1913
+ if (!file) continue;
1914
+ const list = entriesByFile.get(file) ?? [];
1915
+ list.push([id, desc.replacement ?? ""]);
1916
+ entriesByFile.set(file, list);
1917
+ }
1918
+ const snapshot = /* @__PURE__ */ new Map();
1919
+ for (const [file, entries] of entriesByFile) {
1920
+ entries.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
1921
+ snapshot.set(file, JSON.stringify(entries));
1922
+ }
1923
+ if (memoKey) snapshotByManifest.set(memoKey, snapshot);
1924
+ return snapshot;
1925
+ }
1926
+ /**
1927
+ * Rel paths whose plan changed between two snapshots: absent→present,
1928
+ * present→absent, and any change to membership or replacement content.
1929
+ * `exclude` drops one path (the changed file itself on the edit path — its
1930
+ * own modules are already in the update set).
1931
+ */
1932
+ function diffFilePlans(prev, next, opts) {
1933
+ const changed = /* @__PURE__ */ new Set();
1934
+ for (const [file, plan] of next) if (prev.get(file) !== plan) changed.add(file);
1935
+ for (const file of prev.keys()) if (!next.has(file)) changed.add(file);
1936
+ if (opts?.exclude) changed.delete(opts.exclude);
1937
+ return [...changed].sort();
1938
+ }
1939
+ /** Domain prefix keeping the epoch hash disjoint from every other
1940
+ * contentHash use in the pipeline (design D5). */
1941
+ const REPLACEMENT_PLANS_DOMAIN = "animus-replacement-plans-v1\0";
1942
+ /** Section marker for the served-dependency witness. Leads with NUL: every
1943
+ * plan entry ends `…\0` and neither file paths nor plan JSON contain NUL,
1944
+ * so `\0\0` is unreachable from entry serialization alone — a snapshot can
1945
+ * never forge the witness section. */
1946
+ const SERVED_DEPENDENCY_DOMAIN = "\0animus-served-dependency-v1\0";
1947
+ /**
1948
+ * Canonical replacement epoch: contentHash over the domain-prefixed, sorted
1949
+ * canonical serialization of a file-plan snapshot — each entry contributes
1950
+ * `file + '\0' + plan + '\0'` (unambiguous: neither file paths nor the
1951
+ * JSON plan strings contain NUL). Byte-identical plans → identical epoch;
1952
+ * any membership/replacement/absent↔present change moves it.
1953
+ *
1954
+ * `servedDependencyWitness` extends the epoch to content the plans cannot
1955
+ * see but restored modules depend on: transformed modules import the
1956
+ * session's served system-props artifact, so a consumer passes that
1957
+ * artifact's content here and an offline change to it moves the epoch —
1958
+ * invalidating persistent-cache snapshots that reference the old epoch
1959
+ * artifact. Absence and empty-string are distinct values.
1960
+ *
1961
+ * This is THE epoch definition — integrations consume this hash (or the
1962
+ * snapshot diff) and never define their own. Epoch values compare only for
1963
+ * equality (a moved value invalidates), so the serialization is free to
1964
+ * evolve with a domain bump: sessions running different serializations
1965
+ * simply disagree once and rebuild.
1966
+ */
1967
+ function hashReplacementPlans(snapshot, servedDependencyWitness) {
1968
+ const entries = [...snapshot.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
1969
+ let serialized = REPLACEMENT_PLANS_DOMAIN;
1970
+ for (const [file, plan] of entries) serialized += file + "\0" + plan + "\0";
1971
+ if (servedDependencyWitness !== void 0) serialized += SERVED_DEPENDENCY_DOMAIN + servedDependencyWitness;
1972
+ return contentHash(serialized);
1973
+ }
1974
+ //#endregion
1647
1975
  //#region pipeline/utils.ts
1648
1976
  /** Convert camelCase CSS property names to kebab-case. */
1649
1977
  function camelToKebab(str) {
@@ -1695,4 +2023,4 @@ function realpathThroughNearestExistingAncestor(path) {
1695
2023
  }
1696
2024
  }
1697
2025
  //#endregion
1698
- export { ANIMUS_LAYERS, DEFAULT_EXTENSIONS, KEYFRAMES_EXPORT_COLLISION, KEYFRAMES_EXTERNAL_ENTRY_FAILED, RETIRED_ENGINE_MESSAGE, applyPrefix, applyUnitFallback, assembleStylesheet, assertNoRetiredEngineSelection, buildAnalysisInputs, buildAnalyzeProjectArgs, buildDynamicPropConfig, buildPathAliasesJson, buildSystemPropsModule, camelToKebab, clearEngineCache, collectExternalPackageSources, contentHash, createV2EngineApi, discoverFiles, enforceExternalTokenContracts, extractSystemFilePackages, findAssetSpecifiers, findPackageRoot, formatRustTimingWaterfall, loadSystemConfig, mergeExternalKeyframes, postProcessCss, preprocessMdx, readTsconfigAliasPairs, resolveAssetFile, resolveLightningTargets, resolveThroughPathAliases, runProjectAnalysis, serializeStaticCss, staleDistIncludesMessage, stripLeadingLayerDeclaration, substituteAssetPlaceholders, surfaceManifestDiagnostics, toWatchKeys, unresolvableIncludesMessage, validateLayerOrder };
2026
+ export { ANIMUS_LAYERS, DEFAULT_EXTENSIONS, KEYFRAMES_EXPORT_COLLISION, KEYFRAMES_EXTERNAL_ENTRY_FAILED, RETIRED_ENGINE_MESSAGE, applyPrefix, applyUnitFallback, assembleStylesheet, assertNoErrorDiagnostics, assertNoRetiredEngineSelection, buildAnalysisInputs, buildAnalyzeProjectArgs, buildDynamicPropConfig, buildPathAliasesJson, buildSystemPropsModule, camelToKebab, clearEngineCache, collectExternalPackageSources, contentHash, createSourceIdentity, createV2EngineApi, diffFilePlans, discoverFiles, enforceExternalTokenContracts, excludeCollectedPackages, extractSystemFilePackages, findAssetSpecifiers, findPackageRoot, firstOwners, formatRustTimingWaterfall, hashReplacementPlans, isExcludedPackageRelativePath, isPathWithinRoot, isUnresolvedParentDrop, loadSystemConfig, mergeExternalKeyframes, postProcessCss, preprocessMdx, readTsconfigAliasPairs, resolveAbsolutePathSpecifier, resolveAssetFile, resolveLightningTargets, resolveThroughPathAliases, runProjectAnalysis, serializeStaticCss, sharesVolumeRoot, snapshotFilePlans, staleDistIncludesMessage, stripLeadingLayerDeclaration, substituteAssetPlaceholders, surfaceManifestDiagnostics, toWatchKeys, unresolvableIncludesMessage, unresolvedParentName, validateLayerOrder, walkPackageSources };
@@ -15,6 +15,16 @@ export type ManifestDiagnostic = {
15
15
  /** Stable code for selector forms with no substitutable subject.
16
16
  * Mirrors the Rust constant in `extract-v2/src/eval.rs`. */
17
17
  export declare const SELECTOR_UNSUPPORTED_SUBJECT = "animus.selector.unsupported-subject";
18
+ /** True when the diagnostic reports a chain dropped for an unresolved
19
+ * parent component. */
20
+ export declare function isUnresolvedParentDrop(diagnostic: {
21
+ message?: unknown;
22
+ }): boolean;
23
+ /** The parent binding named by an unresolved-parent drop, or null when the
24
+ * diagnostic is not one. */
25
+ export declare function unresolvedParentName(diagnostic: {
26
+ message?: unknown;
27
+ }): string | null;
18
28
  export interface DiagnosticPolicy {
19
29
  /** When true, error-severity diagnostics throw instead of warning. */
20
30
  strict?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"manifest-diagnostics.d.ts","sourceRoot":"","sources":["../pipeline/manifest-diagnostics.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB;6EACyE;IACzE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4DAA4D;IAC5D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;sBACkB;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF;6DAC6D;AAC7D,eAAO,MAAM,4BAA4B,wCACF,CAAC;AAExC,MAAM,WAAW,gBAAgB;IAC/B,sEAAsE;IACtE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;gEAC4D;IAC5D,OAAO,CAAC,EAAE,kBAAkB,EAAE,CAAC;CAChC;AAED;;;wCAGwC;AACxC,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAYzD;AAED;;;;;;;GAOG;AACH,wBAAgB,+BAA+B,CAC7C,mBAAmB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC7C,kBAAkB,EAAE,CAuBtB;AAED;;;;;;;;;GASG;AACH,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE;IAAE,WAAW,CAAC,EAAE,kBAAkB,EAAE,CAAA;CAAE,EAChD,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,EAC/B,MAAM,GAAE,gBAAqB,GAC5B,IAAI,CA+BN"}
1
+ {"version":3,"file":"manifest-diagnostics.d.ts","sourceRoot":"","sources":["../pipeline/manifest-diagnostics.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB;6EACyE;IACzE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4DAA4D;IAC5D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;sBACkB;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF;6DAC6D;AAC7D,eAAO,MAAM,4BAA4B,wCACF,CAAC;AAQxC;wBACwB;AACxB,wBAAgB,sBAAsB,CAAC,UAAU,EAAE;IACjD,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,GAAG,OAAO,CAKV;AAED;6BAC6B;AAC7B,wBAAgB,oBAAoB,CAAC,UAAU,EAAE;IAC/C,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,GAAG,MAAM,GAAG,IAAI,CAGhB;AAED,MAAM,WAAW,gBAAgB;IAC/B,sEAAsE;IACtE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;gEAC4D;IAC5D,OAAO,CAAC,EAAE,kBAAkB,EAAE,CAAC;CAChC;AAED;;;wCAGwC;AACxC,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAqBzD;AAED;;;;;;;GAOG;AACH,wBAAgB,+BAA+B,CAC7C,mBAAmB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC7C,kBAAkB,EAAE,CAuBtB;AAED;;;;;;;;;GASG;AACH,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE;IAAE,WAAW,CAAC,EAAE,kBAAkB,EAAE,CAAA;CAAE,EAChD,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,EAC/B,MAAM,GAAE,gBAAqB,GAC5B,IAAI,CA+BN"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * File-plan snapshot/diff — the invalidation candidate source shared by
3
+ * every analysis path (openspec: dev-transform-coherence,
4
+ * dev-served-transform-coherence). A file's plan is the keyed, ordered set
5
+ * of (component id, replacement) pairs the manifest holds for it. A file
6
+ * with NO entries is ABSENT from the snapshot — a distinct state from a
7
+ * present entry whose replacement is empty, so raw↔extracted transitions
8
+ * are plan changes like any other.
9
+ *
10
+ * Lives in the shared pipeline package so the Vite diff consumer and the
11
+ * Next (webpack/Turbopack) epoch consumers share ONE semantic definition
12
+ * (design D5, next-webpack-served-transform-coherence).
13
+ */
14
+ export type FilePlanSnapshot = Map<string, string>;
15
+ export declare function snapshotFilePlans(manifest: any): FilePlanSnapshot;
16
+ /**
17
+ * Rel paths whose plan changed between two snapshots: absent→present,
18
+ * present→absent, and any change to membership or replacement content.
19
+ * `exclude` drops one path (the changed file itself on the edit path — its
20
+ * own modules are already in the update set).
21
+ */
22
+ export declare function diffFilePlans(prev: FilePlanSnapshot, next: FilePlanSnapshot, opts?: {
23
+ exclude?: string;
24
+ }): string[];
25
+ /**
26
+ * Canonical replacement epoch: contentHash over the domain-prefixed, sorted
27
+ * canonical serialization of a file-plan snapshot — each entry contributes
28
+ * `file + '\0' + plan + '\0'` (unambiguous: neither file paths nor the
29
+ * JSON plan strings contain NUL). Byte-identical plans → identical epoch;
30
+ * any membership/replacement/absent↔present change moves it.
31
+ *
32
+ * `servedDependencyWitness` extends the epoch to content the plans cannot
33
+ * see but restored modules depend on: transformed modules import the
34
+ * session's served system-props artifact, so a consumer passes that
35
+ * artifact's content here and an offline change to it moves the epoch —
36
+ * invalidating persistent-cache snapshots that reference the old epoch
37
+ * artifact. Absence and empty-string are distinct values.
38
+ *
39
+ * This is THE epoch definition — integrations consume this hash (or the
40
+ * snapshot diff) and never define their own. Epoch values compare only for
41
+ * equality (a moved value invalidates), so the serialization is free to
42
+ * evolve with a domain bump: sessions running different serializations
43
+ * simply disagree once and rebuild.
44
+ */
45
+ export declare function hashReplacementPlans(snapshot: FilePlanSnapshot, servedDependencyWitness?: string): string;
46
+ //# sourceMappingURL=replacement-plans.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"replacement-plans.d.ts","sourceRoot":"","sources":["../pipeline/replacement-plans.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,gBAAgB,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAQnD,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,GAAG,GAAG,gBAAgB,CA4BjE;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,gBAAgB,EACtB,IAAI,EAAE,gBAAgB,EACtB,IAAI,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAC1B,MAAM,EAAE,CAUV;AAYD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,gBAAgB,EAC1B,uBAAuB,CAAC,EAAE,MAAM,GAC/B,MAAM,CAYR"}
@@ -1 +1 @@
1
- {"version":3,"file":"run-analysis.d.ts","sourceRoot":"","sources":["../pipeline/run-analysis.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AACnE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAEpD;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,qBAAqB;IAEpC,QAAQ,EAAE,GAAG,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,2DAA2D;IAC3D,SAAS,EAAE,MAAM,CAAC;IAClB,4DAA4D;IAC5D,YAAY,EAAE,MAAM,CAAC;IACrB;6EACyE;IACzE,MAAM,EAAE,oBAAoB,CAAC;IAC7B,4DAA4D;IAC5D,OAAO,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CACtE;AAED,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACpE,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,MAAM,EAAE,YAAY,CAAC;IACrB,OAAO,EAAE,aAAa,CAAC;IACvB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,kEAAkE;IAClE,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,0EAA0E;IAC1E,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB;;8CAE0C;IAC1C,gBAAgB,CAAC,EAAE,OAAO,wBAAwB,EAAE,kBAAkB,EAAE,CAAC;CAC1E;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,eAAe,GACpB,oBAAoB,CAiCtB;AAQD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAEhC,SAAS,EAAE,MAAM,GAAG,EACpB,IAAI,EAAE,eAAe,GAAG;IAAE,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,GAC5E,qBAAqB,CAgCvB;AAED;;;;GAIG;AAEH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,GAAG,IAAI,CAQ3D"}
1
+ {"version":3,"file":"run-analysis.d.ts","sourceRoot":"","sources":["../pipeline/run-analysis.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AACnE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAEpD;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,qBAAqB;IAEpC,QAAQ,EAAE,GAAG,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,2DAA2D;IAC3D,SAAS,EAAE,MAAM,CAAC;IAClB,4DAA4D;IAC5D,YAAY,EAAE,MAAM,CAAC;IACrB;6EACyE;IACzE,MAAM,EAAE,oBAAoB,CAAC;IAC7B,4DAA4D;IAC5D,OAAO,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CACtE;AAED,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACpE,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,MAAM,EAAE,YAAY,CAAC;IACrB,OAAO,EAAE,aAAa,CAAC;IACvB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,kEAAkE;IAClE,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,0EAA0E;IAC1E,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB;;8CAE0C;IAC1C,gBAAgB,CAAC,EAAE,OAAO,wBAAwB,EAAE,kBAAkB,EAAE,CAAC;CAC1E;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,eAAe,GACpB,oBAAoB,CAkCtB;AAQD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAEhC,SAAS,EAAE,MAAM,GAAG,EACpB,IAAI,EAAE,eAAe,GAAG;IAAE,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,GAC5E,qBAAqB,CAgCvB;AAED;;;;GAIG;AAEH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,GAAG,IAAI,CAQ3D"}