dev/glyph-bank-alignment #3

Merged
bquarkz merged 4 commits from dev/glyph-bank-alignment into master 2026-04-10 06:14:08 +00:00
6 changed files with 107 additions and 25 deletions
Showing only changes of commit 76b5717cd8 - Show all commits

View File

@ -1,4 +1,5 @@
{"type":"meta","next_id":{"DSC":24,"AGD":26,"DEC":24,"PLN":47,"LSN":38,"CLSN":1}}
{"type":"meta","next_id":{"DSC":25,"AGD":28,"DEC":25,"PLN":48,"LSN":40,"CLSN":1}}
{"type":"discussion","id":"DSC-0025","status":"done","ticket":"packer-pipeline-metadata-ownership","title":"Pipeline Metadata Ownership and Runtime Contract","created_at":"2026-04-09","updated_at":"2026-04-10","tags":["packer","metadata","runtime-contract","tooling","studio"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0039","file":"discussion/lessons/DSC-0025-packer-pipeline-metadata-ownership/LSN-0039-runtime-header-boundary-and-tooling-owned-pipeline-metadata.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]}
{"type":"discussion","id":"DSC-0024","status":"done","ticket":"jacoco-reports-consolidation","title":"JaCoCo Reports Consolidation in Gradle","created_at":"2026-04-07","updated_at":"2026-04-07","tags":["infra","gradle","jacoco","coverage","jenkins"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0038","file":"discussion/lessons/DSC-0024-jacoco-reports-consolidation/LSN-0038-jacoco-reports-consolidation.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]}
{"type":"discussion","id":"DSC-0001","status":"done","ticket":"studio-docs-import","title":"Import docs/studio into discussion-framework artifacts","created_at":"2026-03-26","updated_at":"2026-03-26","tags":["studio","migration","discussion-framework","docs-import"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0001","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0001-assets-workspace-execution-wave-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0002","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0002-bank-composition-editor-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0003","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0003-mental-model-asset-mutations-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0004","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0004-mental-model-assets-workspace-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0005","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0005-mental-model-studio-events-and-components-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0006","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0006-mental-model-studio-shell-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0007","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0007-pack-wizard-shell-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0008","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0008-project-scoped-state-and-activity-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0016","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0016-studio-docs-import-pattern.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"}]}
{"type":"discussion","id":"DSC-0002","status":"open","ticket":"palette-management-in-studio","title":"Palette Management in Studio","created_at":"2026-03-26","updated_at":"2026-03-26","tags":["studio","legacy-import","palette-management","tile-bank","packer-boundary"],"agendas":[{"id":"AGD-0002","file":"AGD-0002-palette-management-in-studio.md","status":"open","created_at":"2026-03-26","updated_at":"2026-03-26"}],"decisions":[],"plans":[],"lessons":[]}

View File

@ -0,0 +1,71 @@
---
id: LSN-0039
ticket: packer-pipeline-metadata-ownership
title: Runtime Header Boundary and Tooling-Owned Pipeline Metadata
created: 2026-04-10
tags: [packer, metadata, runtime-contract, tooling, codec, assets-pa]
---
## Context
The packer had drift between implementation and spec around `output.pipeline` ownership. The runtime-facing artifact `assets.pa` was documented as if pipeline metadata should materialize under `asset_table[].metadata.pipeline`, while the implementation already treated at least part of that surface as tooling-oriented and excluded palette payloads from the runtime header.
This created the wrong architectural pressure: Studio and third-party tools could start depending on `assets.pa` for authoring/build provenance, and runtime-facing specs could silently absorb build-time structures that do not belong in the execution contract.
The discussion resolved that split explicitly and implemented it end to end in specs, packer materialization, and tests.
## Key Decisions
### Tooling Metadata Must Not Leak Into the Runtime Header
**What:**
`output.pipeline` remains tooling-only metadata in `asset.json` and related sidecar outputs. The runtime-facing `assets.pa` header must not mirror `output.pipeline` by default. Runtime-owned metadata remains under explicit, normalized fields in `asset_table[].metadata`.
**Why:**
`assets.pa` is the authoritative runtime artifact. If it starts carrying build provenance, editor-oriented structures, or reverse-sync payloads merely because they exist in the authoring manifest, the runtime contract becomes larger, noisier, and harder to evolve safely.
**Trade-offs:**
Tools that want provenance cannot rely on one binary artifact alone. They must read `asset.json` and/or `build/asset_table_metadata.json`. That is a deliberate trade-off in favor of a cleaner runtime boundary.
### Promotion Must Be Explicit, Not a Bulk Mirror
**What:**
If pipeline-derived information is needed at runtime, it must be promoted into an explicit normative field under runtime-owned metadata. The packer must not reintroduce a generic mirrored `pipeline` object under another name.
**Why:**
Bulk mirroring hides product decisions inside implementation convenience. Explicit promotion forces each field to justify its presence in the runtime contract.
**Trade-offs:**
This adds small editorial and implementation work whenever a new runtime-facing derived field is introduced, but it keeps the contract intentional and testable.
### Codec Serialization Must Stay in `SCREAMING_SNAKE_CASE`
**What:**
Serialized asset entry `codec` values are locked to `SCREAMING_SNAKE_CASE`. The currently known `NONE` value remains serialized as `NONE`.
**Why:**
The implementation already had a stable manifest/runtime representation through `OutputCodecCatalog`. Recording that casing in spec prevents future drift when more codecs are added.
**Trade-offs:**
Future codec additions must preserve the established serialized style unless a new decision explicitly revises the contract.
## Patterns and Algorithms
- Use separate surfaces for separate consumers: `assets.pa` for runtime contract, `asset_table_metadata.json` for tooling-oriented provenance, and `asset.json` for authoring intent.
- Build runtime metadata from explicit normalized fields first, then derive tooling sidecars from a superset when needed.
- Keep the runtime header free of raw authoring/build objects even if the sidecar keeps them intact for Studio or inspection tools.
- Lock serialized enum casing in spec as soon as the first value ships, even if the current catalog is minimal.
## Pitfalls
- Do not treat “already present in the manifest” as sufficient reason to serialize a field into the runtime header.
- Do not let sidecar convenience become implicit runtime compatibility.
- Do not partially filter a tooling object and leave an empty placeholder in runtime metadata unless that placeholder has explicit runtime meaning.
- Do not leave enum casing as an implementation accident. If the manifest value matters, the spec must say so.
## Takeaways
- The runtime header is a contract surface, not a metadata dump.
- Tooling provenance belongs in `asset.json` and `build/asset_table_metadata.json`, not in `assets.pa`.
- Runtime-facing derived metadata must be promoted explicitly field by field.
- Stable serialized casing rules should be fixed in spec before the value set grows.

View File

@ -92,8 +92,9 @@ Rules:
- `output.format` defines the semantic/runtime format contract;
- `output.codec` defines storage/extraction behavior;
- `output.codec` serialized values must use `SCREAMING_SNAKE_CASE`;
- `output.metadata` carries runtime-relevant format-specific detail;
- `output.pipeline` carries pipeline-injected metadata kept separate at authoring time;
- `output.pipeline` carries tooling/build metadata kept separate at authoring time;
- codec must remain explicit and must not be hidden inside format naming.
### Explicit Index Collections
@ -139,6 +140,8 @@ Rules:
- declaration-time metadata may come from multiple sources under the asset contract (for example, format metadata, codec-related metadata, and build/pipeline-derived declarations);
- `output.pipeline` may carry nested pipeline-derived metadata objects;
- this segmentation exists for authoring clarity and does not define multiple runtime sinks;
- `output.pipeline` is tooling-only and must not become part of the runtime-facing asset header by default;
- pipeline-derived values required at runtime must be promoted explicitly into normative runtime-owned metadata fields;
- runtime consumers must read effective metadata from the runtime asset entry metadata sink (`AssetEntry.metadata`);
- convergence/normalization behavior is normative in the build artifact specification.

View File

@ -95,9 +95,15 @@ Baseline normalized metadata segmentation:
- `output.metadata` materializes at the metadata root;
- `output.codec_configuration` materializes under `metadata.codec`;
- `output.pipeline` materializes under `metadata.pipeline`;
- `output.pipeline` remains tooling-only and must not materialize under `asset_table[].metadata` by default;
- pipeline-derived runtime-required values must be promoted explicitly into normative runtime-owned metadata fields;
- format-specific runtime-required fields may remain directly readable at the metadata root when the runtime consumer requires them there.
Asset entry codec serialization:
- `asset_table[].codec` MUST use `SCREAMING_SNAKE_CASE`;
- the current known `NONE` value serializes as `NONE`.
### Preload
Preload is emitted deterministically from per-asset declaration.
@ -120,7 +126,7 @@ Rules:
- `build/asset_table.json` mirrors `header.asset_table` 1:1;
- `build/preload.json` mirrors `header.preload` 1:1;
- `build/asset_table_metadata.json` is tooling-only;
- `build/asset_table_metadata.json` is tooling-only and may retain pipeline/build provenance that is intentionally excluded from the runtime header;
- richer tooling data must not be added to the 1:1 mirror files.
## Alignment and Offsets

View File

@ -878,7 +878,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
"size", packedAsset.payload().length,
"decoded_size", packedAsset.decodedSize(),
"codec", packedAsset.codec(),
"metadata", packedAsset.metadata())));
"metadata", packedAsset.runtimeMetadata())));
if (packedAsset.preloadEnabled()) {
final int slot = nextPreloadSlotByBankType.getOrDefault(packedAsset.bankType(), 0);
nextPreloadSlotByBankType.put(packedAsset.bankType(), slot + 1);
@ -888,7 +888,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
}
assetTableMetadata.add(new LinkedHashMap<>(Map.of(
"asset_id", packedAsset.assetId(),
"metadata", packedAsset.metadata())));
"metadata", packedAsset.toolingMetadata())));
}
final byte[] headerBytes = canonicalJsonBytes(Map.of(
@ -960,20 +960,22 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
System.arraycopy(packedPixels, 0, payload, 0, packedPixels.length);
System.arraycopy(paletteBytes, 0, payload, packedPixels.length, paletteBytes.length);
final LinkedHashMap<String, Object> metadata = new LinkedHashMap<>();
metadata.put("tile_size", tileSize);
metadata.put("width", width);
metadata.put("height", height);
metadata.put("palette_count", 64);
metadata.put("palette_authored", countAuthoredTileBankPalettes(declaration));
metadata.put("codec", Map.of());
metadata.put("pipeline", normalizeTileBankRuntimePipelineMetadata(declaration));
final LinkedHashMap<String, Object> runtimeMetadata = new LinkedHashMap<>();
runtimeMetadata.put("tile_size", tileSize);
runtimeMetadata.put("width", width);
runtimeMetadata.put("height", height);
runtimeMetadata.put("palette_count", 64);
runtimeMetadata.put("palette_authored", countAuthoredTileBankPalettes(declaration));
runtimeMetadata.put("codec", Map.of());
declaration.outputMetadata().forEach((key, value) -> {
if (!"tile_size".equals(key)) {
metadata.putIfAbsent(key, value);
runtimeMetadata.putIfAbsent(key, value);
}
});
final LinkedHashMap<String, Object> toolingMetadata = new LinkedHashMap<>(runtimeMetadata);
toolingMetadata.put("pipeline", normalizeToolingPipelineMetadata(declaration));
final PackerRegistryEntry registryEntry = runtimeAsset.registryEntry()
.orElseThrow(() -> new IllegalStateException("Packed runtime asset must be registered"));
return new PackedAsset(
@ -983,7 +985,8 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
"NONE",
payload,
width * height + 2048,
metadata,
runtimeMetadata,
toolingMetadata,
declaration.preloadEnabled());
}
@ -1044,11 +1047,8 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
return palettesNode instanceof ArrayNode palettesArray ? palettesArray.size() : 0;
}
private Map<String, Object> normalizeTileBankRuntimePipelineMetadata(PackerAssetDeclaration declaration) {
final LinkedHashMap<String, Object> pipeline = new LinkedHashMap<>(
PackerReadMessageMapper.normalizeMetadata(declaration.outputPipelineMetadata()));
pipeline.remove("palettes");
return pipeline;
private Map<String, Object> normalizeToolingPipelineMetadata(PackerAssetDeclaration declaration) {
return new LinkedHashMap<>(PackerReadMessageMapper.normalizeMetadata(declaration.outputPipelineMetadata()));
}
private byte[] buildPrelude(int headerLength) {
@ -1111,7 +1111,8 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
String codec,
byte[] payload,
int decodedSize,
Map<String, Object> metadata,
Map<String, Object> runtimeMetadata,
Map<String, Object> toolingMetadata,
boolean preloadEnabled) {
}

View File

@ -337,8 +337,7 @@ final class FileSystemPackerWorkspaceServiceTest {
assertEquals(256, assetTable.get(0).path("metadata").path("height").asInt());
assertEquals(64, assetTable.get(0).path("metadata").path("palette_count").asInt());
assertEquals(1, assetTable.get(0).path("metadata").path("palette_authored").asInt());
assertTrue(assetTable.get(0).path("metadata").path("pipeline").isObject());
assertTrue(assetTable.get(0).path("metadata").path("pipeline").path("palettes").isMissingNode());
assertTrue(assetTable.get(0).path("metadata").path("pipeline").isMissingNode());
final var preload = MAPPER.readTree(projectRoot.resolve("build/preload.json").toFile());
assertEquals(1, preload.size());
@ -348,7 +347,8 @@ final class FileSystemPackerWorkspaceServiceTest {
final var assetTableMetadata = MAPPER.readTree(projectRoot.resolve("build/asset_table_metadata.json").toFile());
assertEquals(1, assetTableMetadata.size());
assertEquals(1, assetTableMetadata.get(0).path("metadata").path("palette_authored").asInt());
assertTrue(assetTableMetadata.get(0).path("metadata").path("pipeline").path("palettes").isMissingNode());
assertEquals(1, assetTableMetadata.get(0).path("metadata").path("pipeline").path("palettes").size());
assertEquals(0, assetTableMetadata.get(0).path("metadata").path("pipeline").path("palettes").get(0).path("index").asInt());
}
@Test