@auraindustry/aurajs 0.1.3 → 0.1.5

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 (108) hide show
  1. package/README.md +7 -0
  2. package/benchmarks/perf-thresholds.json +27 -0
  3. package/package.json +6 -1
  4. package/src/ai-guidance.mjs +302 -0
  5. package/src/authored-project.mjs +498 -2
  6. package/src/build-contract/capabilities.mjs +87 -1
  7. package/src/build-contract/constants.mjs +1 -0
  8. package/src/build-contract.mjs +2 -0
  9. package/src/bundler.mjs +143 -13
  10. package/src/cli.mjs +681 -13
  11. package/src/commands/packs.mjs +741 -0
  12. package/src/commands/project-authoring.mjs +128 -1
  13. package/src/conformance/cases/app-and-ui-runtime-cases.mjs +1 -2
  14. package/src/conformance/cases/core-runtime-cases.mjs +6 -2
  15. package/src/conformance/cases/scene3d-and-media-cases.mjs +238 -0
  16. package/src/conformance/cases/systems-and-gameplay-cases.mjs +265 -4
  17. package/src/conformance-mobile.mjs +166 -0
  18. package/src/conformance.mjs +89 -30
  19. package/src/evidence-bundle.mjs +242 -0
  20. package/src/headless-test/runtime-coordinator.mjs +186 -33
  21. package/src/headless-test.mjs +2 -0
  22. package/src/helpers/2d/index.mjs +183 -0
  23. package/src/helpers/index.mjs +26 -0
  24. package/src/helpers/starter-utils/adventure-objectives.js +102 -0
  25. package/src/helpers/starter-utils/adventure-world-2d.js +221 -0
  26. package/src/helpers/starter-utils/animation-2d.js +337 -0
  27. package/src/helpers/starter-utils/animation-packaging-2d.js +203 -0
  28. package/src/helpers/starter-utils/atlas-assets-2d.js +111 -0
  29. package/src/helpers/starter-utils/autoplay-debug-2d.js +215 -0
  30. package/src/helpers/starter-utils/avatar-3d.js +404 -0
  31. package/src/helpers/starter-utils/combat-feedback-2d.js +320 -0
  32. package/src/helpers/starter-utils/combat-runtime-2d.js +290 -0
  33. package/src/helpers/starter-utils/core.js +150 -0
  34. package/src/helpers/starter-utils/dialogue-2d.js +351 -0
  35. package/src/helpers/starter-utils/enemy-archetypes-2d.js +68 -0
  36. package/src/helpers/starter-utils/index.js +26 -0
  37. package/src/helpers/starter-utils/inventory-2d.js +268 -0
  38. package/src/helpers/starter-utils/journal-2d.js +267 -0
  39. package/src/helpers/starter-utils/platformer-3d.js +132 -0
  40. package/src/helpers/starter-utils/scene-audio-2d.js +236 -0
  41. package/src/helpers/starter-utils/streamed-world-2d.js +378 -0
  42. package/src/helpers/starter-utils/tilemap-nav-2d.js +499 -0
  43. package/src/helpers/starter-utils/tilemap-world-2d.js +205 -0
  44. package/src/helpers/starter-utils/triggers.js +662 -0
  45. package/src/helpers/starter-utils/tween-2d.js +615 -0
  46. package/src/helpers/starter-utils/wave-director.js +101 -0
  47. package/src/helpers/starter-utils/world-compositor-2d.js +253 -0
  48. package/src/helpers/starter-utils/world-persistence-2d.js +180 -0
  49. package/src/mobile/android/build.mjs +606 -0
  50. package/src/mobile/android/host-artifact.mjs +280 -0
  51. package/src/mobile/ios/build.mjs +1323 -0
  52. package/src/mobile/ios/host-artifact.mjs +819 -0
  53. package/src/mobile/shared/capabilities.mjs +174 -0
  54. package/src/packs/catalog.mjs +259 -0
  55. package/src/perf-benchmark-runner.mjs +17 -12
  56. package/src/perf-benchmark.mjs +408 -4
  57. package/src/publish-command.mjs +303 -6
  58. package/src/replay-runtime.mjs +257 -0
  59. package/src/scaffold/config.mjs +2 -0
  60. package/src/scaffold/fs.mjs +8 -1
  61. package/src/scaffold/project-docs.mjs +43 -1
  62. package/src/scaffold.mjs +4 -0
  63. package/src/session-runtime.mjs +4 -3
  64. package/src/web-conformance.mjs +0 -36
  65. package/templates/create/2d-adventure/config/gameplay/adventure.config.js +9 -6
  66. package/templates/create/2d-adventure/content/gameplay/dialogue.js +85 -0
  67. package/templates/create/2d-adventure/content/gameplay/world.js +32 -36
  68. package/templates/create/2d-adventure/content/gameplay/world.tilemap.json +273 -0
  69. package/templates/create/2d-adventure/docs/design/loop.md +4 -3
  70. package/templates/create/2d-adventure/prefabs/relic.prefab.js +10 -10
  71. package/templates/create/2d-adventure/prefabs/world.prefab.js +127 -74
  72. package/templates/create/2d-adventure/scenes/gameplay.scene.js +603 -112
  73. package/templates/create/2d-adventure/src/runtime/capabilities.js +16 -0
  74. package/templates/create/2d-adventure/ui/hud.screen.js +187 -4
  75. package/templates/create/2d-adventure/ui/journal.screen.js +183 -0
  76. package/templates/create/3d/scenes/gameplay.scene.js +30 -3
  77. package/templates/create/3d/src/runtime/capabilities.js +5 -0
  78. package/templates/create/3d/src/runtime/materials.js +10 -0
  79. package/templates/create/3d-adventure/scenes/gameplay.scene.js +30 -3
  80. package/templates/create/3d-adventure/src/runtime/capabilities.js +5 -0
  81. package/templates/create/3d-adventure/src/runtime/materials.js +11 -0
  82. package/templates/create/3d-collectathon/scenes/gameplay.scene.js +30 -3
  83. package/templates/create/3d-collectathon/src/runtime/capabilities.js +5 -0
  84. package/templates/create/3d-collectathon/src/runtime/materials.js +10 -0
  85. package/templates/create/shared/src/runtime/ui-forms.js +552 -0
  86. package/templates/create/shared/src/starter-utils/adventure-world-2d.js +221 -0
  87. package/templates/create/shared/src/starter-utils/animation-packaging-2d.js +203 -0
  88. package/templates/create/shared/src/starter-utils/atlas-assets-2d.js +111 -0
  89. package/templates/create/shared/src/starter-utils/autoplay-debug-2d.js +215 -0
  90. package/templates/create/shared/src/starter-utils/combat-runtime-2d.js +290 -0
  91. package/templates/create/shared/src/starter-utils/dialogue-2d.js +351 -0
  92. package/templates/create/shared/src/starter-utils/index.js +15 -1
  93. package/templates/create/shared/src/starter-utils/inventory-2d.js +268 -0
  94. package/templates/create/shared/src/starter-utils/journal-2d.js +267 -0
  95. package/templates/create/shared/src/starter-utils/scene-audio-2d.js +236 -0
  96. package/templates/create/shared/src/starter-utils/streamed-world-2d.js +378 -0
  97. package/templates/create/shared/src/starter-utils/tilemap-nav-2d.js +499 -0
  98. package/templates/create/shared/src/starter-utils/tilemap-world-2d.js +205 -0
  99. package/templates/create/shared/src/starter-utils/world-compositor-2d.js +253 -0
  100. package/templates/create/shared/src/starter-utils/world-persistence-2d.js +180 -0
  101. package/templates/create-bin/play.js +36 -7
  102. package/templates/skills/auramaxx/SKILL.md +46 -0
  103. package/templates/skills/auramaxx/project-requirements.md +68 -0
  104. package/templates/skills/auramaxx/starter-recipes.md +104 -0
  105. package/templates/skills/auramaxx/validation-checklist.md +49 -0
  106. package/templates/skills/aurajs/SKILL.md +0 -96
  107. package/templates/skills/aurajs/api-contract-3d.md +0 -7
  108. package/templates/skills/aurajs/api-contract.md +0 -7
