@aura3d/cli 1.1.0 → 1.3.1

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.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
2
2
  import { createHash } from "node:crypto";
3
3
  import { dirname, extname, join, relative, resolve } from "node:path";
4
- export { cartoonCliAssetProfiles, getCartoonAssetProfileDefinition } from "./cartoon-asset-profiles.js";
4
+ export { animationCliAssetProfiles, getAnimationAssetProfileDefinition } from "./animation-asset-profiles.js";
5
+ export { validateAnimationAssets, parseAnimationClipMap, DEFAULT_ANIMATION_ACTIONS } from "./animation-asset-validator.js";
5
6
  export const DEFAULT_AURA_ASSET_MANIFEST = "aura.assets.json";
6
7
  export const DEFAULT_AURA_ASSET_OUTPUT_DIR = "public/aura-assets";
7
8
  export const DEFAULT_AURA_ASSET_PUBLIC_PATH = "/aura-assets/";
@@ -14,6 +15,9 @@ export function addAsset(options) {
14
15
  }
15
16
  const manifestPath = resolve(projectDir, DEFAULT_AURA_ASSET_MANIFEST);
16
17
  const current = readAssetManifest(projectDir);
18
+ // Re-adding (e.g. resolve overwriting) must not silently drop hand-authored
19
+ // provenance the new add does not re-supply (license/attribution/author/etc.).
20
+ const existing = current.assets.find((asset) => asset.id === options.name);
17
21
  const outputDir = normalizeRelativePath(options.outputDir ?? current.outputDir ?? DEFAULT_AURA_ASSET_OUTPUT_DIR);
18
22
  const publicPath = normalizePublicPath(options.publicPath ?? current.assetBasePath ?? DEFAULT_AURA_ASSET_PUBLIC_PATH);
19
23
  const typegen = normalizeRelativePath(options.typegen ?? current.typegen ?? DEFAULT_AURA_ASSET_TYPEGEN);
