From 2c36925d0b6372cb32e5ddd9b0cf1b4b27cd6866 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Sun, 19 Apr 2026 17:24:28 -0600 Subject: [PATCH] feat(engine): custom modifier descriptor types T3 Wave 1 (T3). Defines the user-authored CustomModifierDescriptor shape that Wave 3 (validator, Zod schema, library, apply) and Wave 4 (server registration, editor UI) build on. - CustomModifierId: branded string (mirrors asEntityId), with the asCustomModifierId() trust-boundary helper. - CustomModifierDescriptor: - type: 'data' discriminator (T4 will add 'scripted' alongside). - id, name (1-40), description (0-200), version: 1 literal. - primitives: readonly EffectPrimitiveNode[] (re-exported from primitives/types so consumers have one import). - targetAttrs: readonly ChessAttrKey[] for editor conflict surfacing. - uiForm: 'primitive-composer' literal (routes editing to the custom-modifier composer UI in T25). - source: 'custom' for library typing. - Optional author, createdAt (auto-populated by library save). Persistence, validation, Zod schema, and apply() arrive in Wave 3. --- .../modifier-profiles-t3/learnings.md | 22 ++++++++ packages/chess/src/modifiers/custom/index.ts | 3 ++ .../chess/src/modifiers/custom/types.test.ts | 52 +++++++++++++++++++ packages/chess/src/modifiers/custom/types.ts | 52 +++++++++++++++++++ 4 files changed, 129 insertions(+) create mode 100644 .sisyphus/notepads/modifier-profiles-t3/learnings.md create mode 100644 packages/chess/src/modifiers/custom/index.ts create mode 100644 packages/chess/src/modifiers/custom/types.test.ts create mode 100644 packages/chess/src/modifiers/custom/types.ts diff --git a/.sisyphus/notepads/modifier-profiles-t3/learnings.md b/.sisyphus/notepads/modifier-profiles-t3/learnings.md new file mode 100644 index 0000000..1b4e80f --- /dev/null +++ b/.sisyphus/notepads/modifier-profiles-t3/learnings.md @@ -0,0 +1,22 @@ +# Modifier Profiles T3 — Learnings + +## [2026-04-19 17:14] Task: T3 + +- Added `packages/chess/src/modifiers/custom/types.ts` with branded `CustomModifierId` and `asCustomModifierId` helper that mirrors `asEntityId` trust-boundary wording/style from `packages/rete/src/schema.ts`. +- Added `CustomModifierDescriptor` with literal discriminators (`type: "data"`, `version: 1`, `uiForm: "primitive-composer"`, `source: "custom"`) and a forward-design JSDoc note for future `"scripted"` descriptors in T4. +- Divergence from ideal import shape: `EffectPrimitiveNode` is a local fallback interface in `custom/types.ts` because `packages/chess/src/modifiers/primitives/types.ts` is not yet committed in this branch state. Included TODO to swap to `../primitives/types.js` import immediately when T2 lands. +- Added `packages/chess/src/modifiers/custom/index.ts` as a focused barrel with explicit Wave 3 scope boundary comment. +- Added `packages/chess/src/modifiers/custom/types.test.ts` with four scenarios: branded helper runtime/type round-trip, descriptor structure assignment, readonly `targetAttrs` typing, readonly `primitives` typing. + +## [2026-04-19 17:13] Task: T2 + +- Added `packages/chess/src/modifiers/primitives/types.ts` with T3 primitive core contracts: + - `PrimitiveKind` union with exactly 15 ADR-2 primitive ids. + - `EffectPrimitive` descriptor shape (`paramsSchema: ZodType`, `apply(ctx, params): void`, optional `maxDepth`, optional `childPrimitives`). + - `EffectPrimitiveNode` runtime node shape (`kind`, `params`). + - `PrimitiveApplyContext` (`engine`, `session`, `pieceId`, `depth`, `descriptor`). + - Forward-declared `CustomModifierDescriptor` placeholder interface to avoid circular dependency with future `../custom/types.ts`. +- Added `packages/chess/src/modifiers/primitives/registry.ts` + singleton export. + - Mirrored `MODIFIER_REGISTRY` class shape exactly: private `Map`, duplicate guard throw, `register/get/list/has`, generic register call-site support. +- Added `packages/chess/src/modifiers/primitives/index.ts` barrel with explicit Wave-2 side-effect-registration stub comment. +- Added `packages/chess/src/modifiers/primitives/registry.test.ts` with 6 scenarios: round-trip get, duplicate throw, list order stability, `has()` accuracy, unknown kind miss, and generic type preservation at register call site. diff --git a/packages/chess/src/modifiers/custom/index.ts b/packages/chess/src/modifiers/custom/index.ts new file mode 100644 index 0000000..5379edd --- /dev/null +++ b/packages/chess/src/modifiers/custom/index.ts @@ -0,0 +1,3 @@ +export * from "./types.js"; + +// Persistence, validation, Zod schema, and apply are added by Wave 3 (T19-T22). diff --git a/packages/chess/src/modifiers/custom/types.test.ts b/packages/chess/src/modifiers/custom/types.test.ts new file mode 100644 index 0000000..d29cfac --- /dev/null +++ b/packages/chess/src/modifiers/custom/types.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, expectTypeOf, it } from "vitest"; +import type { ChessAttrKey } from "../../schema.js"; +import { + asCustomModifierId, + type EffectPrimitiveNode, + type CustomModifierDescriptor, + type CustomModifierId, +} from "./types.js"; + +describe("custom modifier descriptor types", () => { + it("asCustomModifierId round-trips the same runtime string", () => { + const raw = "custom:hp-plus"; + const id = asCustomModifierId(raw); + expect(id).toBe(raw); + expectTypeOf(id).toEqualTypeOf(); + }); + + it("accepts the expected descriptor structure", () => { + const descriptor: CustomModifierDescriptor = { + type: "data", + id: asCustomModifierId("custom:rook-boost"), + name: "Rook boost", + description: "Adds a small directional bonus for rooks.", + version: 1, + primitives: [ + { kind: "add-to-attribute", params: { attr: "RangeBonus", delta: 1 } }, + ], + targetAttrs: ["RangeBonus"], + uiForm: "primitive-composer", + source: "custom", + author: "Test Author", + createdAt: Date.now(), + }; + + expect(descriptor.type).toBe("data"); + expect(descriptor.version).toBe(1); + expect(descriptor.source).toBe("custom"); + expect(descriptor.uiForm).toBe("primitive-composer"); + }); + + it("exposes targetAttrs as a readonly ChessAttrKey array type", () => { + expectTypeOf().toEqualTypeOf< + readonly ChessAttrKey[] + >(); + }); + + it("exposes primitives as a readonly array type", () => { + expectTypeOf().toEqualTypeOf< + readonly EffectPrimitiveNode[] + >(); + }); +}); diff --git a/packages/chess/src/modifiers/custom/types.ts b/packages/chess/src/modifiers/custom/types.ts new file mode 100644 index 0000000..52cb325 --- /dev/null +++ b/packages/chess/src/modifiers/custom/types.ts @@ -0,0 +1,52 @@ +import type { ChessAttrKey } from "../../schema.js"; +import type { EffectPrimitiveNode } from "../primitives/types.js"; + +export type { EffectPrimitiveNode }; + +/** + * Compile-time branded identifier for user-authored custom modifiers. + * + * At runtime this is a plain `string`. The brand exists only in the type + * system to prevent accidental interchange with other string ids. + */ +export type CustomModifierId = string & { readonly __brand: "CustomModifierId" }; + +/** + * Coerce a raw string to the branded CustomModifierId type. Use ONLY at trust + * boundaries where you've already established that `s` is the canonical + * custom-modifier identifier (e.g. persisted library payloads or validated + * user input). Prefer passing `CustomModifierId` through end-to-end when + * possible; this helper is the single legitimate cast site. + */ +export const asCustomModifierId = (s: string): CustomModifierId => s as CustomModifierId; + +/** + * User-authored custom modifier descriptor. + * + * `type` is the forward-compatibility discriminator for the custom-modifier + * family. T3 ships only data descriptors (`"data"`); T4 introduces + * `"scripted"` descriptors while preserving the shared trunk fields + * (`id`/`name`/`description`/`version`). + */ +export interface CustomModifierDescriptor { + readonly type: "data"; + readonly id: CustomModifierId; + /** Human-readable title (1-40 chars, validated in Wave 3). */ + readonly name: string; + /** Optional explanatory text (0-200 chars, validated in Wave 3). */ + readonly description: string; + /** Descriptor schema version; v2+ must use a new id. */ + readonly version: 1; + /** Primitive composition tree/list for this custom modifier. */ + readonly primitives: readonly EffectPrimitiveNode[]; + /** Attr keys this modifier reads/writes for UI conflict surfacing. */ + readonly targetAttrs: readonly ChessAttrKey[]; + /** Routes editing to the custom-modifier primitive composer UI. */ + readonly uiForm: "primitive-composer"; + /** Distinguishes this descriptor family from premade built-ins. */ + readonly source: "custom"; + /** Optional cosmetic attribution string; never server-validated. */ + readonly author?: string; + /** Optional unix timestamp auto-populated by library persistence. */ + readonly createdAt?: number; +}