@@ -1404,6 +1404,10 @@ export const SYSTEMS_AND_GAMEPLAY_CONFORMANCE_CASES = [
1404
1404
  id: 'optional.legacy-steam.not-exposed',
1405
1405
  expression: "typeof aura.steam === 'undefined' && !Object.prototype.hasOwnProperty.call(aura, 'steam')",
1406
1406
  },
1407
+ {
1408
+ id: 'optional.legacy-steam.provider-not-exposed',
1409
+ expression: "typeof aura.providers === 'undefined' || typeof aura.providers.steam === 'undefined'",
1410
+ },
1407
1411
  {
1408
1412
  id: 'optional.legacy-steam.physics-disabled-guidance',
1409
1413
  expression: "(() => { try { aura.physics.step(0.016); return false; } catch (e) { const msg = String(e); return msg.includes('optional module \"physics\" is disabled') && msg.includes('modules.physics = true') && msg.includes('[reason:optional_module_physics_disabled]'); } })()",
@@ -1857,15 +1861,15 @@ export const SYSTEMS_AND_GAMEPLAY_CONFORMANCE_CASES = [
1857
1861
  },
1858
1862
  {
1859
1863
  id: 'draw3d.postfx.target-chain.custom-pass.deterministic',
1860
- expression: "(() => { const runSample = () => { const baseline = aura.draw3d.getPostFXState(); aura.draw3d.removePostFXPass('bloom'); aura.draw3d.removePostFXPass('colorGrade'); aura.draw3d.removePostFXPass('vignette'); aura.draw3d.removePostFXPass('fxaa'); aura.draw3d.removePostFXPass('custom:filmgrain'); aura.draw3d.removePostFXPass('custom:glow'); const setBloom = aura.draw3d.setPostFXPass('bloom', { strength: 1.1, targetChain: { intermediateTargets: ['mainA', 'mainB', 'mainA'], pingPong: true, composeMode: 'additive' } }); const setCustom = aura.draw3d.setPostFXPass('custom:glow', { customParams: { intensity: 0.75, threshold: 0.33 } }); const updateCustom = aura.draw3d.setPostFXPass('custom:glow', { customParams: { intensity: 0.5, threshold: 0.2 } }); const setFilmgrain = aura.draw3d.setPostFXPass('custom:filmgrain', { customParams: { amount: 0.2, grain: 0.1 } }); const state = aura.draw3d.getPostFXState(); const targetChain = state?.targetChain || {}; const customGlow = Array.isArray(state?.passes) ? state.passes.find((pass) => pass.pass === 'custom:glow') : null; const customParams = customGlow?.customParams || {}; const customParamKeys = Object.keys(customParams).sort().join('|'); return { status: [setBloom?.reasonCode, setCustom?.reasonCode, updateCustom?.reasonCode, setFilmgrain?.reasonCode].join('|'), order: Array.isArray(state?.passes) ? state.passes.map((pass) => pass.pass).join('|') : '', enabled: state?.enabledPasses, total: state?.totalPasses, customPassCount: state?.customPassCount, targetChainTargets: Array.isArray(targetChain?.intermediateTargets) ? targetChain.intermediateTargets.join('|') : '', targetChainCount: targetChain?.intermediateTargetCount, targetChainPingPong: targetChain?.pingPong === true, targetChainComposeMode: targetChain?.composeMode, targetChainFingerprint: state?.targetChainFingerprint, customParamFingerprint: state?.customParamFingerprint, customGlowIsCustom: customGlow?.isCustom === true, customGlowParamKeys: customParamKeys, customGlowIntensity: customParams?.intensity, customGlowThreshold: customParams?.threshold, mutationDelta: Number(state?.mutationCount || 0) - Number(baseline?.mutationCount || 0), lastReasonCode: state?.lastReasonCode, lastOk: state?.lastOk }; }; const first = runSample(); const second = runSample(); return JSON.stringify(first) === JSON.stringify(second) && first.status === 'postfx_ok|postfx_ok|postfx_ok|postfx_ok' && first.order === 'bloom|custom:filmgrain|custom:glow' && first.total === 3 && first.enabled === 3 && first.customPassCount === 2 && first.targetChainTargets === 'maina|mainb' && first.targetChainCount === 2 && first.targetChainPingPong === true && first.targetChainComposeMode === 'additive' && Number.isFinite(first.targetChainFingerprint) && first.targetChainFingerprint > 0 && Number.isFinite(first.customParamFingerprint) && first.customParamFingerprint > 0 && first.customGlowIsCustom === true && first.customGlowParamKeys === 'intensity|threshold' && Math.abs(Number(first.customGlowIntensity) - 0.5) < 1e-3 && Math.abs(Number(first.customGlowThreshold) - 0.2) < 1e-3 && first.mutationDelta === 10 && first.lastReasonCode === 'postfx_ok' && first.lastOk === true; })()",
1864
+ expression: "(() => { const runSample = () => { const baseline = aura.draw3d.getPostFXState(); aura.draw3d.removePostFXPass('bloom'); aura.draw3d.removePostFXPass('colorGrade'); aura.draw3d.removePostFXPass('vignette'); aura.draw3d.removePostFXPass('fxaa'); aura.draw3d.removePostFXPass('filmGrain'); aura.draw3d.removePostFXPass('custom:glow'); const setBloom = aura.draw3d.setPostFXPass('bloom', { strength: 1.1, targetChain: { intermediateTargets: ['mainA', 'mainB', 'mainA'], pingPong: true, composeMode: 'additive' } }); const setCustom = aura.draw3d.setPostFXPass('custom:glow', { customParams: { intensity: 0.75, threshold: 0.33 } }); const updateCustom = aura.draw3d.setPostFXPass('custom:glow', { customParams: { intensity: 0.5, threshold: 0.2 } }); const setFilmgrain = aura.draw3d.setPostFXPass('filmGrain', { customParams: { amount: 0.2, speed: 0.1 } }); const state = aura.draw3d.getPostFXState(); const targetChain = state?.targetChain || {}; const customGlow = Array.isArray(state?.passes) ? state.passes.find((pass) => pass.pass === 'custom:glow') : null; const customParams = customGlow?.customParams || {}; const customParamKeys = Object.keys(customParams).sort().join('|'); return { status: [setBloom?.reasonCode, setCustom?.reasonCode, updateCustom?.reasonCode, setFilmgrain?.reasonCode].join('|'), order: Array.isArray(state?.passes) ? state.passes.map((pass) => pass.pass).join('|') : '', enabled: state?.enabledPasses, total: state?.totalPasses, customPassCount: state?.customPassCount, targetChainTargets: Array.isArray(targetChain?.intermediateTargets) ? targetChain.intermediateTargets.join('|') : '', targetChainCount: targetChain?.intermediateTargetCount, targetChainPingPong: targetChain?.pingPong === true, targetChainComposeMode: targetChain?.composeMode, targetChainFingerprint: state?.targetChainFingerprint, customParamFingerprint: state?.customParamFingerprint, customGlowIsCustom: customGlow?.isCustom === true, customGlowParamKeys: customParamKeys, customGlowIntensity: customParams?.intensity, customGlowThreshold: customParams?.threshold, mutationDelta: Number(state?.mutationCount || 0) - Number(baseline?.mutationCount || 0), lastReasonCode: state?.lastReasonCode, lastOk: state?.lastOk }; }; const first = runSample(); const second = runSample(); return JSON.stringify(first) === JSON.stringify(second) && first.status === 'postfx_ok|postfx_ok|postfx_ok|postfx_ok' && first.order === 'bloom|filmGrain|custom:glow' && first.total === 3 && first.enabled === 3 && first.customPassCount === 1 && first.targetChainTargets === 'maina|mainb' && first.targetChainCount === 2 && first.targetChainPingPong === true && first.targetChainComposeMode === 'additive' && Number.isFinite(first.targetChainFingerprint) && first.targetChainFingerprint > 0 && Number.isFinite(first.customParamFingerprint) && first.customParamFingerprint > 0 && first.customGlowIsCustom === true && first.customGlowParamKeys === 'intensity|threshold' && Math.abs(Number(first.customGlowIntensity) - 0.5) < 1e-3 && Math.abs(Number(first.customGlowThreshold) - 0.2) < 1e-3 && first.mutationDelta === 10 && first.lastReasonCode === 'postfx_ok' && first.lastOk === true; })()",
1861
1865
  },
1862
1866
  {
1863
1867
  id: 'draw3d.postfx.motion-blur.state.deterministic',
1864
- expression: "(() => { const resetPasses = () => { for (const passName of ['ssr', 'dof', 'motionBlur', 'bloom', 'colorGrade', 'vignette', 'fxaa', 'ssao', 'custom:filmgrain', 'custom:glow']) { aura.draw3d.removePostFXPass(passName); } }; const runSample = () => { resetPasses(); const baseline = aura.draw3d.getPostFXState() || {}; const setInitial = aura.draw3d.setPostFXPass('motionBlur', { strength: 0.75, radius: 12, threshold: 1.0, targetChain: { intermediateTargets: [], pingPong: false, composeMode: 'replace' } }); const disable = aura.draw3d.setPostFXEnabled('motionBlur', false); const reenable = aura.draw3d.setPostFXEnabled('motionBlur', true); const update = aura.draw3d.setPostFXPass('motionBlur', { strength: 0.35, radius: 6, threshold: 0.0 }); const state = aura.draw3d.getPostFXState() || {}; const pass = Array.isArray(state?.passes) ? state.passes.find((entry) => entry.pass === 'motionBlur') : null; return { status: [setInitial?.reasonCode, disable?.reasonCode, reenable?.reasonCode, update?.reasonCode].join('|'), order: Array.isArray(state?.passes) ? state.passes.map((entry) => entry.pass).join('|') : '', total: Number(state?.totalPasses || 0), enabled: Number(state?.enabledPasses || 0), strength: Number(pass?.strength || 0), radius: Number(pass?.radius || 0), threshold: Number(pass?.threshold || 0), mutationDelta: Number(state?.mutationCount || 0) - Number(baseline?.mutationCount || 0), lastReasonCode: state?.lastReasonCode, lastOk: state?.lastOk, targetChainCount: Number(state?.targetChain?.intermediateTargetCount || 0), targetChainPingPong: state?.targetChain?.pingPong === true, targetChainComposeMode: state?.targetChain?.composeMode || null }; }; const first = runSample(); const second = runSample(); return JSON.stringify(first) === JSON.stringify(second) && first.status === 'postfx_ok|postfx_ok|postfx_ok|postfx_ok' && first.order === 'motionBlur' && first.total === 1 && first.enabled === 1 && Math.abs(first.strength - 0.35) < 1e-3 && Math.abs(first.radius - 6.0) < 1e-3 && Math.abs(first.threshold - 0.0) < 1e-3 && first.targetChainCount === 0 && first.targetChainPingPong === false && first.targetChainComposeMode === 'replace' && first.mutationDelta === 4 && first.lastReasonCode === 'postfx_ok' && first.lastOk === true; })()",
1868
+ expression: "(() => { const resetPasses = () => { for (const passName of ['ssr', 'dof', 'motionBlur', 'bloom', 'colorGrade', 'vignette', 'fxaa', 'ssao', 'filmGrain', 'custom:glow']) { aura.draw3d.removePostFXPass(passName); } }; const runSample = () => { resetPasses(); const baseline = aura.draw3d.getPostFXState() || {}; const setInitial = aura.draw3d.setPostFXPass('motionBlur', { strength: 0.75, radius: 12, threshold: 1.0, targetChain: { intermediateTargets: [], pingPong: false, composeMode: 'replace' } }); const disable = aura.draw3d.setPostFXEnabled('motionBlur', false); const reenable = aura.draw3d.setPostFXEnabled('motionBlur', true); const update = aura.draw3d.setPostFXPass('motionBlur', { strength: 0.35, radius: 6, threshold: 0.0 }); const state = aura.draw3d.getPostFXState() || {}; const pass = Array.isArray(state?.passes) ? state.passes.find((entry) => entry.pass === 'motionBlur') : null; return { status: [setInitial?.reasonCode, disable?.reasonCode, reenable?.reasonCode, update?.reasonCode].join('|'), order: Array.isArray(state?.passes) ? state.passes.map((entry) => entry.pass).join('|') : '', total: Number(state?.totalPasses || 0), enabled: Number(state?.enabledPasses || 0), strength: Number(pass?.strength || 0), radius: Number(pass?.radius || 0), threshold: Number(pass?.threshold || 0), mutationDelta: Number(state?.mutationCount || 0) - Number(baseline?.mutationCount || 0), lastReasonCode: state?.lastReasonCode, lastOk: state?.lastOk, targetChainCount: Number(state?.targetChain?.intermediateTargetCount || 0), targetChainPingPong: state?.targetChain?.pingPong === true, targetChainComposeMode: state?.targetChain?.composeMode || null }; }; const first = runSample(); const second = runSample(); return JSON.stringify(first) === JSON.stringify(second) && first.status === 'postfx_ok|postfx_ok|postfx_ok|postfx_ok' && first.order === 'motionBlur' && first.total === 1 && first.enabled === 1 && Math.abs(first.strength - 0.35) < 1e-3 && Math.abs(first.radius - 6.0) < 1e-3 && Math.abs(first.threshold - 0.0) < 1e-3 && first.targetChainCount === 0 && first.targetChainPingPong === false && first.targetChainComposeMode === 'replace' && first.mutationDelta === 4 && first.lastReasonCode === 'postfx_ok' && first.lastOk === true; })()",
1865
1869
  },
1866
1870
  {
1867
1871
  id: 'draw3d.postfx.ssao.state.deterministic',
1868
- expression: "(() => { const resetPasses = () => { for (const passName of ['ssr', 'dof', 'motionBlur', 'bloom', 'colorGrade', 'vignette', 'fxaa', 'ssao', 'custom:filmgrain', 'custom:glow']) { aura.draw3d.removePostFXPass(passName); } }; const runSample = () => { resetPasses(); const baseline = aura.draw3d.getPostFXState() || {}; const setInitial = aura.draw3d.setPostFXPass('ssao', { strength: 1.1, radius: 0.42, threshold: 0.18, targetChain: { intermediateTargets: [], pingPong: false, composeMode: 'replace' } }); const disable = aura.draw3d.setPostFXEnabled('ssao', false); const reenable = aura.draw3d.setPostFXEnabled('ssao', true); const update = aura.draw3d.setPostFXPass('ssao', { strength: 0.92, radius: 0.36, threshold: 0.24 }); const state = aura.draw3d.getPostFXState() || {}; const pass = Array.isArray(state?.passes) ? state.passes.find((entry) => entry.pass === 'ssao') : null; return { status: [setInitial?.reasonCode, disable?.reasonCode, reenable?.reasonCode, update?.reasonCode].join('|'), order: Array.isArray(state?.passes) ? state.passes.map((entry) => entry.pass).join('|') : '', total: Number(state?.totalPasses || 0), enabled: Number(state?.enabledPasses || 0), strength: Number(pass?.strength || 0), radius: Number(pass?.radius || 0), threshold: Number(pass?.threshold || 0), mutationDelta: Number(state?.mutationCount || 0) - Number(baseline?.mutationCount || 0), lastReasonCode: state?.lastReasonCode, lastOk: state?.lastOk, targetChainCount: Number(state?.targetChain?.intermediateTargetCount || 0), targetChainPingPong: state?.targetChain?.pingPong === true, targetChainComposeMode: state?.targetChain?.composeMode || null }; }; const first = runSample(); const second = runSample(); return JSON.stringify(first) === JSON.stringify(second) && first.status === 'postfx_ok|postfx_ok|postfx_ok|postfx_ok' && first.order === 'ssao' && first.total === 1 && first.enabled === 1 && Math.abs(first.strength - 0.92) < 1e-3 && Math.abs(first.radius - 0.36) < 1e-3 && Math.abs(first.threshold - 0.24) < 1e-3 && first.targetChainCount === 0 && first.targetChainPingPong === false && first.targetChainComposeMode === 'replace' && first.mutationDelta === 4 && first.lastReasonCode === 'postfx_ok' && first.lastOk === true; })()",
1872
+ expression: "(() => { const resetPasses = () => { for (const passName of ['ssr', 'dof', 'motionBlur', 'bloom', 'colorGrade', 'vignette', 'fxaa', 'ssao', 'filmGrain', 'custom:glow']) { aura.draw3d.removePostFXPass(passName); } }; const runSample = () => { resetPasses(); const baseline = aura.draw3d.getPostFXState() || {}; const setInitial = aura.draw3d.setPostFXPass('ssao', { strength: 1.1, radius: 0.42, threshold: 0.18, targetChain: { intermediateTargets: [], pingPong: false, composeMode: 'replace' } }); const disable = aura.draw3d.setPostFXEnabled('ssao', false); const reenable = aura.draw3d.setPostFXEnabled('ssao', true); const update = aura.draw3d.setPostFXPass('ssao', { strength: 0.92, radius: 0.36, threshold: 0.24 }); const state = aura.draw3d.getPostFXState() || {}; const pass = Array.isArray(state?.passes) ? state.passes.find((entry) => entry.pass === 'ssao') : null; return { status: [setInitial?.reasonCode, disable?.reasonCode, reenable?.reasonCode, update?.reasonCode].join('|'), order: Array.isArray(state?.passes) ? state.passes.map((entry) => entry.pass).join('|') : '', total: Number(state?.totalPasses || 0), enabled: Number(state?.enabledPasses || 0), strength: Number(pass?.strength || 0), radius: Number(pass?.radius || 0), threshold: Number(pass?.threshold || 0), mutationDelta: Number(state?.mutationCount || 0) - Number(baseline?.mutationCount || 0), lastReasonCode: state?.lastReasonCode, lastOk: state?.lastOk, targetChainCount: Number(state?.targetChain?.intermediateTargetCount || 0), targetChainPingPong: state?.targetChain?.pingPong === true, targetChainComposeMode: state?.targetChain?.composeMode || null }; }; const first = runSample(); const second = runSample(); return JSON.stringify(first) === JSON.stringify(second) && first.status === 'postfx_ok|postfx_ok|postfx_ok|postfx_ok' && first.order === 'ssao' && first.total === 1 && first.enabled === 1 && Math.abs(first.strength - 0.92) < 1e-3 && Math.abs(first.radius - 0.36) < 1e-3 && Math.abs(first.threshold - 0.24) < 1e-3 && first.targetChainCount === 0 && first.targetChainPingPong === false && first.targetChainComposeMode === 'replace' && first.mutationDelta === 4 && first.lastReasonCode === 'postfx_ok' && first.lastOk === true; })()",
1869
1873
  },
1870
1874
  {
1871
1875
  id: 'draw3d.postfx.reason-codes.stable',
@@ -1873,7 +1877,7 @@ export const SYSTEMS_AND_GAMEPLAY_CONFORMANCE_CASES = [
1873
1877
  },
1874
1878
  {
1875
1879
  id: 'draw3d.postfx.ssr-dof.runtime.setup',
1876
- expression: `(() => { try { const resetPasses = () => { for (const passName of ['ssr', 'dof', 'motionBlur', 'bloom', 'colorGrade', 'vignette', 'fxaa', 'ssao', 'custom:filmgrain', 'custom:glow']) { aura.draw3d.removePostFXPass(passName); } }; resetPasses(); const baseline = aura.draw3d.getPostFXState() || {}; const meshHandle = aura.mesh.createBox(1, 1, 1); const materialHandle = aura.material.create({ color: { r: 0.35, g: 0.7, b: 0.95, a: 1 }, metallic: 0.1, roughness: 0.6 }); const nodeId = aura.scene3d.createNode({ position: { x: 0, y: 0, z: -2.5 } }); const bind = aura.scene3d.bindRenderNode(nodeId, meshHandle, materialHandle, { layer: 0, visible: true, cull: false }); aura.camera3d.perspective(60, 0.1, 100); aura.camera3d.setPosition(0, 0, 4); aura.camera3d.lookAt(0, 0, 0); const setSsr = aura.draw3d.setPostFXPass('ssr', { strength: 1.0, threshold: 0.35, maxSteps: 96, thickness: 0.25, fadeEdge: 0.8, targetChain: { intermediateTargets: [], pingPong: false, composeMode: 'replace' } }); const setDof = aura.draw3d.setPostFXPass('dof', { strength: 0.9, focalDistance: 2.5, focalLength: 50, aperture: 2.2, maxBlur: 6.0 }); const setMotionBlur = aura.draw3d.setPostFXPass('motionBlur', { strength: 0.65, radius: 12.0, threshold: 0.0 }); const baseDraw = aura.draw; aura.draw = function () { const frameIndex = Number(globalThis.__postfxRuntimeFrame || 0); aura.camera3d.setPosition(frameIndex * 0.15, 0, 4 - (frameIndex * 0.1)); aura.camera3d.lookAt(0, 0, 0); aura.draw3d.clear3d({ r: 0.02, g: 0.02, b: 0.03, a: 1 }); aura.scene3d.submitRenderBindings(); globalThis.__postfxRuntimeFrame = frameIndex + 1; baseDraw(); }; globalThis.__postfxRuntimeFrame = 0; globalThis.__postfxRuntimeProbe = { meshHandle, materialHandle, nodeId, bindOk: bind?.ok === true, setSsrReason: setSsr?.reasonCode || null, setDofReason: setDof?.reasonCode || null, setMotionBlurReason: setMotionBlur?.reasonCode || null, baselineMutationCount: Number(baseline?.mutationCount || 0) }; return Number.isInteger(meshHandle) && meshHandle > 0 && Number.isInteger(materialHandle) && materialHandle > 0 && Number.isInteger(nodeId) && nodeId > 0 && bind?.ok === true && setSsr?.ok === true && setDof?.ok === true && setMotionBlur?.ok === true; } catch (_) { return false; } })()`,
1880
+ expression: `(() => { try { const resetPasses = () => { for (const passName of ['ssr', 'dof', 'motionBlur', 'bloom', 'colorGrade', 'vignette', 'fxaa', 'ssao', 'filmGrain', 'custom:glow']) { aura.draw3d.removePostFXPass(passName); } }; resetPasses(); const baseline = aura.draw3d.getPostFXState() || {}; const meshHandle = aura.mesh.createBox(1, 1, 1); const materialHandle = aura.material.create({ color: { r: 0.35, g: 0.7, b: 0.95, a: 1 }, metallic: 0.1, roughness: 0.6 }); const nodeId = aura.scene3d.createNode({ position: { x: 0, y: 0, z: -2.5 } }); const bind = aura.scene3d.bindRenderNode(nodeId, meshHandle, materialHandle, { layer: 0, visible: true, cull: false }); aura.camera3d.perspective(60, 0.1, 100); aura.camera3d.setPosition(0, 0, 4); aura.camera3d.lookAt(0, 0, 0); const setSsr = aura.draw3d.setPostFXPass('ssr', { strength: 1.0, threshold: 0.35, maxSteps: 96, thickness: 0.25, fadeEdge: 0.8, targetChain: { intermediateTargets: [], pingPong: false, composeMode: 'replace' } }); const setDof = aura.draw3d.setPostFXPass('dof', { strength: 0.9, focalDistance: 2.5, focalLength: 50, aperture: 2.2, maxBlur: 6.0 }); const setMotionBlur = aura.draw3d.setPostFXPass('motionBlur', { strength: 0.65, radius: 12.0, threshold: 0.0 }); const baseDraw = aura.draw; aura.draw = function () { const frameIndex = Number(globalThis.__postfxRuntimeFrame || 0); aura.camera3d.setPosition(frameIndex * 0.15, 0, 4 - (frameIndex * 0.1)); aura.camera3d.lookAt(0, 0, 0); aura.draw3d.clear3d({ r: 0.02, g: 0.02, b: 0.03, a: 1 }); aura.scene3d.submitRenderBindings(); globalThis.__postfxRuntimeFrame = frameIndex + 1; baseDraw(); }; globalThis.__postfxRuntimeFrame = 0; globalThis.__postfxRuntimeProbe = { meshHandle, materialHandle, nodeId, bindOk: bind?.ok === true, setSsrReason: setSsr?.reasonCode || null, setDofReason: setDof?.reasonCode || null, setMotionBlurReason: setMotionBlur?.reasonCode || null, baselineMutationCount: Number(baseline?.mutationCount || 0) }; return Number.isInteger(meshHandle) && meshHandle > 0 && Number.isInteger(materialHandle) && materialHandle > 0 && Number.isInteger(nodeId) && nodeId > 0 && bind?.ok === true && setSsr?.ok === true && setDof?.ok === true && setMotionBlur?.ok === true; } catch (_) { return false; } })()`,
1877
1881
  },
1878
1882
  ],
1879
1883
  nativePostChecks: [
@@ -2627,5 +2631,262 @@ export const SYSTEMS_AND_GAMEPLAY_CONFORMANCE_CASES = [
2627
2631
  };
2628
2632
  `,
2629
2633
  frames: 4,
2634
+ },
2635
+ {
2636
+ id: 'asset-forward-2d-flagship-runtime',
2637
+ modes: ['native'],
2638
+ namespaces: ['animation', 'audio', 'draw2d', 'tilemap', 'debug'],
2639
+ functions: [
2640
+ 'aura.animation.registerAtlas',
2641
+ 'aura.animation.resolveAtlasFrame',
2642
+ 'aura.animation.createAtlasClip',
2643
+ 'aura.animation.stepAtlasClip',
2644
+ 'aura.animation.getAtlasClipState',
2645
+ 'aura.audio.play',
2646
+ 'aura.audio.stopAll',
2647
+ 'aura.audio.setBusVolume',
2648
+ 'aura.audio.update',
2649
+ 'aura.audio.getMixerState',
2650
+ 'aura.draw2d.createRenderTarget',
2651
+ 'aura.draw2d.destroyRenderTarget',
2652
+ 'aura.draw2d.runCompositorGraph',
2653
+ 'aura.draw2d.sprite',
2654
+ 'aura.draw2d.text',
2655
+ 'aura.draw2d.clear',
2656
+ 'aura.tilemap.import',
2657
+ 'aura.tilemap.draw',
2658
+ 'aura.tilemap.setTile',
2659
+ 'aura.tilemap.removeRegion',
2660
+ 'aura.tilemap.queryRay',
2661
+ 'aura.tilemap.unload',
2662
+ 'aura.debug.inspectorStats',
2663
+ ],
2664
+ nativeChecks: [
2665
+ {
2666
+ id: 'asset-forward-2d.surface.methods',
2667
+ expression: "(() => ['registerAtlas','resolveAtlasFrame','createAtlasClip','stepAtlasClip','getAtlasClipState'].every((name) => typeof aura.animation?.[name] === 'function') && ['play','stopAll','setBusVolume','update','getMixerState'].every((name) => typeof aura.audio?.[name] === 'function') && ['createRenderTarget','destroyRenderTarget','runCompositorGraph','sprite','text','clear'].every((name) => typeof aura.draw2d?.[name] === 'function') && ['import','draw','setTile','removeRegion','queryRay','unload'].every((name) => typeof aura.tilemap?.[name] === 'function') && typeof aura.debug?.inspectorStats === 'function')()",
2668
+ },
2669
+ ],
2670
+ source: `
2671
+ aura.setup = async function () {
2672
+ if (aura.assets && typeof aura.assets.load === 'function') {
2673
+ await aura.assets.load(['player.png', 'coin.wav']);
2674
+ }
2675
+
2676
+ if (typeof aura.debug?.enableInspector === 'function') {
2677
+ aura.debug.enableInspector(true);
2678
+ }
2679
+
2680
+ const atlas = aura.animation.registerAtlas({
2681
+ image: 'player.png',
2682
+ frames: {
2683
+ drive_0: { x: 0, y: 0, w: 16, h: 16 },
2684
+ drive_1: { x: 0, y: 0, w: 16, h: 16 },
2685
+ drive_2: { x: 0, y: 0, w: 16, h: 16 },
2686
+ },
2687
+ clips: {
2688
+ drive: {
2689
+ frames: ['drive_0', 'drive_1', 'drive_2'],
2690
+ frameDuration: 0.1,
2691
+ loop: true,
2692
+ },
2693
+ },
2694
+ });
2695
+ aura.test.assert(atlas && atlas.ok === true, 'asset-forward atlas registration should succeed');
2696
+
2697
+ const clip = aura.animation.createAtlasClip({
2698
+ atlasId: atlas.atlasId,
2699
+ clipKey: 'drive',
2700
+ playing: true,
2701
+ });
2702
+ aura.test.assert(clip && clip.ok === true, 'asset-forward atlas clip should succeed');
2703
+
2704
+ const mapHandle = aura.tilemap.import({
2705
+ width: 8,
2706
+ height: 4,
2707
+ tilewidth: 16,
2708
+ tileheight: 16,
2709
+ solidLayerNames: ['cover'],
2710
+ layers: [
2711
+ { name: 'ground', type: 'tilelayer', width: 8, height: 4, data: Array.from({ length: 32 }, () => 1) },
2712
+ { name: 'cover', type: 'tilelayer', width: 8, height: 4, data: [
2713
+ 0, 0, 0, 0, 0, 0, 0, 0,
2714
+ 0, 0, 0, 0, 0, 0, 0, 0,
2715
+ 0, 0, 1, 1, 0, 0, 0, 0,
2716
+ 0, 0, 0, 0, 0, 0, 0, 0,
2717
+ ], properties: { solid: true } },
2718
+ { name: 'decor', type: 'tilelayer', width: 8, height: 4, data: [
2719
+ 0, 1, 0, 1, 0, 1, 0, 1,
2720
+ 1, 0, 1, 0, 1, 0, 1, 0,
2721
+ 0, 1, 0, 1, 0, 1, 0, 1,
2722
+ 1, 0, 1, 0, 1, 0, 1, 0,
2723
+ ] },
2724
+ ],
2725
+ tilesets: [
2726
+ { firstgid: 1, image: 'player.png', tilewidth: 16, tileheight: 16, tilecount: 1, columns: 1 },
2727
+ ],
2728
+ });
2729
+ aura.test.assert(Number.isInteger(mapHandle) && mapHandle > 0, 'asset-forward tilemap import should succeed');
2730
+
2731
+ const panelTarget = aura.draw2d.createRenderTarget(160, 96);
2732
+ const fxTarget = aura.draw2d.createRenderTarget(192, 112);
2733
+ const finalTarget = aura.draw2d.createRenderTarget(224, 128);
2734
+ aura.test.assert(panelTarget?.ok === true && fxTarget?.ok === true && finalTarget?.ok === true, 'asset-forward render targets should succeed');
2735
+
2736
+ const musicHandle = aura.audio.play('coin.wav', { volume: 0.2, loop: true, bus: 'music' });
2737
+ const sfxHandle = aura.audio.play('coin.wav', { volume: 0.08, loop: true, bus: 'sfx' });
2738
+ aura.test.assert(Number.isInteger(musicHandle) && musicHandle > 0, 'asset-forward music handle should be valid');
2739
+ aura.test.assert(Number.isInteger(sfxHandle) && sfxHandle > 0, 'asset-forward sfx handle should be valid');
2740
+ aura.test.assert(aura.audio.setBusVolume('music', 0.55)?.ok === true, 'music bus volume should be configurable');
2741
+ aura.test.assert(aura.audio.setBusVolume('sfx', 0.7)?.ok === true, 'sfx bus volume should be configurable');
2742
+
2743
+ globalThis.__assetForward2dFlagship = {
2744
+ atlasId: atlas.atlasId,
2745
+ clipId: clip.clipId,
2746
+ mapHandle,
2747
+ panelTarget,
2748
+ fxTarget,
2749
+ finalTarget,
2750
+ frames: [],
2751
+ lastStepOk: false,
2752
+ };
2753
+ };
2754
+
2755
+ aura.update = function () {
2756
+ const probe = globalThis.__assetForward2dFlagship;
2757
+ const stepDt = [0.05, 0.06, 0.11, 0.08][probe.frames.length] || 0.05;
2758
+ const step = aura.animation.stepAtlasClip(probe.clipId, stepDt);
2759
+ probe.lastStepOk = !!step && step.ok === true;
2760
+ aura.audio.update(0.125);
2761
+
2762
+ const tileX = 2 + (probe.frames.length % 2);
2763
+ aura.tilemap.setTile(probe.mapHandle, 'cover', { x: tileX, y: 2 }, probe.frames.length >= 2 ? 1 : 0);
2764
+ if (probe.frames.length === 1) {
2765
+ aura.tilemap.removeRegion(probe.mapHandle, 'decor', { x: 6, y: 1, w: 1, h: 1 });
2766
+ }
2767
+ };
2768
+
2769
+ aura.draw = function () {
2770
+ const probe = globalThis.__assetForward2dFlagship;
2771
+ const frameIndex = probe.frames.length;
2772
+ const clipState = aura.animation.getAtlasClipState(probe.clipId);
2773
+ const spriteFrame = aura.animation.resolveAtlasFrame(probe.atlasId, clipState?.frameKey || 'drive_0');
2774
+ const ray = aura.tilemap.queryRay(probe.mapHandle, { x: 0.5, y: 32.5, dx: 1, dy: 0, maxDistance: 128 });
2775
+ const mixer = aura.audio.getMixerState() || {};
2776
+ const musicBus = Array.isArray(mixer.buses)
2777
+ ? mixer.buses.find((entry) => entry.bus === 'music')
2778
+ : null;
2779
+
2780
+ aura.draw2d.clear(4, 6, 10);
2781
+ let graph = null;
2782
+ if (frameIndex === 0) {
2783
+ graph = aura.draw2d.runCompositorGraph('asset-forward-2d-proof', [
2784
+ {
2785
+ id: 'panel',
2786
+ target: probe.panelTarget,
2787
+ draw: () => {
2788
+ aura.draw2d.clear(0, 0, 0, 0);
2789
+ aura.tilemap.draw(probe.mapHandle, {
2790
+ camera: { x: 0, y: 0, width: 128, height: 64 },
2791
+ });
2792
+ aura.draw2d.sprite(spriteFrame.image, 34, 22, {
2793
+ width: 32,
2794
+ height: 32,
2795
+ frameX: spriteFrame.frameX,
2796
+ frameY: spriteFrame.frameY,
2797
+ frameW: spriteFrame.frameW,
2798
+ frameH: spriteFrame.frameH,
2799
+ });
2800
+ aura.draw2d.text('ASSET-FORWARD', 10, 10, { size: 10, color: aura.colors.white });
2801
+ },
2802
+ },
2803
+ {
2804
+ id: 'fx',
2805
+ target: probe.fxTarget,
2806
+ draw: () => {
2807
+ aura.draw2d.clear(0, 0, 0, 0);
2808
+ aura.draw2d.sprite(probe.panelTarget, 0, 0, { width: 192, height: 112 });
2809
+ aura.draw2d.text('AUDIO x2', 14, 82, { size: 11, color: aura.colors.white });
2810
+ },
2811
+ },
2812
+ {
2813
+ id: 'final',
2814
+ target: probe.finalTarget,
2815
+ draw: () => {
2816
+ aura.draw2d.clear(0, 0, 0, 0);
2817
+ aura.draw2d.sprite(probe.fxTarget, 0, 0, { width: 224, height: 128 });
2818
+ },
2819
+ },
2820
+ ]);
2821
+ if (Array.isArray(graph?.stages) && graph.stages.length === 3) {
2822
+ aura.draw2d.sprite(graph.stages[2], 8, 8, { width: 224, height: 128 });
2823
+ }
2824
+ } else {
2825
+ aura.draw2d.sprite(probe.finalTarget, 8, 8, { width: 224, height: 128 });
2826
+ }
2827
+
2828
+ const stats = aura.debug.inspectorStats() || {};
2829
+ const compositor = stats.draw2dRuntime?.compositor || {};
2830
+ probe.frames.push({
2831
+ frameIndex,
2832
+ graphReason: graph?.reasonCode || null,
2833
+ graphStageCount: Number(graph?.stageCount || 0),
2834
+ clipFrame: clipState?.frameKey || null,
2835
+ clipLoops: Number(clipState?.loops || 0),
2836
+ frameResolved: spriteFrame?.ok === true,
2837
+ lastStepOk: probe.lastStepOk === true,
2838
+ rayOk: ray?.ok === true,
2839
+ rayHit: ray?.hit === true,
2840
+ rayLayer: Number(ray?.hitCell?.layerIndex ?? -1),
2841
+ trackCount: Array.isArray(mixer.tracks) ? mixer.tracks.length : -1,
2842
+ musicBusVolume: musicBus ? Number(musicBus.volume.toFixed(6)) : null,
2843
+ knownGraphCount: Number(compositor.knownGraphCount || 0),
2844
+ lastGraphStageCount: Number(compositor.lastGraphStageCount || 0),
2845
+ lastGraphReasonCode: compositor.lastGraphReasonCode || null,
2846
+ });
2847
+
2848
+ if (probe.frames.length !== 4) {
2849
+ return;
2850
+ }
2851
+
2852
+ const trace = probe.frames.map((entry) => entry.clipFrame + ':' + entry.clipLoops);
2853
+ const destroyPanel = aura.draw2d.destroyRenderTarget(probe.panelTarget);
2854
+ const destroyFx = aura.draw2d.destroyRenderTarget(probe.fxTarget);
2855
+ const destroyFinal = aura.draw2d.destroyRenderTarget(probe.finalTarget);
2856
+ const missingDestroy = aura.draw2d.destroyRenderTarget(probe.finalTarget);
2857
+ const unloaded = aura.tilemap.unload(probe.mapHandle);
2858
+ aura.audio.stopAll();
2859
+
2860
+ aura.test.equal(
2861
+ trace.join('|'),
2862
+ 'drive_0:0|drive_1:0|drive_2:0|drive_0:1',
2863
+ 'asset-forward animation trace should stay deterministic',
2864
+ );
2865
+ aura.test.assert(
2866
+ probe.frames.every((entry) => entry.frameResolved === true && entry.lastStepOk === true),
2867
+ 'asset-forward atlas stepping should remain resolved each frame',
2868
+ );
2869
+ aura.test.assert(
2870
+ probe.frames.every((entry) => entry.rayOk === true && entry.rayHit === true && entry.rayLayer === 1),
2871
+ 'asset-forward tilemap query pressure should remain deterministic',
2872
+ );
2873
+ aura.test.assert(
2874
+ probe.frames.every((entry) => entry.trackCount === 2 && entry.musicBusVolume === 0.55),
2875
+ 'asset-forward audio concurrency should keep two live tracks and the expected bus volume',
2876
+ );
2877
+ aura.test.assert(
2878
+ probe.frames[0].graphReason === 'draw2d_compositor_captured'
2879
+ && probe.frames[0].graphStageCount === 3
2880
+ && probe.frames[0].knownGraphCount === 1
2881
+ && probe.frames[0].lastGraphStageCount === 3
2882
+ && probe.frames[0].lastGraphReasonCode === 'draw2d_compositor_captured',
2883
+ 'asset-forward compositor capture should remain stable on the setup frame',
2884
+ );
2885
+ aura.test.assert(destroyPanel?.ok === true && destroyFx?.ok === true && destroyFinal?.ok === true, 'asset-forward render targets should destroy cleanly');
2886
+ aura.test.assert(missingDestroy?.ok === false && missingDestroy?.reasonCode === 'missing_render_target', 'asset-forward render target destroy should remain reason-coded');
2887
+ aura.test.equal(unloaded, true, 'asset-forward tilemap should unload cleanly');
2888
+ };
2889
+ `,
2890
+ nativeFrames: 4,
2630
2891
  }
2631
2892
  ];
@@ -0,0 +1,166 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+
4
+ import { buildMobileCapabilityAssertion } from './build-contract/capabilities.mjs';
5
+ import { ANDROID_BUILD_MANIFEST_SCHEMA } from './mobile/android/build.mjs';
6
+ import { IOS_BUILD_MANIFEST_SCHEMA } from './mobile/ios/build.mjs';
7
+ import { normalizeMobileBuildTarget } from './mobile/shared/capabilities.mjs';
8
+
9
+ function readJsonFile(path, label) {
10
+ const resolvedPath = resolve(path);
11
+ if (!existsSync(resolvedPath)) {
12
+ throw new Error(`${label} is missing: ${resolvedPath}`);
13
+ }
14
+ try {
15
+ return JSON.parse(readFileSync(resolvedPath, 'utf8'));
16
+ } catch (error) {
17
+ throw new Error(`Failed to parse ${label}: ${error.message}`);
18
+ }
19
+ }
20
+
21
+ export function validateAndroidBuildManifest(manifest) {
22
+ if (!manifest || typeof manifest !== 'object') {
23
+ return { ok: false, reasonCode: 'mobile_android_manifest_invalid' };
24
+ }
25
+ if (manifest.schema !== ANDROID_BUILD_MANIFEST_SCHEMA || manifest.buildTarget !== 'android') {
26
+ return { ok: false, reasonCode: 'mobile_android_manifest_invalid' };
27
+ }
28
+ if (
29
+ !manifest.buildStatus
30
+ || typeof manifest.buildStatus.release !== 'string'
31
+ || !manifest.toolchain
32
+ || !Array.isArray(manifest.toolchain.reasonCodes)
33
+ ) {
34
+ return { ok: false, reasonCode: 'mobile_android_manifest_invalid' };
35
+ }
36
+ return {
37
+ ok: true,
38
+ reasonCode: 'android_manifest_contract_ok',
39
+ buildStatus: manifest.buildStatus,
40
+ toolchain: manifest.toolchain,
41
+ };
42
+ }
43
+
44
+ export function validateIosBuildManifest(manifest) {
45
+ if (!manifest || typeof manifest !== 'object') {
46
+ return { ok: false, reasonCode: 'mobile_ios_manifest_invalid' };
47
+ }
48
+ if (manifest.schema !== IOS_BUILD_MANIFEST_SCHEMA || manifest.buildTarget !== 'ios') {
49
+ return { ok: false, reasonCode: 'mobile_ios_manifest_invalid' };
50
+ }
51
+ if (
52
+ !manifest.toolchain
53
+ || typeof manifest.toolchain.reasonCode !== 'string'
54
+ || !manifest.signing
55
+ || typeof manifest.signing.reasonCode !== 'string'
56
+ || !manifest.buildStatus
57
+ || typeof manifest.buildStatus.reasonCode !== 'string'
58
+ ) {
59
+ return { ok: false, reasonCode: 'mobile_ios_manifest_invalid' };
60
+ }
61
+ return {
62
+ ok: true,
63
+ reasonCode: 'ios_manifest_contract_ok',
64
+ buildStatus: manifest.buildStatus,
65
+ toolchain: manifest.toolchain,
66
+ signing: manifest.signing,
67
+ };
68
+ }
69
+
70
+ export function runMobileTargetConformance(options = {}) {
71
+ const projectRoot = resolve(options.projectRoot || process.cwd());
72
+ const target = normalizeMobileBuildTarget(options.target || 'mobile');
73
+ const capabilityReport = buildMobileCapabilityAssertion({
74
+ projectRoot,
75
+ modules: options.modules || {},
76
+ target,
77
+ });
78
+
79
+ if (!capabilityReport.ok) {
80
+ return {
81
+ ok: false,
82
+ reasonCode: 'mobile_required_api_unsupported',
83
+ target,
84
+ capabilityReport,
85
+ manifests: {},
86
+ };
87
+ }
88
+
89
+ const manifests = {};
90
+
91
+ if (target === 'android' || target === 'mobile') {
92
+ if (!options.androidManifestPath) {
93
+ return {
94
+ ok: false,
95
+ reasonCode: 'mobile_android_manifest_missing',
96
+ target,
97
+ capabilityReport,
98
+ manifests,
99
+ };
100
+ }
101
+ const resolvedAndroidManifestPath = resolve(options.androidManifestPath);
102
+ if (!existsSync(resolvedAndroidManifestPath)) {
103
+ return {
104
+ ok: false,
105
+ reasonCode: 'mobile_android_manifest_missing',
106
+ target,
107
+ capabilityReport,
108
+ manifests,
109
+ };
110
+ }
111
+ const androidManifest = readJsonFile(resolvedAndroidManifestPath, 'Android build manifest');
112
+ const androidReport = validateAndroidBuildManifest(androidManifest);
113
+ if (!androidReport.ok) {
114
+ return {
115
+ ok: false,
116
+ reasonCode: androidReport.reasonCode,
117
+ target,
118
+ capabilityReport,
119
+ manifests,
120
+ };
121
+ }
122
+ manifests.android = androidReport;
123
+ }
124
+
125
+ if (target === 'ios' || target === 'mobile') {
126
+ if (!options.iosManifestPath) {
127
+ return {
128
+ ok: false,
129
+ reasonCode: 'mobile_ios_manifest_missing',
130
+ target,
131
+ capabilityReport,
132
+ manifests,
133
+ };
134
+ }
135
+ const resolvedIosManifestPath = resolve(options.iosManifestPath);
136
+ if (!existsSync(resolvedIosManifestPath)) {
137
+ return {
138
+ ok: false,
139
+ reasonCode: 'mobile_ios_manifest_missing',
140
+ target,
141
+ capabilityReport,
142
+ manifests,
143
+ };
144
+ }
145
+ const iosManifest = readJsonFile(resolvedIosManifestPath, 'iOS build manifest');
146
+ const iosReport = validateIosBuildManifest(iosManifest);
147
+ if (!iosReport.ok) {
148
+ return {
149
+ ok: false,
150
+ reasonCode: iosReport.reasonCode,
151
+ target,
152
+ capabilityReport,
153
+ manifests,
154
+ };
155
+ }
156
+ manifests.ios = iosReport;
157
+ }
158
+
159
+ return {
160
+ ok: true,
161
+ reasonCode: 'mobile_target_conformance_ok',
162
+ target,
163
+ capabilityReport,
164
+ manifests,
165
+ };
166
+ }