@@ -47,7 +51,7 @@ export function addAsset(options) {
47
51
  humanoid: inspection.humanoid,
48
52
  skeleton: inspection.skeleton,
49
53
  morphTargets: inspection.morphTargets,
50
- provenance: createAssetProvenance(projectDir, sourcePath, options, inspection.provenance),
54
+ provenance: createAssetProvenance(projectDir, sourcePath, options, mergeDetectedProvenance(existing?.provenance, inspection.provenance)),
51
55
  textures: inspection.textures,
52
56
  dependencies: inspection.dependencies,
53
57
  orientation: inspection.orientation,
@@ -305,8 +309,8 @@ export function checkDeploy(options = {}) {
305
309
  export function validateGameAssets(options = {}) {
306
310
  return validateAssetReadiness("game", options);
307
311
  }
308
- export function validateCartoonAssets(options = {}) {
309
- return validateAssetReadiness("cartoon", options);
312
+ export function validateAnimationStudioAssets(options = {}) {
313
+ return validateAssetReadiness("animation", options);
310
314
  }
311
315
  export function createCharacterAssemblyPlan(options) {
312
316
  const projectDir = resolve(options.projectDir ?? process.cwd());
@@ -468,7 +472,7 @@ function validateAssetReadiness(profile, options) {
468
472
  pushUnique(warnings, [...readinessIssues.warnings, ...profileIssues.warnings]);
469
473
  const gameReady = asset.type === "model" && Boolean(asset.bounds) && asset.materials.length > 0 && asset.sizeBytes <= 50 * 1024 * 1024 && readinessIssues.failures.length === 0 && profileIssues.failures.length === 0;
470
474
  const profileReady = profileTarget && gameReady;
471
- const cartoonReady = asset.type === "model"
475
+ const animationReady = asset.type === "model"
472
476
  ? Boolean(asset.bounds) && (asset.animations.length > 0 || /prop|set|stage|background|environment/i.test(asset.id))
473
477
  : asset.type === "audio" || asset.type === "texture";
474
478
  const artifactPaths = artifacts.assetFiles.find((entry) => entry.id === asset.id) ?? createReadinessAssetArtifacts(projectDir, manifest, asset);
@@ -499,7 +503,7 @@ function validateAssetReadiness(profile, options) {
499
503
  nodeNames: asset.nodeNames,
500
504
  artifactPaths,
501
505
  gameReady,
502
- cartoonReady,
506
+ animationReady,
503
507
  ...(gameProfile === "fighting-character" ? { profileTarget, profileReady, ...(profileSkippedReason ? { profileSkippedReason } : {}) } : {}),
504
508
  warnings: assetWarnings
505
509
  };
@@ -544,25 +548,25 @@ function validateAssetReadiness(profile, options) {
544
548
  }
545
549
  else {
546
550
  if (modelAssets.length === 0)
547
- failures.push("Cartoon readiness requires at least one typed model/set/prop GLB or GLTF.");
551
+ failures.push("Animation readiness requires at least one typed model/set/prop GLB or GLTF.");
548
552
  if (animatedModels.length === 0)
549
553
  warnings.push("No animated character clips detected. Prompt-to-episode output can use transform animation, but character acting needs skeletal or pose clips.");
550
554
  if (animatedModels.length > 0 && humanoidModels === 0)
551
- warnings.push("No humanoid model metadata detected. Acting-heavy cartoon routes should confirm character rigs with assets inspect --humanoid.");
555
+ warnings.push("No humanoid model metadata detected. Acting-heavy animation routes should confirm character rigs with assets inspect --humanoid.");
552
556
  const audioAssets = manifest.assets.filter((asset) => asset.type === "audio");
553
557
  if (audioAssets.length === 0)
554
558
  warnings.push("No audio assets detected. AuraVoice bridge can still reference external narration manifests, but local episode proof is stronger with audio registered.");
555
559
  }
556
- const cartoonEpisode = profile === "cartoon"
557
- ? createCartoonEpisodeReadinessReport(manifest, assets, options)
560
+ const animationEpisode = profile === "animation"
561
+ ? createAnimationEpisodeReadinessReport(manifest, assets, options)
558
562
  : undefined;
559
- if (cartoonEpisode && options.episode) {
560
- pushUnique(failures, cartoonEpisode.failures);
561
- pushUnique(warnings, cartoonEpisode.warnings);
563
+ if (animationEpisode && options.episode) {
564
+ pushUnique(failures, animationEpisode.failures);
565
+ pushUnique(warnings, animationEpisode.warnings);
562
566
  }
563
567
  const ok = failures.length === 0;
564
568
  const baseMessage = ok
565
- ? `${profile === "game" ? "Game" : "Cartoon"} asset readiness report completed.`
569
+ ? `${profile === "game" ? "Game" : "Animation"} asset readiness report completed.`
566
570
  : failures;
567
571
  const messages = [
568
572
  ...(Array.isArray(baseMessage) ? baseMessage : [baseMessage]),
@@ -579,7 +583,7 @@ function validateAssetReadiness(profile, options) {
579
583
  manifestPath,
580
584
  artifacts,
581
585
  contracts: createReadinessValidationContracts(profile),
582
- ...(cartoonEpisode ? { cartoonEpisode } : {}),
586
+ ...(animationEpisode ? { animationEpisode } : {}),
583
587
  summary: {
584
588
  totalAssets: manifest.assets.length,
585
589
  modelAssets: modelAssets.length,
@@ -589,14 +593,14 @@ function validateAssetReadiness(profile, options) {
589
593
  environmentAssets: manifest.assets.filter((asset) => asset.type === "environment").length,
590
594
  animationClips,
591
595
  humanoidModels,
592
- ...(cartoonEpisode
596
+ ...(animationEpisode
593
597
  ? {
594
- cartoonCharacters: cartoonEpisode.selectedCharacters.length,
595
- cartoonSets: cartoonEpisode.selectedSets.length,
596
- cartoonProps: cartoonEpisode.selectedProps.length,
597
- episodeReadyCharacters: cartoonEpisode.readiness.filter((entry) => entry.role === "character" && entry.episodeReady).length,
598
- mouthReadyCharacters: cartoonEpisode.readiness.filter((entry) => entry.role === "character" && entry.mouthReady).length,
599
- animationReadyCharacters: cartoonEpisode.readiness.filter((entry) => entry.role === "character" && entry.animationReady).length
598
+ animationCharacters: animationEpisode.selectedCharacters.length,
599
+ animationSets: animationEpisode.selectedSets.length,
600
+ animationProps: animationEpisode.selectedProps.length,
601
+ episodeReadyCharacters: animationEpisode.readiness.filter((entry) => entry.role === "character" && entry.episodeReady).length,
602
+ mouthReadyCharacters: animationEpisode.readiness.filter((entry) => entry.role === "character" && entry.mouthReady).length,
603
+ animationReadyCharacters: animationEpisode.readiness.filter((entry) => entry.role === "character" && entry.animationReady).length
600
604
  }
601
605
  : {}),
602
606
  ...(gameProfile === "fighting-character"
@@ -660,9 +664,9 @@ function createReadinessValidatorEvidence(profile) {
660
664
  label: "AuraClash game asset validator"
661
665
  }
662
666
  : {
663
- id: "aura-voice-cartoon-assets",
664
- command: "assets validate-cartoon",
665
- label: "AuraVoice cartoon asset validator"
667
+ id: "aura-voice-animation-assets",
668
+ command: "assets validate-animation",
669
+ label: "AuraVoice animation asset validator"
666
670
  };
667
671
  }
668
672
  function createReadinessValidationContracts(profile) {
@@ -694,11 +698,11 @@ function createReadinessValidationContracts(profile) {
694
698
  }
695
699
  return [
696
700
  {
697
- id: "auravoice-cartoon-character-asset-validation-contract",
698
- label: "AuraVoice cartoon asset validation contract",
699
- profile: "cartoon",
701
+ id: "auravoice-animation-character-asset-validation-contract",
702
+ label: "AuraVoice animation asset validation contract",
703
+ profile: "animation",
700
704
  sourceFamily: "AuraVoice",
701
- intendedUse: "cartoon-character",
705
+ intendedUse: "animation-character",
702
706
  sourceOnly: true,
703
707
  requiredChecks: [
704
708
  "typed Aura model, texture, audio, or environment asset entry",
@@ -706,7 +710,7 @@ function createReadinessValidationContracts(profile) {
706
710
  "animation or transform-ready character metadata",
707
711
  "audio or external AuraVoice manifest references for stronger episode proof"
708
712
  ],
709
- evidenceBoundary: "This CLI contract is source-only. It does not prove cartoon route readiness until validate-cartoon output, rendered frames, timing proof, and AuraVoice evidence are archived."
713
+ evidenceBoundary: "This CLI contract is source-only. It does not prove animation route readiness until validate-animation output, rendered frames, timing proof, and AuraVoice evidence are archived."
710
714
  }
711
715
  ];
712
716
  }
@@ -744,8 +748,8 @@ function createReadinessAnimationMetadata(animations) {
744
748
  clips: animations.map((name, index) => ({ index, name }))
745
749
  };
746
750
  }
747
- function createCartoonEpisodeReadinessReport(manifest, assets, options) {
748
- const readiness = assets.map(createCartoonEpisodeAssetReadiness);
751
+ function createAnimationEpisodeReadinessReport(manifest, assets, options) {
752
+ const readiness = assets.map(createAnimationEpisodeAssetReadiness);
749
753
  const characters = readiness.filter((entry) => entry.role === "character");
750
754
  const sets = readiness.filter((entry) => entry.role === "set");
751
755
  const props = readiness.filter((entry) => entry.role === "prop");
@@ -756,26 +760,26 @@ function createCartoonEpisodeReadinessReport(manifest, assets, options) {
756
760
  const readyCharacters = characters.filter((entry) => entry.episodeReady);
757
761
  const readySets = sets.filter((entry) => entry.episodeReady);
758
762
  if (characters.length < 2) {
759
- failures.push(`cartoon episode validation requires at least 2 typed cartoon character assets; found ${characters.length}${characters.length ? `: ${characters.map((entry) => entry.id).join(", ")}` : ""}.`);
763
+ failures.push(`animation episode validation requires at least 2 typed animation character assets; found ${characters.length}${characters.length ? `: ${characters.map((entry) => entry.id).join(", ")}` : ""}.`);
760
764
  }
761
765
  if (characters.length >= 2 && distinctCharacterHashes.size < characters.length) {
762
- failures.push("cartoon episode validation requires distinct character files/hashes; duplicated character assets cannot prove a two-character episode cast.");
766
+ failures.push("animation episode validation requires distinct character files/hashes; duplicated character assets cannot prove a two-character episode cast.");
763
767
  }
764
768
  if (readyCharacters.length < 2) {
765
- failures.push(`cartoon episode validation found only ${readyCharacters.length} episode-ready character asset${readyCharacters.length === 1 ? "" : "s"}; required 2 with license, provenance, animation, and mouth readiness.`);
769
+ failures.push(`animation episode validation found only ${readyCharacters.length} episode-ready character asset${readyCharacters.length === 1 ? "" : "s"}; required 2 with license, provenance, animation, and mouth readiness.`);
766
770
  }
767
771
  if (sets.length < 1) {
768
- failures.push("cartoon episode validation requires at least 1 typed cartoon set/location asset.");
772
+ failures.push("animation episode validation requires at least 1 typed animation set/location asset.");
769
773
  }
770
774
  if (readySets.length < 1) {
771
- failures.push("cartoon episode validation requires at least 1 episode-ready set with license, provenance, bounds, readable materials, and walkable scale.");
775
+ failures.push("animation episode validation requires at least 1 episode-ready set with license, provenance, bounds, readable materials, and walkable scale.");
772
776
  }
773
777
  for (const entry of readiness) {
774
778
  pushUnique(failures, entry.failures);
775
779
  pushUnique(warnings, entry.warnings);
776
780
  }
777
781
  if (audio.length === 0) {
778
- warnings.push("cartoon episode validation found no typed audio assets; publish-ready dialogue/audio proof will need registered dialogue, music, or SFX stems.");
782
+ warnings.push("animation episode validation found no typed audio assets; publish-ready dialogue/audio proof will need registered dialogue, music, or SFX stems.");
779
783
  }
780
784
  const evidenceDirectory = options.output ? dirname(normalizeRelativePath(options.output)) : "artifacts/aura3d";
781
785
  return {
@@ -801,56 +805,56 @@ function createCartoonEpisodeReadinessReport(manifest, assets, options) {
801
805
  warnings
802
806
  };
803
807
  }
804
- function createCartoonEpisodeAssetReadiness(asset) {
805
- const role = inferCartoonEpisodeAssetRole(asset);
808
+ function createAnimationEpisodeAssetReadiness(asset) {
809
+ const role = inferAnimationEpisodeAssetRole(asset);
806
810
  const failures = [];
807
811
  const warnings = [];
808
812
  const provenanceReady = Boolean(asset.provenance?.sourceUrl || asset.provenance?.sourceFamily || asset.provenance?.sourcePath);
809
- const animationReady = role === "character" ? isCartoonCharacterAnimationReady(asset) : asset.animations.length > 0;
810
- const mouthMode = role === "character" ? inferCartoonMouthMode(asset) : undefined;
813
+ const animationReady = role === "character" ? isAnimationCharacterAnimationReady(asset) : asset.animations.length > 0;
814
+ const mouthMode = role === "character" ? inferAnimationMouthMode(asset) : undefined;
811
815
  const mouthReady = role === "character" ? mouthMode !== "missing-mouth-motion" : false;
812
- const setReady = role === "set" ? isCartoonSetReady(asset) : undefined;
816
+ const setReady = role === "set" ? isAnimationSetReady(asset) : undefined;
813
817
  if (role === "character") {
814
818
  if (asset.type !== "model")
815
- failures.push(`${asset.id}: cartoon character must be a typed model asset, found ${asset.type}.`);
819
+ failures.push(`${asset.id}: animation character must be a typed model asset, found ${asset.type}.`);
816
820
  if (!asset.licenseVerified)
817
- failures.push(`${asset.id}: cartoon character requires verified redistributable license evidence.`);
821
+ failures.push(`${asset.id}: animation character requires verified redistributable license evidence.`);
818
822
  if (!provenanceReady)
819
- failures.push(`${asset.id}: cartoon character requires source provenance for episode packaging.`);
823
+ failures.push(`${asset.id}: animation character requires source provenance for episode packaging.`);
820
824
  if (!asset.placeholderFree)
821
- failures.push(`${asset.id}: cartoon character is placeholder-tagged and cannot satisfy episode-ready validation.`);
825
+ failures.push(`${asset.id}: animation character is placeholder-tagged and cannot satisfy episode-ready validation.`);
822
826
  if (!asset.boundsMetadata && !asset.bounds)
823
- failures.push(`${asset.id}: cartoon character requires bounds metadata for framing and motion analysis.`);
827
+ failures.push(`${asset.id}: animation character requires bounds metadata for framing and motion analysis.`);
824
828
  if (!animationReady)
825
- failures.push(`${asset.id}: cartoon character requires embedded animation clips or explicit segmented-rig metadata.`);
829
+ failures.push(`${asset.id}: animation character requires embedded animation clips or explicit segmented-rig metadata.`);
826
830
  if (!mouthReady)
827
- failures.push(`${asset.id}: cartoon character requires blendshape, mouth-card, viseme, talk, face, or primitive mouth fallback metadata.`);
831
+ failures.push(`${asset.id}: animation character requires blendshape, mouth-card, viseme, talk, face, or primitive mouth fallback metadata.`);
828
832
  if (asset.materials.length === 0)
829
- failures.push(`${asset.id}: cartoon character requires at least one readable material.`);
833
+ failures.push(`${asset.id}: animation character requires at least one readable material.`);
830
834
  if (asset.sizeBytes > 45 * 1024 * 1024)
831
- failures.push(`${asset.id}: cartoon character payload is ${asset.sizeBytes} bytes; expected <= 47185920 for browser episode playback.`);
835
+ failures.push(`${asset.id}: animation character payload is ${asset.sizeBytes} bytes; expected <= 47185920 for browser episode playback.`);
832
836
  if (mouthMode === "amplitude-only")
833
837
  warnings.push(`${asset.id}: mouth readiness is amplitude-only; add blendshape or mouth-card metadata for stronger lip-sync proof.`);
834
838
  }
835
839
  else if (role === "set") {
836
840
  if (asset.type !== "model")
837
- failures.push(`${asset.id}: cartoon set must be a typed model asset, found ${asset.type}.`);
841
+ failures.push(`${asset.id}: animation set must be a typed model asset, found ${asset.type}.`);
838
842
  if (!asset.licenseVerified)
839
- failures.push(`${asset.id}: cartoon set requires verified redistributable license evidence.`);
843
+ failures.push(`${asset.id}: animation set requires verified redistributable license evidence.`);
840
844
  if (!provenanceReady)
841
- failures.push(`${asset.id}: cartoon set requires source provenance for episode packaging.`);
845
+ failures.push(`${asset.id}: animation set requires source provenance for episode packaging.`);
842
846
  if (!asset.placeholderFree)
843
- failures.push(`${asset.id}: cartoon set is placeholder-tagged and cannot satisfy episode-ready validation.`);
847
+ failures.push(`${asset.id}: animation set is placeholder-tagged and cannot satisfy episode-ready validation.`);
844
848
  if (!setReady)
845
- failures.push(`${asset.id}: cartoon set requires bounds, readable materials, and at least 1.5m walkable/framing scale.`);
849
+ failures.push(`${asset.id}: animation set requires bounds, readable materials, and at least 1.5m walkable/framing scale.`);
846
850
  if (asset.sizeBytes > 90 * 1024 * 1024)
847
- failures.push(`${asset.id}: cartoon set payload is ${asset.sizeBytes} bytes; expected <= 94371840 for browser episode playback.`);
851
+ failures.push(`${asset.id}: animation set payload is ${asset.sizeBytes} bytes; expected <= 94371840 for browser episode playback.`);
848
852
  }
849
853
  else if (role === "prop" || role === "environment") {
850
854
  if (!asset.licenseVerified)
851
- warnings.push(`${asset.id}: optional cartoon ${role} lacks verified license evidence; publish packages should include provenance for every visible asset.`);
855
+ warnings.push(`${asset.id}: optional animation ${role} lacks verified license evidence; publish packages should include provenance for every visible asset.`);
852
856
  if (!provenanceReady)
853
- warnings.push(`${asset.id}: optional cartoon ${role} lacks source provenance.`);
857
+ warnings.push(`${asset.id}: optional animation ${role} lacks source provenance.`);
854
858
  }
855
859
  return {
856
860
  id: asset.id,
@@ -872,12 +876,12 @@ function createCartoonEpisodeAssetReadiness(asset) {
872
876
  failures
873
877
  };
874
878
  }
875
- function inferCartoonEpisodeAssetRole(asset) {
879
+ function inferAnimationEpisodeAssetRole(asset) {
876
880
  if (asset.type === "audio")
877
881
  return "audio";
878
882
  if (asset.type === "texture")
879
883
  return "texture";
880
- const text = cartoonAssetText(asset);
884
+ const text = animationAssetText(asset);
881
885
  if (asset.type === "environment" || /\b(environment|skybox|backdrop|background|world|terrain|horizon)\b/i.test(text))
882
886
  return "environment";
883
887
  if (/\b(set|stage|room|garden|park|school|street|house|classroom|moon garden|walkable|floor|location)\b/i.test(text))
@@ -894,17 +898,17 @@ function inferCartoonEpisodeAssetRole(asset) {
894
898
  return "prop";
895
899
  return "unknown";
896
900
  }
897
- function isCartoonCharacterAnimationReady(asset) {
901
+ function isAnimationCharacterAnimationReady(asset) {
898
902
  if (asset.animations.length > 0)
899
903
  return true;
900
- return /\b(segmented rig|segmented-rig|puppet rig|body parts|armature|skinned|skeleton)\b/i.test(cartoonAssetText(asset));
904
+ return /\b(segmented rig|segmented-rig|puppet rig|body parts|armature|skinned|skeleton)\b/i.test(animationAssetText(asset));
901
905
  }
902
- function inferCartoonMouthMode(asset) {
906
+ function inferAnimationMouthMode(asset) {
903
907
  const morphNames = asset.morphTargets?.targetNames ?? [];
904
908
  if (morphNames.some((name) => /\b(aa|oh|ee|mouth|lip|jaw|viseme|smile|frown|blink|expression)\b/i.test(name))) {
905
909
  return "blendshape-lip-sync";
906
910
  }
907
- const text = cartoonAssetText(asset);
911
+ const text = animationAssetText(asset);
908
912
  if (/\b(primitive mouth|mouth card|mouth-card|mouth sprite|face card|viseme card|2d mouth)\b/i.test(text)) {
909
913
  return "primitive-mouth-card";
910
914
  }
@@ -913,14 +917,14 @@ function inferCartoonMouthMode(asset) {
913
917
  }
914
918
  return "missing-mouth-motion";
915
919
  }
916
- function isCartoonSetReady(asset) {
920
+ function isAnimationSetReady(asset) {
917
921
  const size = asset.boundsMetadata?.size ?? asset.bounds;
918
922
  if (!size)
919
923
  return false;
920
924
  const largest = Math.max(...size);
921
925
  return largest >= 1.5 && asset.materials.length > 0;
922
926
  }
923
- function cartoonAssetText(asset) {
927
+ function animationAssetText(asset) {
924
928
  return [
925
929
  asset.id,
926
930
  asset.source,
@@ -1553,10 +1557,35 @@ function createAssetProvenance(projectDir, sourcePath, options, detected) {
1553
1557
  ...(options.author ?? detected?.author ? { author: options.author ?? detected?.author } : {}),
1554
1558
  ...(options.sourceFamily ?? detected?.sourceFamily ? { sourceFamily: options.sourceFamily ?? detected?.sourceFamily } : {}),
1555
1559
  ...(options.attribution ?? detected?.attribution ? { attribution: options.attribution ?? detected?.attribution } : {}),
1560
+ ...(options.sha256 ?? detected?.sha256 ? { sha256: options.sha256 ?? detected?.sha256 } : {}),
1561
+ ...(options.retrievedAt ?? detected?.retrievedAt ? { retrievedAt: options.retrievedAt ?? detected?.retrievedAt } : {}),
1556
1562
  ...(detected?.evidence && detected.evidence.length > 0 ? { evidence: detected.evidence } : {}),
1557
1563
  checkedAt: new Date().toISOString()
1558
1564
  };
1559
1565
  }
1566
+ /**
1567
+ * Merge a freshly inspected asset's auto-detected provenance over the entry that
1568
+ * already exists in the manifest, so a re-add/re-resolve preserves hand-authored
1569
+ * fields (license, attribution, author, sourceUrl, sourceFamily, evidence,
1570
+ * sha256) that the new pass did not re-detect. Freshly detected values win over
1571
+ * the prior entry; explicit `addAsset` options still win over both (applied in
1572
+ * {@link createAssetProvenance}). (#26)
1573
+ */
1574
+ function mergeDetectedProvenance(existing, detected) {
1575
+ if (!existing)
1576
+ return detected;
1577
+ const evidence = [...(detected?.evidence ?? []), ...(existing.evidence ?? [])];
1578
+ return {
1579
+ sourceUrl: detected?.sourceUrl ?? existing.sourceUrl,
1580
+ license: detected?.license ?? existing.license,
1581
+ author: detected?.author ?? existing.author,
1582
+ sourceFamily: detected?.sourceFamily ?? existing.sourceFamily,
1583
+ attribution: detected?.attribution ?? existing.attribution,
1584
+ sha256: detected?.sha256 ?? existing.sha256,
1585
+ retrievedAt: detected?.retrievedAt ?? existing.retrievedAt,
1586
+ ...(evidence.length > 0 ? { evidence: [...new Set(evidence)] } : {})
1587
+ };
1588
+ }
1560
1589
  function readExternalProvenance(projectDir, provenanceFile) {
1561
1590
  if (!provenanceFile)
1562
1591
  return new Map();
@@ -1586,6 +1615,8 @@ function readExternalProvenance(projectDir, provenanceFile) {
1586
1615
  const sourceFamily = stringValue(record.sourceFamily ?? nestedProvenance?.sourceFamily ?? nestedProvenance?.sourcePack);
1587
1616
  const author = stringValue(record.author ?? nestedProvenance?.author);
1588
1617
  const attribution = stringValue(record.attribution ?? record.credit ?? nestedProvenance?.attribution);
1618
+ const sha256 = stringValue(record.sha256 ?? nestedProvenance?.sha256);
1619
+ const retrievedAt = stringValue(record.retrievedAt ?? nestedProvenance?.retrievedAt);
1589
1620
  const evidence = [
1590
1621
  ...stringArrayValue(record.evidence),
1591
1622
  ...stringArrayValue(record.intendedRouteUsage),
@@ -1598,6 +1629,8 @@ function readExternalProvenance(projectDir, provenanceFile) {
1598
1629
  ...(author ? { author } : {}),
1599
1630
  ...(sourceFamily ? { sourceFamily } : {}),
1600
1631
  ...(attribution ? { attribution } : {}),
1632
+ ...(sha256 ? { sha256 } : {}),
1633
+ ...(retrievedAt ? { retrievedAt } : {}),
1601
1634
  ...(evidence.length > 0 ? { evidence } : {}),
1602
1635
  checkedAt
1603
1636
  });