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.
This commit is contained in:
parent
2e655a0c1a
commit
2c36925d0b
4 changed files with 129 additions and 0 deletions
22
.sisyphus/notepads/modifier-profiles-t3/learnings.md
Normal file
22
.sisyphus/notepads/modifier-profiles-t3/learnings.md
Normal file
|
|
@ -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<Params>` descriptor shape (`paramsSchema: ZodType<Params>`, `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.
|
||||||
3
packages/chess/src/modifiers/custom/index.ts
Normal file
3
packages/chess/src/modifiers/custom/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * from "./types.js";
|
||||||
|
|
||||||
|
// Persistence, validation, Zod schema, and apply are added by Wave 3 (T19-T22).
|
||||||
52
packages/chess/src/modifiers/custom/types.test.ts
Normal file
52
packages/chess/src/modifiers/custom/types.test.ts
Normal file
|
|
@ -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<CustomModifierId>();
|
||||||
|
});
|
||||||
|
|
||||||
|
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<CustomModifierDescriptor["targetAttrs"]>().toEqualTypeOf<
|
||||||
|
readonly ChessAttrKey[]
|
||||||
|
>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("exposes primitives as a readonly array type", () => {
|
||||||
|
expectTypeOf<CustomModifierDescriptor["primitives"]>().toEqualTypeOf<
|
||||||
|
readonly EffectPrimitiveNode[]
|
||||||
|
>();
|
||||||
|
});
|
||||||
|
});
|
||||||
52
packages/chess/src/modifiers/custom/types.ts
Normal file
52
packages/chess/src/modifiers/custom/types.ts
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue