diff --git a/docs/architecture/legacy-engine-constraints.md b/docs/architecture/legacy-engine-constraints.md index 1bfd409..365924f 100644 --- a/docs/architecture/legacy-engine-constraints.md +++ b/docs/architecture/legacy-engine-constraints.md @@ -47,6 +47,117 @@ Constraints are organized by domain: Each constraint sets a failure reason string used by UI and logs. + +## Rewrite Constraint Contract (Draft) + +This section proposes a shared contract for the rewrite so both the turn daemon +(in-memory) and API server (DB-backed) can evaluate constraints consistently. + +### Goals + +- Single source of truth for constraint logic. +- Support full evaluation in daemon and precheck in API. +- Explicit data requirements for batching and caching. +- Deterministic reasons for deny vs unknown outcomes. + +### Types (TypeScript sketch) + +```ts +export type ConstraintResult = + | { kind: 'allow' } + | { kind: 'deny'; reason: string; code?: string } + | { kind: 'unknown'; missing: RequirementKey[] }; + +export type RequirementKey = + | { kind: 'general'; id: number } + | { kind: 'city'; id: number } + | { kind: 'nation'; id: number } + | { kind: 'destGeneral'; id: number } + | { kind: 'destCity'; id: number } + | { kind: 'destNation'; id: number } + | { kind: 'arg'; key: string } + | { kind: 'env'; key: string }; + +export interface ConstraintContext { + actorId: number; + cityId?: number; + nationId?: number; + destGeneralId?: number; + destCityId?: number; + destNationId?: number; + args: Record; + env: Record; + mode: 'full' | 'precheck'; +} + +export interface StateView { + has(req: RequirementKey): boolean; + get(req: RequirementKey): unknown | null; +} + +export interface Constraint { + name: string; + requires(ctx: ConstraintContext): RequirementKey[]; + test(ctx: ConstraintContext, view: StateView): ConstraintResult; +} +``` + +### Evaluation Flow + +- `ConstraintPlanner` collects requirements across constraints. +- `StateView` loads those requirements (daemon: in-memory, API: DB). +- `test()` returns: + - `allow` if constraint passes. + - `deny` with a stable reason/code for UI. + - `unknown` if required data is missing and `mode === 'precheck'`. + +```ts +function evaluateConstraints( + constraints: Constraint[], + ctx: ConstraintContext, + view: StateView, +): ConstraintResult { + for (const constraint of constraints) { + const missing = constraint.requires(ctx).filter((req) => !view.has(req)); + if (missing.length && ctx.mode === 'precheck') { + return { kind: 'unknown', missing }; + } + const result = constraint.test(ctx, view); + if (result.kind !== 'allow') { + return result; + } + } + return { kind: 'allow' }; +} +``` + +### StateView Selection Boundary + +The split between in-memory and DB-backed evaluation happens outside the +constraint logic. A factory or loader chooses the `StateView` implementation +based on the execution environment: + +- Turn daemon -> `InMemoryStateView` with a full in-memory snapshot. +- API server -> `DbStateView` (or `ProjectedStateView`) that fetches only the + required fields from the DB or precomputed projections. + +This keeps constraints pure and deterministic, while the infrastructure layer +decides how to satisfy `requires()` in each runtime. + +### Mapping from Legacy Flags + +- `REQ_GENERAL` -> `{ kind: 'general', id: actorId }` +- `REQ_CITY` -> `{ kind: 'city', id: cityId }` +- `REQ_NATION` -> `{ kind: 'nation', id: nationId }` +- `REQ_DEST_*` -> respective `dest` key +- `REQ_ARG` -> `{ kind: 'arg', key: }` +- `env` dependencies (for example `turnterm`, `year`) -> `{ kind: 'env', key: 'turnterm' }` + +### Data Projection Suggestion + +- API prechecks can rely on a small read model (for example `general_summary`) + updated by the turn daemon; `StateView` selects the source per requirement. + ## Open Questions / Follow-ups - Some constraints rely on `env` values (`turnterm`, `year`, etc.); document diff --git a/docs/architecture/todo.md b/docs/architecture/todo.md index aa9ac3e..3eedfa1 100644 --- a/docs/architecture/todo.md +++ b/docs/architecture/todo.md @@ -8,6 +8,7 @@ Move items into the main docs once they are finalized. - Turn daemon scheduling details and preemption rules - Turn daemon vs API server priority policy under load - In-memory state lifecycle and DBMS flush checkpoints +- [AI suggestion] Define a constraint evaluation contract with explicit data requirements and a tri-state result (allow/deny/unknown) to support DB-backed prechecks vs in-memory full checks. - Recovery behavior after partial flush or crash - Observability: metrics, logs, and alerts for turn processing - [AI suggestion] Define a stable in-memory AI state contract (snapshot + delta invalidation rules) aligned with `GeneralAI` inputs.