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