@instruktlabs/kiln 1.2.1 → 1.2.3

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 (219) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +86 -1
  4. package/README.md +18 -0
  5. package/dist/agent-run.mjs +6392 -1477
  6. package/dist/build.json +23 -23
  7. package/dist/cli.mjs +8453 -3407
  8. package/dist/evaluator-worker.mjs +16608 -12387
  9. package/dist/mcp-engine.mjs +6746 -1725
  10. package/dist/mcp-server.mjs +4 -4
  11. package/dist/viewer/app.js +203 -199
  12. package/dist/viewer/chat.html +209 -205
  13. package/dist/viewer/style.css +10 -0
  14. package/docs/atlas-provenance.md +23 -0
  15. package/docs/community-exporter.md +27 -0
  16. package/docs/export-profiles.md +20 -0
  17. package/docs/install.md +7 -6
  18. package/docs/material-library.md +9 -5
  19. package/docs/migration.md +16 -0
  20. package/docs/procedural-surfaces-v3.md +276 -0
  21. package/docs/projects-and-live-review.md +1 -1
  22. package/docs/rendering.md +41 -0
  23. package/docs/runtime.md +3 -3
  24. package/docs/sdk.md +55 -2
  25. package/docs/tools-operations.md +2 -2
  26. package/docs/tools.md +3 -3
  27. package/lib/asset-materials-node.d.ts +3 -3
  28. package/lib/asset-materials-node.js +29 -33
  29. package/lib/assets-node.d.ts +1 -1
  30. package/lib/assets-node.js +11 -4
  31. package/lib/assets.d.ts +4 -2
  32. package/lib/assets.js +61 -16
  33. package/lib/atlas-provenance.d.ts +86 -0
  34. package/lib/atlas-provenance.js +448 -0
  35. package/lib/character.d.ts +11 -2
  36. package/lib/character.js +90 -2
  37. package/lib/community-exporter.d.ts +3 -2
  38. package/lib/community-exporter.js +139 -3
  39. package/lib/discovery/helper-contracts.js +161 -7
  40. package/lib/discovery/helper-specs.js +49 -4
  41. package/lib/discovery/shapes.js +20 -7
  42. package/lib/engine-identity.d.ts +1 -1
  43. package/lib/engine-identity.js +1 -1
  44. package/lib/evaluator/authoring-diagnostic.d.ts +1 -1
  45. package/lib/evaluator/authoring-diagnostic.js +12 -0
  46. package/lib/evaluator/protocol.d.ts +2 -2
  47. package/lib/evaluator/protocol.js +16 -6
  48. package/lib/evaluator/subprocess.js +2 -2
  49. package/lib/exporter-node.js +9 -1
  50. package/lib/geometry-budget.d.ts +7 -0
  51. package/lib/geometry-budget.js +8 -0
  52. package/lib/gltf-texture-binding.d.ts +6 -0
  53. package/lib/gltf-texture-binding.js +54 -0
  54. package/lib/material-authoring-node.d.ts +6 -0
  55. package/lib/material-authoring-node.js +49 -0
  56. package/lib/material-authoring-schema.d.ts +871 -0
  57. package/lib/material-authoring-schema.js +310 -0
  58. package/lib/material-export.d.ts +5 -0
  59. package/lib/material-export.js +21 -0
  60. package/lib/material-library-node.d.ts +34 -8
  61. package/lib/material-library-node.js +691 -11
  62. package/lib/material-library.d.ts +3704 -4
  63. package/lib/material-library.js +272 -0
  64. package/lib/mcp-core.d.ts +1 -1
  65. package/lib/pinned-material-map.d.ts +33 -0
  66. package/lib/pinned-material-map.js +1 -0
  67. package/lib/portable-material-runtime.d.ts +5 -0
  68. package/lib/portable-material-runtime.js +160 -0
  69. package/lib/portable-sampler-runtime.d.ts +4 -0
  70. package/lib/portable-sampler-runtime.js +26 -0
  71. package/lib/portable-sampler.d.ts +11 -0
  72. package/lib/portable-sampler.js +42 -0
  73. package/lib/primitives.d.ts +15 -3
  74. package/lib/primitives.js +24 -4
  75. package/lib/procedural-material-v2.d.ts +2 -0
  76. package/lib/procedural-material-v2.js +2 -1
  77. package/lib/procedural-material-v3.d.ts +72 -0
  78. package/lib/procedural-material-v3.js +162 -0
  79. package/lib/procedural-texture-v2-compiler.d.ts +31 -0
  80. package/lib/procedural-texture-v2-compiler.js +172 -0
  81. package/lib/procedural-texture-v3-contract.d.ts +125 -0
  82. package/lib/procedural-texture-v3-contract.js +333 -0
  83. package/lib/procedural-texture-v3.d.ts +12 -0
  84. package/lib/procedural-texture-v3.js +165 -0
  85. package/lib/procedural-texture.d.ts +9 -31
  86. package/lib/procedural-texture.js +43 -170
  87. package/lib/project-bundle-node.d.ts +1 -1
  88. package/lib/project-bundle-node.js +70 -13
  89. package/lib/project-bundle.d.ts +91 -3
  90. package/lib/project-bundle.js +48 -7
  91. package/lib/qa/atlas.d.ts +21 -0
  92. package/lib/qa/atlas.js +474 -0
  93. package/lib/qa/index.d.ts +1 -0
  94. package/lib/qa/index.js +1 -0
  95. package/lib/qa/material.js +2 -0
  96. package/lib/qa/skin-integrity.d.ts +18 -0
  97. package/lib/qa/skin-integrity.js +221 -0
  98. package/lib/render-service-host.js +4 -7
  99. package/lib/render.d.ts +3 -3
  100. package/lib/render.js +108 -16
  101. package/lib/skin-binding.d.ts +20 -0
  102. package/lib/skin-binding.js +144 -0
  103. package/lib/texture-atlas-contract.d.ts +97 -0
  104. package/lib/texture-atlas-contract.js +414 -0
  105. package/lib/texture-atlas-pixels.d.ts +73 -0
  106. package/lib/texture-atlas-pixels.js +290 -0
  107. package/lib/texture-atlas.d.ts +10 -0
  108. package/lib/texture-atlas.js +143 -0
  109. package/lib/texture-bake.d.ts +3 -0
  110. package/lib/texture-bake.js +66 -11
  111. package/lib/texture-resolver.d.ts +22 -0
  112. package/lib/texture-resolver.js +24 -0
  113. package/lib/tools/materials.js +8 -7
  114. package/lib/tools/registry.js +5 -16
  115. package/lib/tools/review.js +1 -1
  116. package/lib/uv-data.d.ts +32 -0
  117. package/lib/uv-data.js +172 -0
  118. package/lib/uv-shapes.js +3 -0
  119. package/lib/uv.d.ts +9 -3
  120. package/lib/uv.js +53 -82
  121. package/lib/views/camera.d.ts +3 -3
  122. package/lib/views/camera.js +32 -28
  123. package/lib/views/face-policy.d.ts +9 -0
  124. package/lib/views/face-policy.js +16 -0
  125. package/lib/views/glb.js +218 -24
  126. package/lib/views/pose.d.ts +7 -1
  127. package/lib/views/pose.js +63 -12
  128. package/lib/views/raster.d.ts +6 -2
  129. package/lib/views/raster.js +49 -37
  130. package/lib/views/scene-copy.d.ts +6 -0
  131. package/lib/views/scene-copy.js +73 -0
  132. package/lib/views/scene-raster.js +2 -1
  133. package/lib/views/triangle-coverage.d.ts +8 -0
  134. package/lib/views/triangle-coverage.js +58 -0
  135. package/lib/workspace-node.js +7 -3
  136. package/lib/workspace.d.ts +2 -2
  137. package/package.json +3 -1
  138. package/plugin.json +1 -1
  139. package/plugins/kiln-engine/.claude-plugin/plugin.json +1 -1
  140. package/plugins/kiln-engine/README.md +3 -3
  141. package/plugins/kiln-engine/package-provenance.json +6 -6
  142. package/plugins/kiln-engine/plugin.json +1 -1
  143. package/plugins/kiln-engine/runtime.json +1 -1
  144. package/skills/kiln-author-asset/SKILL.md +1 -1
  145. package/skills/kiln-author-asset/references/camera-cli.md +13 -0
  146. package/skills/kiln-author-asset/references/geometry-recipes.md +134 -0
  147. package/skills/kiln-author-asset/references/projects-and-materials.md +32 -2
  148. package/skills/kiln-compose-scene/references/runtime-scenes.md +19 -0
  149. package/skills/kiln-qa-asset/references/integration-checks.md +37 -2
  150. package/src/asset-materials-node.ts +38 -45
  151. package/src/assets-node.ts +22 -5
  152. package/src/assets.ts +61 -20
  153. package/src/atlas-provenance.ts +522 -0
  154. package/src/character.ts +107 -2
  155. package/src/community-exporter.ts +143 -3
  156. package/src/discovery/helper-contracts.ts +188 -7
  157. package/src/discovery/helper-specs.ts +69 -4
  158. package/src/discovery/shapes.ts +25 -9
  159. package/src/engine-identity.ts +1 -1
  160. package/src/evaluator/authoring-diagnostic.ts +19 -1
  161. package/src/evaluator/protocol.ts +19 -9
  162. package/src/evaluator/subprocess.ts +2 -2
  163. package/src/exporter-node.ts +12 -1
  164. package/src/generated/mcp-manifest.json +3 -3
  165. package/src/geometry-budget.ts +8 -0
  166. package/src/gltf-texture-binding.ts +60 -0
  167. package/src/material-authoring-node.ts +68 -0
  168. package/src/material-authoring-schema.ts +343 -0
  169. package/src/material-export.ts +25 -0
  170. package/src/material-library-node.ts +840 -16
  171. package/src/material-library.ts +348 -5
  172. package/src/pinned-material-map.ts +43 -0
  173. package/src/portable-material-runtime.ts +200 -0
  174. package/src/portable-sampler-runtime.ts +40 -0
  175. package/src/portable-sampler.ts +54 -0
  176. package/src/primitives.ts +98 -4
  177. package/src/procedural-material-v2.ts +2 -1
  178. package/src/procedural-material-v3.ts +224 -0
  179. package/src/procedural-texture-v2-compiler.ts +234 -0
  180. package/src/procedural-texture-v3-contract.ts +437 -0
  181. package/src/procedural-texture-v3.ts +220 -0
  182. package/src/procedural-texture.ts +61 -229
  183. package/src/project-bundle-node.ts +106 -14
  184. package/src/project-bundle.ts +61 -18
  185. package/src/project-cli.ts +29 -17
  186. package/src/qa/atlas.ts +597 -0
  187. package/src/qa/index.ts +1 -0
  188. package/src/qa/material.ts +2 -0
  189. package/src/qa/skin-integrity.ts +291 -0
  190. package/src/render-service-host.ts +4 -6
  191. package/src/render.ts +133 -15
  192. package/src/skin-binding.ts +225 -0
  193. package/src/texture-atlas-contract.ts +558 -0
  194. package/src/texture-atlas-pixels.ts +456 -0
  195. package/src/texture-atlas.ts +185 -0
  196. package/src/texture-bake.ts +88 -10
  197. package/src/texture-resolver.ts +55 -0
  198. package/src/tools/materials.ts +15 -11
  199. package/src/tools/registry.ts +5 -15
  200. package/src/tools/review.ts +1 -1
  201. package/src/uv-data.ts +238 -0
  202. package/src/uv-shapes.ts +3 -0
  203. package/src/uv.ts +73 -128
  204. package/src/viewer/app.ts +37 -0
  205. package/src/viewer/live-panel.ts +57 -1
  206. package/src/viewer/material-preview-panel.ts +140 -0
  207. package/src/viewer/material-preview.ts +138 -0
  208. package/src/viewer/materials-panel.ts +34 -5
  209. package/src/viewer/navigation.ts +2 -0
  210. package/src/views/camera.ts +37 -40
  211. package/src/views/face-policy.ts +20 -0
  212. package/src/views/glb.ts +294 -17
  213. package/src/views/pose.ts +71 -13
  214. package/src/views/raster.ts +73 -46
  215. package/src/views/scene-copy.ts +86 -0
  216. package/src/views/scene-raster.ts +2 -1
  217. package/src/views/triangle-coverage.ts +78 -0
  218. package/src/workspace-node.ts +11 -3
  219. package/src/workspace.ts +2 -2
