feat: complete TASK-WP5-03 asset allowlist
Dada P0-A isolated Windows CI / validate-and-package (push) Failing after 1m5s
Dada P0-A isolated Windows CI / validate-and-package (push) Failing after 1m5s
This commit is contained in:
@@ -14,6 +14,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@dada/static-sticker-catalog": "workspace:*",
|
||||
"@dada/template-registry": "workspace:*",
|
||||
"csv-parse": "7.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { isAbsolute, dirname, join, relative, resolve, sep } from "node:path";
|
||||
import { parse } from "csv-parse/sync";
|
||||
|
||||
import type { StaticStickerCatalog, StaticStickerCatalogItem } from "@dada/static-sticker-catalog";
|
||||
import { P0A_TEXT_TEMPLATE_IDS } from "@dada/template-registry";
|
||||
|
||||
type JsonObject = Record<string, unknown>;
|
||||
type CsvRow = Record<string, string>;
|
||||
@@ -487,6 +488,24 @@ export function compileAssetArchive(options: AssetCompilerOptions): AssetCompile
|
||||
ids.add(entry.canonical_id);
|
||||
}
|
||||
|
||||
const fontPanelIdBySha256 = new Map<string, string>();
|
||||
for (const entry of entries.filter((item) => item.collection_id === "font_panel")) {
|
||||
const value = entry.metadata.local_sha256;
|
||||
if (typeof value === "string" && /^[0-9a-f]{64}$/i.test(value)) fontPanelIdBySha256.set(value.toUpperCase(), entry.canonical_id);
|
||||
}
|
||||
const p0aTextIds = new Set<string>(P0A_TEXT_TEMPLATE_IDS);
|
||||
const fontRegistrations = new Map<string, { panelIds: string[]; sha256s: string[] }>();
|
||||
for (const entry of entries.filter((item) => item.collection_id === "text_templates" && p0aTextIds.has(item.canonical_id))) {
|
||||
const references = collectFiles(entry.metadata, ["fonts"], `${entry.canonical_id}.fonts`);
|
||||
const sha256s = references.map((reference) => {
|
||||
const path = resolveSourcePath(reference, entry.item_directory, entry.collection_root, `${entry.canonical_id} font`);
|
||||
return sha256(tracker.read(path, `${entry.canonical_id} font`));
|
||||
});
|
||||
const panelIds = [...new Set(sha256s.map((hash) => fontPanelIdBySha256.get(hash)).filter((id): id is string => id !== undefined))]
|
||||
.sort((left, right) => Number(left.slice(4)) - Number(right.slice(4)));
|
||||
fontRegistrations.set(entry.canonical_id, { panelIds, sha256s });
|
||||
}
|
||||
|
||||
const outputDirectory = resolve(options.outputDirectory);
|
||||
const outputRoots = [manifestDirectory, ...roots.roots.map((item) => item.path)];
|
||||
if (outputRoots.some((root) => inside(outputDirectory, root))) throw new Error("output directory is inside a source root");
|
||||
@@ -527,6 +546,12 @@ export function compileAssetArchive(options: AssetCompilerOptions): AssetCompile
|
||||
};
|
||||
if (entry.metadata.default_text !== undefined || entry.row.default_text !== undefined) item.default_text = entry.metadata.default_text ?? entry.row.default_text ?? "";
|
||||
if (defaultFont) item.default_font_reference = relativeReference(defaultFont, "default font");
|
||||
const fontRegistration = fontRegistrations.get(entry.canonical_id);
|
||||
if (fontRegistration) {
|
||||
item.font_panel_references = fontRegistration.panelIds;
|
||||
item.font_resource_sha256s = fontRegistration.sha256s;
|
||||
}
|
||||
if (entry.collection_id === "font_panel" && typeof entry.metadata.local_sha256 === "string") item.source_sha256 = entry.metadata.local_sha256.toUpperCase();
|
||||
if (preview) item.preview_reference = relativeReference(preview, "preview");
|
||||
if (typeof runtime.stable_style_id === "string") item.renderer_id = runtime.stable_style_id;
|
||||
return item;
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "@dada/asset-renderer",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./dist/index.js"
|
||||
},
|
||||
"types": "./dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"typecheck": "tsc --noEmit -p tsconfig.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dada/template-registry": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "7.0.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { P0A_COLOR_CARD_IDS } from "@dada/template-registry";
|
||||
|
||||
export interface ColorCardDefinition {
|
||||
cardId: typeof P0A_COLOR_CARD_IDS[number];
|
||||
displayName: string;
|
||||
mappingStatus: "confirmed_native_mapping" | "stable_web_style_native_mapping_provisional";
|
||||
rendererName: "horizontal_line" | "ticket_strip" | "vertical_stack" | "vertical_ticket";
|
||||
styleId: "style_01" | "style_02" | "style_08" | "style_16";
|
||||
}
|
||||
|
||||
export type FiveColorPalette = readonly [string, string, string, string, string];
|
||||
|
||||
export interface ColorCardRenderPlan extends ColorCardDefinition {
|
||||
palette: FiveColorPalette;
|
||||
}
|
||||
|
||||
export const P0A_COLOR_CARD_DEFINITIONS: readonly ColorCardDefinition[] = [
|
||||
{ cardId: "COLOR001", displayName: "纵向票据", mappingStatus: "confirmed_native_mapping", rendererName: "vertical_ticket", styleId: "style_01" },
|
||||
{ cardId: "COLOR002", displayName: "纵向色阶", mappingStatus: "stable_web_style_native_mapping_provisional", rendererName: "vertical_stack", styleId: "style_02" },
|
||||
{ cardId: "COLOR008", displayName: "横向标尺", mappingStatus: "stable_web_style_native_mapping_provisional", rendererName: "horizontal_line", styleId: "style_08" },
|
||||
{ cardId: "COLOR016", displayName: "横向票条", mappingStatus: "stable_web_style_native_mapping_provisional", rendererName: "ticket_strip", styleId: "style_16" },
|
||||
] as const;
|
||||
|
||||
function paletteSnapshot(palette: readonly string[]): FiveColorPalette {
|
||||
if (palette.length !== 5 || palette.some((color) => !/^#[0-9A-Fa-f]{6}$/.test(color))) {
|
||||
throw new Error("color-card renderer requires exactly five colors");
|
||||
}
|
||||
return Object.freeze(palette.map((color) => color.toUpperCase())) as unknown as FiveColorPalette;
|
||||
}
|
||||
|
||||
export function createP0aColorCardRenderPlans(palette: readonly string[]): readonly ColorCardRenderPlan[] {
|
||||
const snapshot = paletteSnapshot(palette);
|
||||
return P0A_COLOR_CARD_DEFINITIONS.map((definition) => ({ ...definition, palette: snapshot }));
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"lib": ["ES2024"],
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "@dada/template-registry",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./dist/index.js"
|
||||
},
|
||||
"types": "./dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"typecheck": "tsc --noEmit -p tsconfig.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dada/static-sticker-catalog": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "7.0.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
import type { StaticStickerCatalog, StaticStickerCatalogItem } from "@dada/static-sticker-catalog";
|
||||
|
||||
export const P0A_TEXT_TEMPLATE_IDS = [
|
||||
"FLOWER001", "FLOWER002", "FLOWER003", "FLOWER004", "FLOWER005", "FLOWER006", "FLOWER007", "FLOWER008",
|
||||
"H001", "H002", "H003", "H004", "H005", "H006", "H007", "H008",
|
||||
"TAG001", "TAG002", "TAG003", "TAG004", "TAG005", "TAG006", "TAG007", "TAG051",
|
||||
"SIMPLE001", "SIMPLE002", "SIMPLE003", "SIMPLE004", "SIMPLE005", "SIMPLE006", "SIMPLE007", "SIMPLE008",
|
||||
] as const;
|
||||
|
||||
// Derived from exact package-hash matches between the 32 frozen templates and the 86-item font panel.
|
||||
export const P0A_REQUIRED_FONT_PANEL_IDS = [
|
||||
"FONT005", "FONT008", "FONT011", "FONT021", "FONT022", "FONT027",
|
||||
"FONT039", "FONT043", "FONT046", "FONT052", "FONT081",
|
||||
] as const;
|
||||
|
||||
export const P0A_COLOR_CARD_IDS = ["COLOR001", "COLOR002", "COLOR008", "COLOR016"] as const;
|
||||
|
||||
export const P0A_DYNAMIC_STICKER_IDS = [
|
||||
"DYN001", "DYN002", "DYN003", "DYN004", "DYN007",
|
||||
"DYN008", "DYN011", "DYN012", "DYN015", "DYN016",
|
||||
] as const;
|
||||
|
||||
export type RegisteredComplexFamily = "color_card" | "font_panel" | "interactive_sticker" | "text_template";
|
||||
|
||||
export interface RegisteredComplexAsset extends Record<string, unknown> {
|
||||
canonical_id: string;
|
||||
family: RegisteredComplexFamily;
|
||||
font_panel_references?: string[];
|
||||
release_status: string;
|
||||
release_tier: string;
|
||||
validation_status: string;
|
||||
}
|
||||
|
||||
export interface ComplexRegistryManifest {
|
||||
items: RegisteredComplexAsset[];
|
||||
release_version: string;
|
||||
schema_version: string;
|
||||
}
|
||||
|
||||
export interface PublicComplexAsset extends RegisteredComplexAsset {
|
||||
release_status: "enabled";
|
||||
release_tier: "alpha_whitelist";
|
||||
validation_status: "passed";
|
||||
}
|
||||
|
||||
export interface P0aPublicManifest {
|
||||
assets: {
|
||||
color_cards: PublicComplexAsset[];
|
||||
dynamic_stickers: PublicComplexAsset[];
|
||||
font_panel_items: PublicComplexAsset[];
|
||||
static_stickers: StaticStickerCatalogItem[];
|
||||
text_templates: PublicComplexAsset[];
|
||||
};
|
||||
counts: {
|
||||
color_cards: 4;
|
||||
dynamic_stickers: 10;
|
||||
font_panel_items: 11;
|
||||
static_parts: 25;
|
||||
static_stickers: 1407;
|
||||
text_templates: 32;
|
||||
};
|
||||
release_tier: "alpha_whitelist";
|
||||
release_version: string;
|
||||
schema_version: "P0AAssetManifest/v1";
|
||||
source_versions: {
|
||||
complex: string;
|
||||
static_stickers: string;
|
||||
};
|
||||
static_part_counts: Record<string, number>;
|
||||
}
|
||||
|
||||
const expectedFamilyCounts: Readonly<Record<RegisteredComplexFamily, number>> = {
|
||||
color_card: 16,
|
||||
font_panel: 86,
|
||||
interactive_sticker: 35,
|
||||
text_template: 332,
|
||||
};
|
||||
|
||||
function orderedItems(items: readonly RegisteredComplexAsset[], ids: readonly string[], family: RegisteredComplexFamily) {
|
||||
const byId = new Map(items.filter((item) => item.family === family).map((item) => [item.canonical_id, item]));
|
||||
return ids.map((id) => {
|
||||
const item = byId.get(id);
|
||||
if (!item) throw new Error(`P0-A allowlist item is not registered: ${id}`);
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
function publicItem(item: RegisteredComplexAsset): PublicComplexAsset {
|
||||
return {
|
||||
...item,
|
||||
release_status: "enabled",
|
||||
release_tier: "alpha_whitelist",
|
||||
validation_status: "passed",
|
||||
};
|
||||
}
|
||||
|
||||
function validateFullRegistry(manifest: ComplexRegistryManifest) {
|
||||
if (manifest.schema_version !== "asset-release-compiler/v1") throw new Error("complex registry schema version is unsupported");
|
||||
const ids = new Set<string>();
|
||||
for (const item of manifest.items) {
|
||||
if (ids.has(item.canonical_id)) throw new Error(`duplicate registered asset ${item.canonical_id}`);
|
||||
ids.add(item.canonical_id);
|
||||
if (item.release_tier !== "full_p0") throw new Error(`${item.canonical_id} must remain in full_p0 before public projection`);
|
||||
if (item.release_status === "enabled") throw new Error(`full_p0 item is enabled before atomic full release: ${item.canonical_id}`);
|
||||
}
|
||||
for (const [family, expected] of Object.entries(expectedFamilyCounts) as Array<[RegisteredComplexFamily, number]>) {
|
||||
const actual = manifest.items.filter((item) => item.family === family).length;
|
||||
if (actual !== expected) throw new Error(`${family} count mismatch: expected ${expected}, received ${actual}`);
|
||||
}
|
||||
}
|
||||
|
||||
function validateStaticCatalog(catalog: StaticStickerCatalog) {
|
||||
if (catalog.count !== 1_407 || catalog.items.length !== 1_407) throw new Error("static sticker count mismatch: expected 1407");
|
||||
const parts = new Set(catalog.items.map((item) => item.part));
|
||||
if (parts.size !== 25 || [...parts].some((part) => part < 1 || part > 25)) throw new Error("static sticker parts must be exactly part1-part25");
|
||||
if (Object.keys(catalog.part_counts).length !== 25) throw new Error("static sticker part counts must contain 25 parts");
|
||||
}
|
||||
|
||||
function fontIdsForTemplates(templates: readonly RegisteredComplexAsset[]) {
|
||||
const referenced = new Set<string>();
|
||||
for (const template of templates) {
|
||||
for (const fontId of template.font_panel_references ?? []) referenced.add(fontId);
|
||||
}
|
||||
referenced.add("FONT081");
|
||||
return [...referenced].sort((left, right) => Number(left.slice(4)) - Number(right.slice(4)));
|
||||
}
|
||||
|
||||
export function createP0aPublicManifest(input: {
|
||||
complexManifest: ComplexRegistryManifest;
|
||||
staticCatalog: StaticStickerCatalog;
|
||||
}): P0aPublicManifest {
|
||||
validateFullRegistry(input.complexManifest);
|
||||
validateStaticCatalog(input.staticCatalog);
|
||||
|
||||
const textTemplates = orderedItems(input.complexManifest.items, P0A_TEXT_TEMPLATE_IDS, "text_template");
|
||||
const derivedFontIds = fontIdsForTemplates(textTemplates);
|
||||
if (JSON.stringify(derivedFontIds) !== JSON.stringify(P0A_REQUIRED_FONT_PANEL_IDS)) {
|
||||
throw new Error(`P0-A referenced font panel mismatch: received ${derivedFontIds.join(",")}`);
|
||||
}
|
||||
const fontPanelItems = orderedItems(input.complexManifest.items, derivedFontIds, "font_panel");
|
||||
const colorCards = orderedItems(input.complexManifest.items, P0A_COLOR_CARD_IDS, "color_card");
|
||||
const dynamicStickers = orderedItems(input.complexManifest.items, P0A_DYNAMIC_STICKER_IDS, "interactive_sticker");
|
||||
|
||||
return {
|
||||
assets: {
|
||||
color_cards: colorCards.map(publicItem),
|
||||
dynamic_stickers: dynamicStickers.map(publicItem),
|
||||
font_panel_items: fontPanelItems.map(publicItem),
|
||||
static_stickers: input.staticCatalog.items.map((item) => ({ ...item })),
|
||||
text_templates: textTemplates.map(publicItem),
|
||||
},
|
||||
counts: {
|
||||
color_cards: 4,
|
||||
dynamic_stickers: 10,
|
||||
font_panel_items: 11,
|
||||
static_parts: 25,
|
||||
static_stickers: 1_407,
|
||||
text_templates: 32,
|
||||
},
|
||||
release_tier: "alpha_whitelist",
|
||||
release_version: input.complexManifest.release_version,
|
||||
schema_version: "P0AAssetManifest/v1",
|
||||
source_versions: {
|
||||
complex: input.complexManifest.release_version,
|
||||
static_stickers: input.staticCatalog.release_version,
|
||||
},
|
||||
static_part_counts: { ...input.staticCatalog.part_counts },
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"lib": ["ES2024"],
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Reference in New Issue
Block a user