Asset Entry Metadata Normalization Contract

This commit is contained in:
bQUARKz 2026-04-09 08:25:37 +01:00
parent 9228fb1e29
commit 52d4a91c71
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
6 changed files with 110 additions and 138 deletions

View File

@ -186,29 +186,22 @@ impl AssetManager {
fn decode_tile_bank_layout(
entry: &AssetEntry,
) -> Result<(TileSize, usize, usize, usize), String> {
let tile_size_val =
entry.metadata.get("tile_size").and_then(|v| v.as_u64()).ok_or("Missing tile_size")?;
let width =
entry.metadata.get("width").and_then(|v| v.as_u64()).ok_or("Missing width")? as usize;
let height =
entry.metadata.get("height").and_then(|v| v.as_u64()).ok_or("Missing height")? as usize;
let palette_count = entry
.metadata
.get("palette_count")
.and_then(|v| v.as_u64())
.ok_or("Missing palette_count")? as usize;
let meta = entry.metadata_as_tiles()?;
let tile_size = match tile_size_val {
let tile_size = match meta.tile_size {
8 => TileSize::Size8,
16 => TileSize::Size16,
32 => TileSize::Size32,
_ => return Err(format!("Invalid tile_size: {}", tile_size_val)),
_ => return Err(format!("Invalid tile_size: {}", meta.tile_size)),
};
if palette_count != TILE_BANK_PALETTE_COUNT_V1 {
return Err(format!("Invalid palette_count: {}", palette_count));
if meta.palette_count as usize != TILE_BANK_PALETTE_COUNT_V1 {
return Err(format!("Invalid palette_count: {}", meta.palette_count));
}
let width = meta.width as usize;
let height = meta.height as usize;
let logical_pixels = width.checked_mul(height).ok_or("TileBank dimensions overflow")?;
let serialized_pixel_bytes = logical_pixels.div_ceil(2);
let serialized_size = serialized_pixel_bytes
@ -637,8 +630,8 @@ impl AssetManager {
entry: &AssetEntry,
buffer: &[u8],
) -> Result<SoundBank, String> {
let sample_rate =
entry.metadata.get("sample_rate").and_then(|v| v.as_u64()).unwrap_or(44100) as u32;
let meta = entry.metadata_as_sounds()?;
let sample_rate = meta.sample_rate;
let mut data = Vec::with_capacity(buffer.len() / 2);
for i in (0..buffer.len()).step_by(2) {

View File

@ -24,6 +24,43 @@ pub struct AssetEntry {
pub metadata: serde_json::Value,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct TilesMetadata {
pub tile_size: u32,
pub width: u32,
pub height: u32,
pub palette_count: u32,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SoundsMetadata {
pub sample_rate: u32,
}
impl AssetEntry {
pub fn metadata_as_tiles(&self) -> Result<TilesMetadata, String> {
if self.bank_type != BankType::TILES {
return Err(format!(
"Asset {} is not a TILES bank (type: {:?})",
self.asset_id, self.bank_type
));
}
serde_json::from_value(self.metadata.clone())
.map_err(|e| format!("Invalid TILES metadata for asset {}: {}", self.asset_id, e))
}
pub fn metadata_as_sounds(&self) -> Result<SoundsMetadata, String> {
if self.bank_type != BankType::SOUNDS {
return Err(format!(
"Asset {} is not a SOUNDS bank (type: {:?})",
self.asset_id, self.bank_type
));
}
serde_json::from_value(self.metadata.clone())
.map_err(|e| format!("Invalid SOUNDS metadata for asset {}: {}", self.asset_id, e))
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct PreloadEntry {
pub asset_id: AssetId,

View File

@ -1,4 +1,4 @@
{"type":"meta","next_id":{"DSC":21,"AGD":19,"DEC":3,"PLN":3,"LSN":23,"CLSN":1}}
{"type":"meta","next_id":{"DSC":21,"AGD":19,"DEC":5,"PLN":3,"LSN":24,"CLSN":1}}
... (mantendo as linhas anteriores) ...
{"type":"discussion","id":"DSC-0020","status":"done","ticket":"jenkins-gitea-integration","title":"Jenkins Gitea Integration and Relocation","created_at":"2026-04-07","updated_at":"2026-04-07","tags":["ci","jenkins","gitea"],"agendas":[{"id":"AGD-0018","file":"workflow/agendas/AGD-0018-jenkins-gitea-integration-and-relocation.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"decisions":[{"id":"DEC-0003","file":"workflow/decisions/DEC-0003-jenkins-gitea-strategy.md","status":"accepted","created_at":"2026-04-07","updated_at":"2026-04-07"}],"plans":[{"id":"PLN-0003","file":"workflow/plans/PLN-0003-jenkins-gitea-execution.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"lessons":[{"id":"LSN-0021","file":"lessons/DSC-0020-jenkins-gitea-integration/LSN-0021-jenkins-gitea-integration.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]}
{"type":"discussion","id":"DSC-0001","status":"done","ticket":"legacy-runtime-learn-import","title":"Import legacy runtime learn into discussion lessons","created_at":"2026-03-27","updated_at":"2026-03-27","tags":["migration","tech-debt"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0001","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0001-prometeu-learn-index.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0002","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0002-historical-asset-status-first-fault-and-return-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0003","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0003-historical-audio-status-first-fault-and-return-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0004","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0004-historical-cartridge-boot-protocol-and-manifest-authority.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0005","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0005-historical-game-memcard-slots-surface-and-semantics.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0006","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0006-historical-gfx-status-first-fault-and-return-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0007","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0007-historical-retired-fault-and-input-decisions.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0008","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0008-historical-vm-core-and-assets.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0009","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0009-mental-model-asset-management.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0010","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0010-mental-model-audio.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0011","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0011-mental-model-gfx.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0012","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0012-mental-model-input.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0013","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0013-mental-model-observability-and-debugging.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0014","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0014-mental-model-portability-and-cross-platform.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0015","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0015-mental-model-save-memory-and-memcard.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0016","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0016-mental-model-status-first-and-fault-thinking.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0017","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0017-mental-model-time-and-cycles.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0018","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0018-mental-model-touch.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"}]}
@ -17,6 +17,6 @@
{"type":"discussion","id":"DSC-0014","status":"open","ticket":"perf-vm-allocation-and-copy-pressure","title":"Agenda - [PERF] VM Allocation and Copy Pressure","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0013","file":"workflow/agendas/AGD-0013-perf-vm-allocation-and-copy-pressure.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0015","status":"open","ticket":"perf-cartridge-boot-and-program-ownership","title":"Agenda - [PERF] Cartridge Boot and Program Ownership","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0014","file":"workflow/agendas/AGD-0014-perf-cartridge-boot-and-program-ownership.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0016","status":"done","ticket":"tilemap-empty-cell-vs-tile-id-zero","title":"Tilemap Empty Cell vs Tile ID Zero","created_at":"2026-03-27","updated_at":"2026-04-09","tags":[],"agendas":[{"id":"AGD-0015","file":"workflow/agendas/AGD-0015-tilemap-empty-cell-vs-tile-id-zero.md","status":"done","created_at":"2026-03-27","updated_at":"2026-04-09"}],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0022","file":"lessons/DSC-0016-tilemap-empty-cell-semantics/LSN-0022-tilemap-empty-cell-convergence.md","status":"done","created_at":"2026-04-09","updated_at":"2026-04-09"}]}
{"type":"discussion","id":"DSC-0017","status":"open","ticket":"asset-entry-metadata-normalization-contract","title":"Asset Entry Metadata Normalization Contract","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0016","file":"workflow/agendas/AGD-0016-asset-entry-metadata-normalization-contract.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0017","status":"done","ticket":"asset-entry-metadata-normalization-contract","title":"Asset Entry Metadata Normalization Contract","created_at":"2026-03-27","updated_at":"2026-04-09","tags":[],"agendas":[{"id":"AGD-0016","file":"workflow/agendas/AGD-0016-asset-entry-metadata-normalization-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-04-09"}],"decisions":[{"id":"DEC-0004","file":"workflow/decisions/DEC-0004-asset-entry-metadata-normalization-contract.md","status":"accepted","created_at":"2026-04-09","updated_at":"2026-04-09"}],"plans":[],"lessons":[{"id":"LSN-0023","file":"lessons/DSC-0017-asset-metadata-normalization/LSN-0023-typed-asset-metadata-helpers.md","status":"done","created_at":"2026-04-09","updated_at":"2026-04-09"}]}
{"type":"discussion","id":"DSC-0018","status":"done","ticket":"asset-load-asset-id-int-contract","title":"Asset Load Asset ID Int Contract","created_at":"2026-03-27","updated_at":"2026-03-27","tags":["asset","runtime","abi"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0019","file":"lessons/DSC-0018-asset-load-asset-id-int-contract/LSN-0019-asset-load-id-abi-convergence.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"}]}
{"type":"discussion","id":"DSC-0019","status":"done","ticket":"jenkinsfile-correction","title":"Jenkinsfile Correction and Relocation","created_at":"2026-04-07","updated_at":"2026-04-07","tags":["ci","jenkins"],"agendas":[{"id":"AGD-0017","file":"workflow/agendas/AGD-0017-jenkinsfile-correction.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"decisions":[{"id":"DEC-0002","file":"workflow/decisions/DEC-0002-jenkinsfile-strategy.md","status":"accepted","created_at":"2026-04-07","updated_at":"2026-04-07"}],"plans":[{"id":"PLN-0002","file":"workflow/plans/PLN-0002-jenkinsfile-execution.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"lessons":[{"id":"LSN-0020","file":"lessons/DSC-0019-jenkins-ci-standardization/LSN-0020-jenkins-standard-relocation.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]}

View File

@ -0,0 +1,33 @@
# LSN-0023: Typed Helpers for Asset Metadata
Status: Published
Decisions: [[DEC-0004]]
Tags: [asset, runtime, rust, pattern]
## Contexto
A decisão [[DEC-0004]] estabeleceu um contrato de metadados segmentados para assets, mantendo campos críticos na raiz do JSON e detalhes técnicos em subárvores (`codec`, `pipeline`). No entanto, o `AssetEntry.metadata` no runtime é um `serde_json::Value` dinâmico.
## Lição
O uso de `serde_json::Value` diretamente nos loaders do runtime introduz riscos de runtime (erros de tipo, campos ausentes) e polui o código com chamadas repetitivas de `.get()`, `.as_u64()`, etc.
### Abordagem Adotada
Para mitigar isso, implementamos o padrão de **Typed Metadata Helpers**:
1. **Structs Dedicadas**: Criamos structs Rust (ex: `TilesMetadata`, `SoundsMetadata`) que representam o contrato exato de cada banco.
2. **Conversion Methods**: Adicionamos métodos ao `AssetEntry` (ex: `metadata_as_tiles()`) que utilizam `serde_json::from_value` para realizar o "cast" do JSON dinâmico para a struct tipada.
3. **Fail-Fast**: A falha no parsing dos metadados deve ser tratada como erro de carregamento do asset, garantindo que o motor não opere com metadados corrompidos ou incompletos.
### Benefícios
- **Segurança de Tipo**: Erros de estrutura de metadados são detectados no momento do carregamento.
- **Ergonomia**: O código dos drivers passa a usar `meta.tile_size` em vez de parsing manual.
- **Desacoplamento**: A complexidade do JSON fica encapsulada nos helpers de conversão.
## Referências
- DEC-0004: Asset Entry Metadata Normalization Contract
- `prometeu-hal/src/asset.rs`
- `prometeu-drivers/src/asset.rs`

View File

@ -1,100 +0,0 @@
---
id: AGD-0016
ticket: asset-entry-metadata-normalization-contract
title: Asset Entry Metadata Normalization Contract
status: open
created: 2026-03-27
resolved:
decision:
tags: []
---
# Asset Entry Metadata Normalization Contract
Status: Open
Domain Owner: `docs/runtime`
Cross-Domain Impact: `../studio/docs/packer`, `shipper`, `asset` loader
## Purpose
Normatizar como `AssetEntry.metadata` deve preservar a convergencia entre metadata autoral, metadata de codec e metadata de pipeline sem colapsar tudo num mapa plano ambiguo.
## Problem
O lado produtor (`packer`) ja convergiu para um contrato em que o runtime precisa ler campos obrigatorios diretamente de `AssetEntry.metadata`, mas tambem precisa manter segmentacao suficiente para nao perder significado entre:
- `asset.json.output.metadata`
- `asset.json.output.codec_configuration`
- `asset.json.output.pipeline`
Sem um contrato explicito no runtime:
- o packer pode materializar estruturas diferentes entre formatos;
- o loader/runtime pode passar a depender de flattening incidental;
- tooling e debug surfaces perdem previsibilidade;
- futuros formatos tendem a misturar metadata efetiva com detalhe interno de pipeline.
## Context
No ciclo atual de `tile bank`, o produtor ja fechou esta direcao:
- `asset.json.output.metadata` -> `AssetEntry.metadata`
- `asset.json.output.codec_configuration` -> `AssetEntry.metadata.codec`
- `asset.json.output.pipeline` -> `AssetEntry.metadata.pipeline`
Ao mesmo tempo, o runtime ainda consome alguns campos obrigatorios do tile bank diretamente no nivel raiz de `AssetEntry.metadata`, em especial:
- `tile_size`
- `width`
- `height`
- `palette_count`
A agenda precisa fechar se esse shape vira contrato geral de runtime para metadata normalizada de assets, e como o consumidor deve tratar campos obrigatorios format-specific versus subtrees segmentadas.
## Options
### Option A - Flat effective metadata map only
Tudo converge para um unico mapa plano em `AssetEntry.metadata`.
### Option B - Root effective metadata plus stable segmented subtrees
Campos runtime-obrigatorios ficam legiveis no nivel raiz, enquanto dados de codec e pipeline ficam em subtrees estaveis:
- `metadata.<field>`
- `metadata.codec.<field>`
- `metadata.pipeline.<field>`
### Option C - Fully segmented metadata only
Nada fica no nivel raiz; todo consumo passa por subtrees por origem.
## Tradeoffs
- Option A simplifica leitura curta, mas perde origem semantica e aumenta risco de colisao.
- Option B preserva leitura direta para campos obrigatorios do runtime e mantem segmentacao estavel para evolucao futura.
- Option C e semanticamente limpa, mas quebra o consumo direto atual de formatos como `tile bank` e introduz custo de migracao desnecessario agora.
## Recommendation
Adotar `Option B`.
Direcao recomendada:
- campos format-specific obrigatorios para decode/runtime continuam legiveis no nivel raiz de `AssetEntry.metadata`;
- `output.codec_configuration` materializa em `AssetEntry.metadata.codec`;
- `output.pipeline` materializa em `AssetEntry.metadata.pipeline`;
- o runtime nao deve exigir flattening total para consumir metadata segmentada;
- specs format-specific devem declarar explicitamente quais campos sao obrigatorios no nivel raiz.
## Open Questions
1. O contrato deve tratar o subtree raiz como semanticamente equivalente a `output.metadata` ou como effective metadata map mais amplo?
2. Quais readers/helpers do runtime devem ser criados para evitar parsing manual disperso de `metadata.codec` e `metadata.pipeline`?
## Expected Follow-up
1. Converter esta agenda em decision no `runtime`.
2. Propagar a decisao para `15-asset-management.md`.
3. Ajustar loaders format-specific para usar helpers consistentes de metadata quando necessario.
4. Alinhar o `packer` e testes de conformance com o shape final.

View File

@ -110,37 +110,46 @@ This table describes content identity and storage layout, not live residency.
### 4.1 `TILES` asset contract in v1
For `BankType::TILES`, the runtime-facing v1 contract is:
Para `BankType::TILES`, o contrato v1 voltado para o runtime é:
- `codec = NONE`
- serialized pixels use packed `u4` palette indices
- serialized palettes use `RGB565` (`u16`, little-endian)
- pixels serializados usam índices de paleta `u4` empacotados
- paletas serializadas usam `RGB565` (`u16`, little-endian)
- `palette_count = 64`
- runtime materialization may expand pixel indices to one `u8` per pixel
- a materialização em runtime pode expandir índices de pixel para um `u8` por pixel
`NONE` for `TILES` means there is no additional generic codec layer beyond the bank contract itself.
`NONE` para `TILES` significa que não há camada de codec genérica adicional além do próprio contrato do banco.
For the current transition window:
Para a janela de transição atual:
- `RAW` is a deprecated legacy alias of `NONE`
- new published material must use `NONE` as the canonical value
- `RAW` é um alias legado e depreciado de `NONE`
- novos materiais publicados devem usar `NONE` como valor canônico
Even with `codec = NONE`, `TILES` still requires deterministic bank-specific decode from its serialized payload. The serialized byte layout is therefore not required to be identical to the in-memory layout.
Mesmo com `codec = NONE`, `TILES` ainda requer decode específico de banco a partir de seu payload serializado. O layout de bytes serializados não precisa, portanto, ser idêntico ao layout em memória.
Required `AssetEntry.metadata` fields for `TILES`:
#### 4.1.1 Metadata Normalization
- `tile_size`: tile edge in pixels; valid values are `8`, `16`, or `32`
- `width`: full bank sheet width in pixels
- `height`: full bank sheet height in pixels
- `palette_count`: number of palettes serialized for the bank
Seguindo a `DEC-0004`, o campo `AssetEntry.metadata` deve ser estruturado de forma segmentada para evitar ambiguidades.
Validation rules for `TILES` v1:
Campos de metadados obrigatórios (efetivos) para `TILES` no nível raiz:
- `palette_count` must be `64`
- `width * height` defines the number of logical indexed pixels in the decoded sheet
- additional metadata may exist, but the runtime contract must not depend on it to reconstruct the bank in memory
- `tile_size`: aresta do tile em pixels; valores válidos são `8`, `16`, ou `32`
- `width`: largura total da folha do banco em pixels
- `height`: altura total da folha do banco em pixels
- `palette_count`: número de paletas serializadas para o banco
Serialized payload layout for `TILES` v1:
Subárvores opcionais e informativas:
- `metadata.codec`: Configuração específica do codec/compressor (ex: dicionários, flags de compressão).
- `metadata.pipeline`: Metadados informativos do processo de build do Studio (ex: source hashes, timestamps, tool versions).
Regras de validação para `TILES` v1:
- `palette_count` deve ser `64`
- `width * height` define o número de pixels indexados lógicos na folha decodificada
- metadados adicionais podem existir, mas o contrato do runtime não deve depender deles para reconstruir o banco em memória (exceto se definidos na raiz como campos efetivos).
#### 4.1.2 Payload Layout
1. packed indexed pixels for the full sheet, using `ceil(width * height / 2)` bytes;
2. palette table, using `palette_count * 16 * 2` bytes.