feat(spa-types): add SnapshotEntry + EmbedBundle + embedBundle/hashes client

This commit is contained in:
DumiFlex
2026-04-28 14:26:52 +03:00
parent 323dd55e63
commit bfe98f43b8
10 changed files with 174 additions and 8 deletions
@@ -30,6 +30,7 @@ function makeWildcardRow(id: string, name: string, opts: {
} = {}): ModuleRow {
return {
id,
uuid: "aabbccdd",
name,
type: "wildcard",
description: "",
@@ -40,6 +41,7 @@ function makeWildcardRow(id: string, name: string, opts: {
options: (opts.values ?? []).map((v, i) => ({ id: `o${i}`, value: v, weight: 1 })),
sub_categories: opts.subs ?? [],
},
payload_hash: "0".repeat(64),
version: 1,
created_at: "",
updated_at: "",
@@ -29,6 +29,7 @@ const apiAny = api as unknown as {
function mkModule(over: Partial<ModuleRow>): ModuleRow {
return {
id: "wc_aaaa1111",
uuid: "aabbccdd",
type: "wildcard",
name: "Sample",
description: "",
@@ -36,6 +37,7 @@ function mkModule(over: Partial<ModuleRow>): ModuleRow {
tags: [],
is_favorite: false,
payload: { options: [] },
payload_hash: "0".repeat(64),
version: 1,
created_at: "2024-01-01T00:00:00Z",
updated_at: "2024-01-01T00:00:00Z",
@@ -6,6 +6,7 @@ import type { ModuleRow, ModuleType, PipelineStep } from "../api/types";
function makeModule(over: Partial<ModuleRow>): ModuleRow {
return {
id: "m_a",
uuid: "aabbccdd",
type: "wildcard",
name: "alpha",
description: "",
@@ -13,6 +14,7 @@ function makeModule(over: Partial<ModuleRow>): ModuleRow {
tags: [],
is_favorite: false,
payload: {},
payload_hash: "0".repeat(64),
version: 1,
created_at: "",
updated_at: "",
+14 -7
View File
@@ -6,7 +6,7 @@ import type { ModuleRow } from "../api/types";
vi.mock("../api/client", () => {
const MOCK_MODULES: ModuleRow[] = [
{
id: "wc_a", type: "wildcard", name: "Hair Color",
id: "wc_a", uuid: "aabbccdd", type: "wildcard", name: "Hair Color",
description: "", category_id: null, tags: [], is_favorite: false,
payload: {
options: [
@@ -16,10 +16,11 @@ vi.mock("../api/client", () => {
sub_categories: ["warm", "cool"],
var_binding: "hair_color",
},
payload_hash: "0".repeat(64),
version: 1, created_at: "", updated_at: "",
},
{
id: "wc_o", type: "wildcard", name: "Outfit",
id: "wc_o", uuid: "bbbbbbbb", type: "wildcard", name: "Outfit",
description: "", category_id: null, tags: [], is_favorite: false,
payload: {
options: [
@@ -27,26 +28,29 @@ vi.mock("../api/client", () => {
],
sub_categories: [],
},
payload_hash: "0".repeat(64),
version: 1, created_at: "", updated_at: "",
},
{
id: "fv_a", type: "fixed_values", name: "Subject Profile",
id: "fv_a", uuid: "cccccccc", type: "fixed_values", name: "Subject Profile",
description: "", category_id: null, tags: [], is_favorite: false,
payload: { values: [{ var: "name", value: "Mira" }, { var: "age", value: "29" }] },
payload_hash: "0".repeat(64),
version: 1, created_at: "", updated_at: "",
},
{
id: "cb_a", type: "combine", name: "Subject Phrase",
id: "cb_a", uuid: "dddddddd", type: "combine", name: "Subject Phrase",
description: "", category_id: null, tags: [], is_favorite: false,
payload: {
template: "$name with $hair_color hair",
output_var: "subject_phrase",
input_vars: ["name", "hair_color"],
},
payload_hash: "0".repeat(64),
version: 1, created_at: "", updated_at: "",
},
{
id: "dv_a", type: "derivation", name: "Always Append",
id: "dv_a", uuid: "eeeeeeee", type: "derivation", name: "Always Append",
description: "", category_id: null, tags: [], is_favorite: false,
payload: {
rules: [{
@@ -57,10 +61,11 @@ vi.mock("../api/client", () => {
}],
}],
},
payload_hash: "0".repeat(64),
version: 1, created_at: "", updated_at: "",
},
{
id: "cn_a", type: "constraint", name: "Hair × Outfit",
id: "cn_a", uuid: "ffffffff", type: "constraint", name: "Hair × Outfit",
description: "", category_id: null, tags: [], is_favorite: false,
payload: {
source_wildcard_id: "wc_a",
@@ -68,15 +73,17 @@ vi.mock("../api/client", () => {
matrix: { warm: {} },
exceptions: [],
},
payload_hash: "0".repeat(64),
version: 1, created_at: "", updated_at: "",
},
{
id: "pl_a", type: "pipeline", name: "Quick Portrait",
id: "pl_a", uuid: "11111111", type: "pipeline", name: "Quick Portrait",
description: "", category_id: null, tags: [], is_favorite: false,
payload: { steps: [
{ id: "s1", module_id: "fv_a", enabled: true },
{ id: "s2", module_id: "wc_a", enabled: true },
] },
payload_hash: "0".repeat(64),
version: 1, created_at: "", updated_at: "",
},
];
+83
View File
@@ -0,0 +1,83 @@
/**
* Spec §4.3 — TS twin of canonical Python SnapshotEntry shape. Pinned
* here so a future refactor on either side surfaces a type error before
* runtime drift can happen.
*/
import { describe, expect, it } from "vitest";
import type {
ModuleRow,
SnapshotEntry,
WalkOverflow,
EmbedBundle,
} from "../api/types";
import { api } from "../api/client";
describe("API types", () => {
it("SnapshotEntry has the seven canonical fields", () => {
// Compile-time check: this object literal must satisfy the type.
const entry: SnapshotEntry = {
snapshot_version: 1,
uuid: "aabbccdd",
type: "wildcard",
name: "color",
payload: { options: [] },
payload_hash: "0".repeat(64),
source: { kind: "user" },
};
expect(entry.snapshot_version).toBe(1);
});
it("SnapshotEntry source supports kind=dep with parent_uuids", () => {
const entry: SnapshotEntry = {
snapshot_version: 1, uuid: "a1b2c3d4", type: "wildcard", name: "x",
payload: {}, payload_hash: "h".repeat(64),
source: { kind: "dep", parent_uuids: ["aabbccdd"] },
};
expect((entry.source as { kind: "dep"; parent_uuids: string[] }).parent_uuids)
.toEqual(["aabbccdd"]);
});
it("WalkOverflow shape", () => {
const overflows: WalkOverflow[] = [
{ uuid: "x", reason: "max_depth" },
{ uuid: "y", reason: "cycle_detected" },
{ uuid: "z", reason: "missing_target" },
];
expect(overflows).toHaveLength(3);
});
it("EmbedBundle has flat envelope shape", () => {
const bundle: EmbedBundle = {
modules: [{ type: "wildcard" }],
snapshots: {},
pickOrder: ["aabbccdd"],
walkOverflow: [],
};
expect(bundle.pickOrder).toEqual(["aabbccdd"]);
});
it("ModuleRow includes uuid and payload_hash", () => {
const row: ModuleRow = {
id: "wc_x_aabbccdd",
uuid: "aabbccdd",
type: "wildcard",
name: "x",
description: "",
category_id: null,
tags: [],
is_favorite: false,
payload: {},
payload_hash: "0".repeat(64),
version: 1,
created_at: "2026-04-28T00:00:00.000000Z",
updated_at: "2026-04-28T00:00:00.000000Z",
};
expect(row.uuid).toBe("aabbccdd");
expect(row.payload_hash).toBeDefined();
});
it("client exposes embedBundle and hashes methods", () => {
expect(typeof api.modules.embedBundle).toBe("function");
expect(typeof api.modules.hashes).toBe("function");
});
});
@@ -19,6 +19,7 @@ import type { CategoryRow, ModuleRow } from "../api/types";
function mkModule(over: Partial<ModuleRow>): ModuleRow {
return {
id: "wc_aaaa1111",
uuid: "aabbccdd",
type: "wildcard",
name: "Sample",
description: "",
@@ -26,6 +27,7 @@ function mkModule(over: Partial<ModuleRow>): ModuleRow {
tags: [],
is_favorite: false,
payload: {},
payload_hash: "0".repeat(64),
version: 1,
created_at: "2024-01-01T00:00:00Z",
updated_at: "2024-01-01T00:00:00Z",
+4 -1
View File
@@ -24,6 +24,7 @@ function makeWildcard(
): ModuleRow {
return {
id: extras.id ?? `wc_${name.toLowerCase()}`,
uuid: "aabbccdd",
type: "wildcard",
name,
description: "",
@@ -40,6 +41,7 @@ function makeWildcard(
sub_categories: extras.sub_categories ?? [],
var_binding: extras.var_binding,
},
payload_hash: "0".repeat(64),
version: 1,
created_at: "",
updated_at: "",
@@ -255,9 +257,10 @@ describe("runStep", () => {
it("runs a fixed_values step writing every binding into ctx", () => {
const f: ModuleRow = {
id: "fv1", type: "fixed_values", name: "Pack",
id: "fv1", uuid: "aabbccdd", type: "fixed_values", name: "Pack",
description: "", category_id: null, tags: [], is_favorite: false,
payload: { values: [{ var: "city", value: "Tokyo" }, { name: "$ward", value: "Shibuya" }] },
payload_hash: "0".repeat(64),
version: 1, created_at: "", updated_at: "",
};
const step: PipelineStep = { id: "s1", module_id: f.id, enabled: true };
@@ -23,6 +23,7 @@ function wc(
}
return {
id: partial.id ?? `wc_${partial.name}`,
uuid: partial.uuid ?? "aabbccdd",
type: "wildcard",
name: partial.name,
description: partial.description ?? "",
@@ -30,6 +31,7 @@ function wc(
tags: partial.tags ?? [],
is_favorite: partial.is_favorite ?? false,
payload,
payload_hash: partial.payload_hash ?? "0".repeat(64),
version: partial.version ?? 1,
created_at: partial.created_at ?? "",
updated_at: partial.updated_at ?? "",
+11
View File
@@ -1,5 +1,6 @@
import type {
CategoryCreateInput, CategoryRow,
EmbedBundle,
ImportBundle, ImportResult,
MatchRequest, MatchResponse,
ModuleCreateInput, ModuleListResponse, ModuleRow, ModuleUpdateInput,
@@ -91,6 +92,16 @@ export const api = {
method: "POST", body: JSON.stringify(body),
});
},
embedBundle(uuids: string[]) {
return request<EmbedBundle>("/wp/api/modules/embed-bundle", {
method: "POST", body: JSON.stringify({ uuids }),
});
},
hashes() {
return request<{ hashes: Record<string, string> }>(
"/wp/api/modules/hashes", { method: "GET" },
);
},
},
categories: {
list() {
+52
View File
@@ -107,6 +107,7 @@ export interface PipelinePayload {
export interface ModuleRow {
id: string;
uuid: string; // spec §4.2
type: ModuleType;
name: string;
description: string;
@@ -114,6 +115,7 @@ export interface ModuleRow {
tags: string[];
is_favorite: boolean;
payload: Record<string, unknown>;
payload_hash: string; // spec §4.2
version: number;
created_at: string;
updated_at: string;
@@ -224,3 +226,53 @@ export interface ImportResult {
categories_imported: number;
skipped: string[];
}
/**
* Spec §2.4 — canonical snapshot entry. Mirrors the Python
* `engine.modules.snapshot.SnapshotEntry` TypedDict. Stored:
* - in `__wp_catalog__` at runtime (memory only)
* - in WP_Context workflow JSON (persisted, decoupled from DB)
*
* `payload_hash` covers `payload` only (not `name` / `tags` / etc.) so a
* rename does not flip drift state.
*/
export interface SnapshotEntry {
snapshot_version: 1;
uuid: string; // 8 hex chars
type: ModuleType; // wildcards-only in catalog (spec §2.7)
name: string;
payload: Record<string, unknown>;
payload_hash: string; // SHA-256 hex, 64 chars
source:
| { kind: "user" }
| { kind: "dep"; parent_uuids: string[] };
}
/**
* Walker-recorded anomaly. Spec §2.8 — extensible discriminator. New
* reasons can be added without breaking existing consumers; they
* default-handle unknown variants.
*/
export interface WalkOverflow {
uuid: string;
reason: "max_depth" | "cycle_detected" | "missing_target";
}
/**
* Server response shape from `POST /wp/api/modules/embed-bundle`.
* Pre-split so the SPA picker doesn't filter wildcards out of a mixed
* bundle. Spec §4.2.
*
* - `modules`: full payloads of explicit picks, in input order. The
* pipeline executes these.
* - `snapshots`: wildcard catalog (picks of type wildcard + transitive
* wildcards). Becomes `ctx['__wp_catalog__']` at run time.
* - `pickOrder`: uuid order for UI display.
* - `walkOverflow`: cycles / depth caps / missing targets the walker hit.
*/
export interface EmbedBundle {
modules: Record<string, unknown>[];
snapshots: Record<string, SnapshotEntry>;
pickOrder: string[];
walkOverflow: WalkOverflow[];
}