3.4 KiB
3.4 KiB
Rewrite Constraint Contract
This document defines a shared constraint 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)
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<string, unknown>;
env: Record<string, unknown>;
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
ConstraintPlannercollects requirements across constraints.StateViewloads those requirements (daemon: in-memory, API: DB).test()returns:allowif constraint passes.denywith a stable reason/code for UI.unknownif required data is missing andmode === 'precheck'.
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 ->
InMemoryStateViewwith a full in-memory snapshot. - API server ->
DbStateView(orProjectedStateView) 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_*-> respectivedestkeyREQ_ARG->{ kind: 'arg', key: <argName> }envdependencies (for exampleturnterm,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;StateViewselects the source per requirement.