package/docs/sdk.md CHANGED
@@ -22,7 +22,7 @@ const result = await renderGLB(code);
22
22
  ```
23
23
 
24
24
  The complete authoring and host APIs remain available through documented subpaths.
25
- The original 55 export paths are retained; version 1.1 adds four
25
+ The original 55 export paths are retained; version 1.1 added four
26
26
  host entrypoints below. The root exposes rendering, validation,
27
27
  Discovery and engine identity; it does not re-export every name from every module.
28
28
  CommonJS is not part of this release's contract.
@@ -53,7 +53,7 @@ entrypoint list. Files inside `lib/`, including declaration dependencies under
53
53
 
54
54
  ## Stability in 1.x
55
55
 
56
- The table classifies all 59 public entrypoints in version 1.1. The root and every listed subpath
56
+ The table classifies all 59 public entrypoints. The root and every listed subpath
57
57
  except `implicit` are stable entrypoints for v1: their declared public signatures,
58
58
  record shapes and documented behavior receive normal semantic-versioning
59
59
  compatibility. Optional dependencies do not make the two agent APIs experimental.
@@ -80,6 +80,59 @@ and source/revision receipts for reproducible replay. Compatibility of the SDK
80
80
  does not imply identical generated GLB bytes or renderer output across releases.
81
81
  Persisted data and rebuild restrictions follow the [migration contract](migration.md).
82
82
 
83
+ ## Procedural RGBA surfaces
84
+
85
+ The `primitives` entrypoint and authored `proceduralTexture` helper accept an
86
+ explicit V3 recipe for bounded rectangular RGBA images, shape coverage, alpha
87
+ masks, palettes, dithering and seeded stamps. Pure V3 compilation returns pixels,
88
+ canonical JSON and a compiler-qualified recipe hash without creating a mesh.
89
+ Read the [V3 contract](procedural-surfaces-v3.md) for the exact limits and rounding
90
+ rules. V1/V2 remain supported with their existing pixels, hashes and sampler
91
+ defaults. The same entrypoint exposes `textureAtlas` and `remapUVToAtlas` for
92
+ named regions. Portable V3 materials add explicit per-map sampling; opt-in V2
93
+ material records persist recipes, compiled maps and exact dependency pins.
94
+ See the [standalone material and atlas workflow](procedural-surfaces-v3.md#standalone-saved-materials-and-atlases).
95
+
96
+ ## Explicit character skinning
97
+
98
+ The `character` and `primitives` entrypoints expose opt-in `createBoneChain` and
99
+ `createSkinnedPart`; both are also sandbox helpers. `createJointChain` continues
100
+ to return ordinary rigid `Object3D` pivots. Bone chains use the same versioned
101
+ joint descriptors, infer an attached declared parent role and reject ambiguous
102
+ roles/names before attaching nodes. They do not choose anatomy or a body plan.
103
+
104
+ `createSkinnedPart(name, geometry, material, { parent, bones, indices, weights })`
105
+ clones unchanged geometry and binds it to the assembled hierarchy's current rest
106
+ pose. Supply exactly four indices and weights per vertex, in the explicit bone
107
+ array order. Indices must be valid even in zero-weight lanes. By default weights
108
+ must be in 0..1 and sum to one within 1e-5; `normalizeWeights: true` explicitly
109
+ normalizes positive finite sums. Zero sums are rejected. Optional local position,
110
+ Euler rotation (degrees), scale and semantic metadata match `createPart`'s units.
111
+
112
+ The helper owns geometry, influence arrays and inverse bind matrices; bones and
113
+ materials remain shared. Limits are 1024 bones, 262144 vertices and 128 MiB of
114
+ cloned attributes plus new influence storage. These are helper limits, not new
115
+ restrictions on direct Three.js authoring. Existing skin attributes and singular
116
+ bind transforms are rejected. Set up rest transforms before binding; later rest
117
+ edits/reparenting need deliberate rebinding. Animated bounds remain a consumer
118
+ responsibility. The selected Three exporter is required to preserve skins;
119
+ calling these helpers does not change the legacy default or select a converter.
120
+
121
+ `inspectSkinIntegrity(root)` from `@instruktlabs/kiln/qa` reads source bindings
122
+ without updating matrices, normalizing weights or repairing the scene. It returns
123
+ bounded findings, checked counts and current posed world bounds (including position
124
+ morphs). It is not an animation envelope, deformation-quality score or proof of
125
+ collision clearance. Rigid meshes do not acquire skin or body-plan requirements.
126
+
127
+ Export runs this check before bounds, texture baking or conversion. Invalid source
128
+ bindings produce an `AssetQaBlockedError` at the scene stage with mesh/joint or
129
+ vertex-range context and repair advice. This is mandatory input integrity, like
130
+ invalid geometry buffers; QA warning mode and derivative serialization do not
131
+ bypass it. The partial failure report does not claim the remaining QA rules ran.
132
+ The independent final glTF validation remains in place. Source Three skeletons
133
+ need their inverse-bind arrays populated; this does not change glTF's valid
134
+ identity-matrix default when the file omits `inverseBindMatrices`.
135
+
83
136
  ## Execution and dependencies
84
137
 
85
138
  The root SDK is Node-oriented. `/node` adapters use filesystem or server services;
@@ -1123,7 +1123,7 @@ Create and retain one immutable material from a preset or procedural definition.
1123
1123
  "type": "object",
1124
1124
  "properties": {
1125
1125
  "draft": {
1126
- "description": "create-procedural: materialId, name, tileable, sources, maps with layered procedural specs; optional tags, physicalSizeMeters, parameters. Shape: kiln_discover({ ids: ['shape:material-draft'] }).",
1126
+ "description": "create-procedural: legacy layered draft, or schemaVersion 2 with explicit samplers and RGBA/atlas recipes. Exact atlas sources must exist in this library. Shape: kiln_discover({ ids: ['shape:material-draft'] }).",
1127
1127
  "type": "object",
1128
1128
  "propertyNames": {
1129
1129
  "type": "string"
@@ -1166,7 +1166,7 @@ Import complete normalized material records and embedded PNGs. Does not fetch UR
1166
1166
  "type": "object",
1167
1167
  "properties": {
1168
1168
  "payload": {
1169
- "description": "import: schemaVersion 1 and complete normalized records with embedded PNG bytes. Shape: kiln_discover({ ids: ['shape:material-import'] }).",
1169
+ "description": "import: schemaVersion 1 or 2, complete normalized records and their exact dependencies with embedded PNG bytes. Shape: kiln_discover({ ids: ['shape:material-import'] }).",
1170
1170
  "type": "object",
1171
1171
  "propertyNames": {
1172
1172
  "type": "string"
package/docs/tools.md CHANGED
@@ -1328,7 +1328,7 @@ Manage optional shared workspace projects: list discovers IDs; get reads the cur
1328
1328
 
1329
1329
  ## kiln_material
1330
1330
 
1331
- Manage optional immutable material resources in this workspace. presets discovers shipped architecture, wood, metal, fabric and ground recipes; create-preset bakes one with an explicit seed, creator and license; list returns compact material/revision summaries; get returns full provenance, hashes, map conventions, physical repeat scale and a code-ready portable material spec; create-procedural bakes bounded editable layer recipes including optional height-derived normals; import accepts complete normalized records with embedded PNG bytes. No action downloads URLs or executes source. Pin the returned materialId/revisionId through per-invocation materialDependencies or project dependencies before authored evaluation resolves the resources; no project is required.
1331
+ Manage optional immutable material resources in this workspace. presets lists shipped recipes; create-preset uses explicit seed, creator and license; list returns revision summaries; get returns provenance, hashes, sampling and a portable material spec. create-procedural accepts legacy layer drafts or schemaVersion 2 drafts with explicit samplers and bounded RGBA or named atlas recipes. Atlas resource sources require exact saved revisions; import requires complete normalized records and dependencies with embedded PNG bytes. Read shape:material-draft or shape:material-import through kiln_discover for full fields. No action downloads URLs or executes source. Pin the returned materialId/revisionId through per-invocation materialDependencies or project dependencies before evaluation; no project or mesh is required.
1332
1332
 
1333
1333
  <details>
1334
1334
  <summary>Input JSON Schema</summary>
@@ -1431,7 +1431,7 @@ Manage optional immutable material resources in this workspace. presets discover
1431
1431
  "pattern": "^sha256:[a-f0-9]{64}$"
1432
1432
  },
1433
1433
  "draft": {
1434
- "description": "create-procedural: materialId, name, tileable, sources, maps with layered procedural specs; optional tags, physicalSizeMeters, parameters. Shape: kiln_discover({ ids: ['shape:material-draft'] }).",
1434
+ "description": "create-procedural: legacy layered draft, or schemaVersion 2 with explicit samplers and RGBA/atlas recipes. Exact atlas sources must exist in this library. Shape: kiln_discover({ ids: ['shape:material-draft'] }).",
1435
1435
  "type": "object",
1436
1436
  "propertyNames": {
1437
1437
  "type": "string"
@@ -1439,7 +1439,7 @@ Manage optional immutable material resources in this workspace. presets discover
1439
1439
  "additionalProperties": {}
1440
1440
  },
1441
1441
  "payload": {
1442
- "description": "import: schemaVersion 1 and complete normalized records with embedded PNG bytes. Shape: kiln_discover({ ids: ['shape:material-import'] }).",
1442
+ "description": "import: schemaVersion 1 or 2, complete normalized records and their exact dependencies with embedded PNG bytes. Shape: kiln_discover({ ids: ['shape:material-import'] }).",
1443
1443
  "type": "object",
1444
1444
  "propertyNames": {
1445
1445
  "type": "string"
@@ -1,5 +1,5 @@
1
1
  /** Exact saved material closure, shared by standalone asset and project delivery. */
2
2
  import type { AssetRecord } from "./assets.js";
3
- import { type MaterialLibrary, type MaterialManifestV1, type MaterialRecordV1 } from "./material-library.js";
4
- export declare function dependencyManifests(asset: AssetRecord): MaterialManifestV1[];
5
- export declare function resolveSavedAssetMaterials(asset: AssetRecord, library?: MaterialLibrary): Promise<MaterialRecordV1[]>;
3
+ import { type MaterialLibrary, type MaterialManifest, type MaterialRecord } from "./material-library.js";
4
+ export declare function dependencyManifests(asset: AssetRecord): MaterialManifest[];
5
+ export declare function resolveSavedAssetMaterials(asset: AssetRecord, library?: MaterialLibrary): Promise<MaterialRecord[]>;
@@ -1,30 +1,13 @@
1
- import { validateAssetMaterialClosure } from "./assets.js";
2
- import { canonicalMaterialJson, MATERIAL_LIBRARY_LIMITS, } from "./material-library.js";
3
- import { decodeMaterialLibraryPayload, validateMaterialManifestIdentity, } from "./material-library-node.js";
1
+ import { assetMaterialRootManifests, validateAssetMaterialClosure } from "./assets.js";
2
+ import { MATERIAL_LIBRARY_LIMITS, } from "./material-library.js";
3
+ import { decodeMaterialLibraryPayload, validateMaterialManifestIdentityAny, resolveMaterialRecordClosure, } from "./material-library-node.js";
4
4
  export function dependencyManifests(asset) {
5
- const result = new Map();
6
- for (const raw of asset.manifest.build?.dependencies ?? []) {
7
- if (!raw || typeof raw !== 'object' || Array.isArray(raw))
8
- throw new Error('Invalid saved asset dependency');
9
- const dependency = raw;
10
- if (dependency.kind === 'kiln.material.v1') {
11
- if (dependency.delivery !== 'runtime')
12
- throw new Error('Invalid material dependency delivery');
13
- const manifest = validateMaterialManifestIdentity(dependency.manifest);
14
- result.set(`${manifest.materialId}/${manifest.revisionId}`, manifest);
15
- if (result.size > MATERIAL_LIBRARY_LIMITS.maxPayloadRecords)
16
- throw new Error('Saved asset material dependency count exceeds limit');
17
- }
18
- else if (dependency.delivery === 'runtime')
19
- throw new Error(`Unsupported runtime dependency: ${String(dependency.resourceId ?? 'unknown')}`);
20
- else if (dependency.delivery !== 'embedded')
21
- throw new Error('Unsupported saved asset dependency');
22
- }
23
- if (asset.manifest.build?.rebuild === 'external-dependencies-required' && !result.size)
5
+ const result = assetMaterialRootManifests(asset).map(validateMaterialManifestIdentityAny);
6
+ if (asset.manifest.build?.rebuild === 'external-dependencies-required' && !result.length)
24
7
  throw new Error('Saved asset has unrecorded external dependencies');
25
8
  let bytes = 0;
26
9
  let pixels = 0;
27
- for (const manifest of result.values())
10
+ for (const manifest of result)
28
11
  for (const map of manifest.maps) {
29
12
  bytes += map.bytes;
30
13
  pixels += map.width * map.height;
@@ -32,7 +15,7 @@ export function dependencyManifests(asset) {
32
15
  if (bytes > MATERIAL_LIBRARY_LIMITS.maxPayloadBytes ||
33
16
  pixels > MATERIAL_LIBRARY_LIMITS.maxPayloadPixels)
34
17
  throw new Error('Saved asset material allocation exceeds limit');
35
- return [...result.values()];
18
+ return result;
36
19
  }
37
20
  export async function resolveSavedAssetMaterials(asset, library) {
38
21
  const dependencies = dependencyManifests(asset);
@@ -40,17 +23,30 @@ export async function resolveSavedAssetMaterials(asset, library) {
40
23
  validateAssetMaterialClosure(asset, true);
41
24
  return decodeMaterialLibraryPayload(asset.materialResources);
42
25
  }
43
- return Promise.all(dependencies.map(async (manifest) => {
26
+ if (!dependencies.length)
27
+ return [];
28
+ if (!library)
29
+ throw new Error('Locked asset material unavailable: No material library configured');
30
+ const roots = new Map(dependencies.map((m) => [`${m.materialId}/${m.revisionId}`, m]));
31
+ const wrap = async (id, revision, load) => {
44
32
  try {
45
- if (!library)
46
- throw new Error('No material library configured');
47
- const record = await library.read(manifest.materialId, manifest.revisionId);
48
- if (canonicalMaterialJson(record.manifest) !== canonicalMaterialJson(manifest))
49
- throw new Error('Saved dependency manifest mismatch');
50
- return record;
33
+ return await load();
51
34
  }
52
35
  catch (error) {
53
- throw new Error(`Locked asset material unavailable: ${manifest.materialId} at ${manifest.revisionId}`, { cause: error });
36
+ throw new Error(`Locked asset material unavailable: ${id} at ${revision}`, { cause: error });
54
37
  }
55
- }));
38
+ };
39
+ return resolveMaterialRecordClosure(dependencies, {
40
+ readManifest: async (id, revision) => wrap(id, revision, async () => {
41
+ const root = roots.get(`${id}/${revision}`);
42
+ if (root)
43
+ return root;
44
+ if (!library.readManifest)
45
+ throw new Error('Material library requires manifest-first dependency capability');
46
+ return library.readManifest(id, revision);
47
+ }),
48
+ read: async (id, revision) => wrap(id, revision, () => library.readRecordFiles
49
+ ? library.readRecordFiles(id, revision)
50
+ : library.read(id, revision)),
51
+ });
56
52
  }
@@ -1,5 +1,5 @@
1
1
  import { type AssetDraft, type AssetLibrary, type AssetManifest, type AssetRecord } from "./assets.js";
2
- import type { MaterialLibrary } from "./material-library.js";
2
+ import { type MaterialLibrary } from "./material-library.js";
3
3
  export { resolveSavedAssetMaterials } from "./asset-materials-node.js";
4
4
  export declare function verifyAssetRecord(record: AssetRecord): Promise<void>;
5
5
  export declare class FileAssetLibrary implements AssetLibrary {
@@ -6,6 +6,7 @@ import { homedir, platform } from 'node:os';
6
6
  import { dirname, join, relative, resolve, sep, win32 } from 'node:path';
7
7
  import { localWorkspaceRoot } from "./workspace-location.js";
8
8
  import { assetIdSchema, assetManifestSchema, ASSET_LIMIT, ASSET_MATERIAL_BYTES, ASSET_MATERIAL_FILE, encodeAssetBundle, validateRecordShape, } from "./assets.js";
9
+ import { canonicalMaterialJson, } from "./material-library.js";
9
10
  import { FileMaterialLibrary, createMaterialLibraryPayload } from "./material-library-node.js";
10
11
  import { dependencyManifests, resolveSavedAssetMaterials } from "./asset-materials-node.js";
11
12
  import { renameDirectoryAtomically } from "./atomic-directory.js";
@@ -201,7 +202,7 @@ export class FileAssetLibrary {
201
202
  assertMaterialAllocation(records.filter((record) => record.materialResources ||
202
203
  record.manifest.build?.dependencies?.some((raw) => raw &&
203
204
  typeof raw === 'object' &&
204
- raw.kind === 'kiln.material.v1')));
205
+ ['kiln.material.v1', 'kiln.material.v2'].includes(String(raw.kind)))));
205
206
  let resourceBytes = 0;
206
207
  for (const record of records) {
207
208
  validateRecordShape(record);
@@ -222,10 +223,16 @@ export class FileAssetLibrary {
222
223
  (this.materials &&
223
224
  record.manifest.build?.dependencies?.some((raw) => raw &&
224
225
  typeof raw === 'object' &&
225
- raw.kind === 'kiln.material.v1'))) {
226
+ ['kiln.material.v1', 'kiln.material.v2'].includes(String(raw.kind))))) {
226
227
  const materials = await resolveSavedAssetMaterials(record, this.materials);
227
- for (const material of materials)
228
- resources.set(material.manifest.revisionId, material);
228
+ for (const material of materials) {
229
+ const key = `${material.manifest.materialId}/${material.manifest.revisionId}`;
230
+ const previous = resources.get(key);
231
+ if (previous &&
232
+ canonicalMaterialJson(previous.manifest) !== canonicalMaterialJson(material.manifest))
233
+ throw new Error('Conflicting asset material dependencies');
234
+ resources.set(key, material);
235
+ }
229
236
  prepared.push({
230
237
  ...record,
231
238
  materialResources: await createMaterialLibraryPayload(materials),
package/lib/assets.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /** Portable asset records. No filesystem, renderer, or server dependencies. */
2
2
  import { z } from 'zod';
3
- import { type MaterialLibraryPayloadV1 } from "./material-library.js";
3
+ import { type MaterialLibraryPayload, type MaterialManifest } from "./material-library.js";
4
4
  export declare const ASSET_LIMIT: number;
5
5
  export declare const assetIdSchema: z.ZodString;
6
6
  export declare const assetManifestSchema: z.ZodObject<{
@@ -50,7 +50,7 @@ export interface AssetRecord {
50
50
  manifest: AssetManifest;
51
51
  files: Record<string, Uint8Array>;
52
52
  /** Editable delivery closure; not part of the canonical asset manifest or revision identity. */
53
- materialResources?: MaterialLibraryPayloadV1;
53
+ materialResources?: MaterialLibraryPayload;
54
54
  }
55
55
  export interface AssetDraft {
56
56
  name: string;
@@ -82,6 +82,8 @@ export interface AssetLibrary {
82
82
  }
83
83
  export declare const ASSET_MATERIAL_FILE = "materials.kiln.json";
84
84
  export declare const ASSET_MATERIAL_BYTES: number;
85
+ /** Pure declaration boundary shared by portable delivery and host hash verification. */
86
+ export declare function assetMaterialRootManifests(record: AssetRecord): MaterialManifest[];
85
87
  /** Shape and closure checks are portable; hosts verify hashes and PNG/recipe bytes before import. */
86
88
  export declare function validateAssetMaterialClosure(record: AssetRecord, required?: boolean): void;
87
89
  export declare function validateRecordShape(record: AssetRecord): void;
package/lib/assets.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { z } from 'zod';
3
3
  import { BACKDROP_IDS } from "./views/background.js";
4
4
  import { zipSync, unzipSync } from 'three/addons/libs/fflate.module.js';
5
- import { assertMaterialJson, canonicalMaterialJson, materialLibraryPayloadSchema, validateMaterialManifest, MATERIAL_LIBRARY_LIMITS, } from "./material-library.js";
5
+ import { assertMaterialJson, canonicalMaterialJson, materialLibraryPayloadAnySchema, materialRecordDependencyPins, validateMaterialManifestAny, MATERIAL_LIBRARY_LIMITS, } from "./material-library.js";
6
6
  export const ASSET_LIMIT = 64 * 1024 * 1024;
7
7
  export const assetIdSchema = z.string().regex(/^[a-z][a-z0-9_-]{0,79}$/);
8
8
  const hash = z.string().regex(/^sha256:[a-f0-9]{64}$/);
@@ -47,32 +47,59 @@ export const assetManifestSchema = z.object({
47
47
  });
48
48
  export const ASSET_MATERIAL_FILE = 'materials.kiln.json';
49
49
  export const ASSET_MATERIAL_BYTES = 24 * 1024 * 1024;
50
- /** Shape and closure checks are portable; hosts verify hashes and PNG/recipe bytes before import. */
51
- export function validateAssetMaterialClosure(record, required = false) {
52
- const dependencies = (record.manifest.build?.dependencies ?? []).filter((raw) => raw &&
53
- typeof raw === 'object' &&
54
- raw.kind === 'kiln.material.v1');
55
- const expected = new Map(dependencies.map((dependency) => {
50
+ /** Pure declaration boundary shared by portable delivery and host hash verification. */
51
+ export function assetMaterialRootManifests(record) {
52
+ const roots = new Map();
53
+ for (const raw of record.manifest.build?.dependencies ?? []) {
54
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
55
+ throw new Error('Invalid saved asset dependency');
56
+ const dependency = raw;
57
+ if (dependency.kind !== 'kiln.material.v1' && dependency.kind !== 'kiln.material.v2') {
58
+ if (dependency.delivery === 'runtime')
59
+ throw new Error('Unsupported runtime dependency');
60
+ if (dependency.delivery !== 'embedded')
61
+ throw new Error('Unsupported saved asset dependency');
62
+ continue;
63
+ }
56
64
  if (dependency.delivery !== 'runtime')
57
65
  throw new Error('Invalid saved material dependency');
58
- const manifest = validateMaterialManifest(dependency.manifest);
59
- return [`${manifest.materialId}/${manifest.revisionId}`, canonicalMaterialJson(manifest)];
60
- }));
66
+ const manifest = validateMaterialManifestAny(dependency.manifest);
67
+ if (dependency.kind !== `kiln.material.v${manifest.schemaVersion}`)
68
+ throw new Error('Saved material dependency kind/version mismatch');
69
+ const key = `${manifest.materialId}/${manifest.revisionId}`;
70
+ const existing = roots.get(key);
71
+ if (existing && canonicalMaterialJson(existing) !== canonicalMaterialJson(manifest))
72
+ throw new Error('Conflicting saved material dependency manifests');
73
+ roots.set(key, manifest);
74
+ if (roots.size > MATERIAL_LIBRARY_LIMITS.maxPayloadRecords)
75
+ throw new Error('Saved asset material dependency count exceeds limit');
76
+ }
77
+ return [...roots.values()];
78
+ }
79
+ /** Shape and closure checks are portable; hosts verify hashes and PNG/recipe bytes before import. */
80
+ export function validateAssetMaterialClosure(record, required = false) {
81
+ const dependencies = assetMaterialRootManifests(record);
82
+ const expected = new Map(dependencies.map((manifest) => [
83
+ `${manifest.materialId}/${manifest.revisionId}`,
84
+ canonicalMaterialJson(manifest),
85
+ ]));
61
86
  if (!record.materialResources) {
62
87
  if (required && expected.size)
63
88
  throw new Error('Editable asset material closure unavailable');
64
89
  return;
65
90
  }
66
91
  assertMaterialJson(record.materialResources);
67
- const payload = materialLibraryPayloadSchema.parse(record.materialResources);
92
+ const payload = materialLibraryPayloadAnySchema.parse(record.materialResources);
68
93
  let bytes = 0;
69
94
  let pixels = 0;
70
- const seen = new Set();
95
+ const available = new Map();
71
96
  for (const wire of payload.records) {
97
+ const manifest = validateMaterialManifestAny(wire.manifest);
72
98
  const key = `${wire.manifest.materialId}/${wire.manifest.revisionId}`;
73
- if (seen.has(key) || expected.get(key) !== canonicalMaterialJson(wire.manifest))
99
+ if (available.has(key) ||
100
+ (expected.has(key) && expected.get(key) !== canonicalMaterialJson(manifest)))
74
101
  throw new Error('Asset material closure mismatch');
75
- seen.add(key);
102
+ available.set(key, manifest);
76
103
  if (Object.keys(wire.files).length !== wire.manifest.maps.length)
77
104
  throw new Error('Asset material map inventory mismatch');
78
105
  for (const map of wire.manifest.maps) {
@@ -82,11 +109,29 @@ export function validateAssetMaterialClosure(record, required = false) {
82
109
  pixels += map.width * map.height;
83
110
  }
84
111
  }
85
- if (seen.size !== expected.size)
86
- throw new Error('Asset material closure incomplete');
87
112
  if (bytes > MATERIAL_LIBRARY_LIMITS.maxPayloadBytes ||
88
113
  pixels > MATERIAL_LIBRARY_LIMITS.maxPayloadPixels)
89
114
  throw new Error('Asset material closure exceeds allocation limits');
115
+ const visited = new Set();
116
+ const active = new Set();
117
+ const visit = (key) => {
118
+ if (active.has(key))
119
+ throw new Error('Asset material closure contains a cycle');
120
+ if (visited.has(key))
121
+ return;
122
+ const manifest = available.get(key);
123
+ if (!manifest)
124
+ throw new Error('Asset material closure incomplete');
125
+ active.add(key);
126
+ for (const pin of materialRecordDependencyPins(manifest))
127
+ visit(`${pin.resourceId}/${pin.revisionId}`);
128
+ active.delete(key);
129
+ visited.add(key);
130
+ };
131
+ for (const key of expected.keys())
132
+ visit(key);
133
+ if (visited.size !== available.size)
134
+ throw new Error('Asset material closure has unrelated records');
90
135
  }
91
136
  const allowedFiles = new Set(['asset.glb', 'source.kiln.js', 'preview.png']);
92
137
  export function validateRecordShape(record) {
@@ -0,0 +1,86 @@
1
+ /** Internal atlas metadata owner. TextureInfo, rather than an image, owns each sampler use. */
2
+ import type { Document, Texture as GltfTexture, TextureInfo } from '@gltf-transform/core';
3
+ import * as THREE from 'three';
4
+ import { type CanonicalTextureAtlasSpecV1 } from "./texture-atlas-contract.js";
5
+ import type { TextureAtlasManifestV1 } from "./texture-atlas-pixels.js";
6
+ export declare const ATLAS_PROVENANCE_VERSION: 'kiln.atlas-provenance.v1';
7
+ export declare const ATLAS_PROVENANCE_LIMITS: Readonly<{
8
+ atlases: 128;
9
+ bytes: 4194304;
10
+ bindings: 1024;
11
+ entryBytes: 262144;
12
+ encodedBytes: 16777216;
13
+ aggregateEncodedBytes: 67108864;
14
+ bakeDecodedBytes: 67108864;
15
+ }>;
16
+ export interface AtlasProvenanceV1 {
17
+ spec: CanonicalTextureAtlasSpecV1;
18
+ manifest: TextureAtlasManifestV1;
19
+ }
20
+ export interface AtlasProvenanceTableV1 {
21
+ version: typeof ATLAS_PROVENANCE_VERSION;
22
+ atlases: (AtlasProvenanceV1 & {
23
+ id: string;
24
+ })[];
25
+ }
26
+ export interface AtlasReferenceV1 {
27
+ version: typeof ATLAS_PROVENANCE_VERSION;
28
+ id: string;
29
+ effectiveSampler?: {
30
+ minFilter: number | null;
31
+ magFilter: number | null;
32
+ wrapS: number;
33
+ wrapT: number;
34
+ };
35
+ finalMime?: string;
36
+ finalImageSha256?: string;
37
+ }
38
+ export interface AtlasBindingV1 {
39
+ version: typeof ATLAS_PROVENANCE_VERSION;
40
+ atlasId: string;
41
+ region: string;
42
+ inputOrigin: 'top-left' | 'bottom-left';
43
+ mode: 'content-boundary' | 'texel-center';
44
+ }
45
+ /** Reads only engine metadata data descriptors; author metadata and encoded arrays are not serialized. */
46
+ export declare function readAtlasTextureProvenance(texture: THREE.Texture): AtlasProvenanceV1 | undefined;
47
+ export declare function validateAtlasProvenanceTable(input: unknown): AtlasProvenanceTableV1;
48
+ export declare function expandAtlasBinding(input: unknown, table: AtlasProvenanceTableV1): {
49
+ manifestHash: `sha256:${string}`;
50
+ specHash: `sha256:${string}`;
51
+ layoutHash: `sha256:${string}`;
52
+ region: string;
53
+ content: import("./texture-atlas-contract.js").AtlasRectangleV1;
54
+ allocation: import("./texture-atlas-contract.js").AtlasRectangleV1;
55
+ inputOrigin: unknown;
56
+ mode: unknown;
57
+ scale: number[];
58
+ offset: number[];
59
+ };
60
+ /** A geometry-wide stamp can be associated only with the exact primary atlas supplied here. */
61
+ export declare function readAtlasGeometryBinding(geometry: THREE.BufferGeometry, provenance: AtlasProvenanceV1): AtlasBindingV1 | undefined;
62
+ export interface AtlasProvenanceCollection {
63
+ table?: AtlasProvenanceTableV1;
64
+ texture(texture: THREE.Texture): AtlasReferenceV1 | undefined;
65
+ binding(geometry: THREE.BufferGeometry): AtlasBindingV1 | undefined;
66
+ }
67
+ export declare function collectAtlasProvenance(root: THREE.Object3D): AtlasProvenanceCollection;
68
+ export declare function atlasDocumentTable(doc: Document): AtlasProvenanceTableV1 | undefined;
69
+ export declare function atlasMaterialUses(doc: Document): {
70
+ texture: GltfTexture;
71
+ info: TextureInfo;
72
+ }[];
73
+ /** Export/optimization only: bind source identity to the final encoded bytes and actual per-use sampler. */
74
+ export declare function refreshAtlasDocumentProvenance(doc: Document): Promise<void>;
75
+ /** Import is validation, never a repair or identity refresh. */
76
+ export declare function validateAtlasDocumentProvenance(doc: Document): Promise<void>;
77
+ /** Host-only owned identity established by the validated GLB loader; no serialized skip flag. */
78
+ export declare function readValidatedAtlasDelivery(texture: THREE.Texture): Readonly<AtlasReferenceV1> | undefined;
79
+ export declare function readAtlasEncodedTexture(texture: THREE.Texture): {
80
+ mime: 'image/png';
81
+ bytes: Uint8Array;
82
+ } | undefined;
83
+ /** Called only after validateAtlasDocumentProvenance; each sampler use gets owned Three state. */
84
+ export declare function restoreAtlasTextureProvenance(texture: THREE.Texture, info: TextureInfo, table: AtlasProvenanceTableV1): void;
85
+ /** Compose before mergeDocuments copies image/geometry buffers or deletes the source scene. */
86
+ export declare function mergeAtlasDocumentProvenance(target: Document, source: Document): Promise<void>;