From d9c14ddec96ebddf8e38fffcc5c5279395a45b97 Mon Sep 17 00:00:00 2001 From: Hide_D Date: Sat, 27 Dec 2025 02:34:23 +0000 Subject: [PATCH] =?UTF-8?q?=EB=AC=B8=EC=84=9C=20=EC=B6=94=EA=B0=80:=20?= =?UTF-8?q?=EC=A0=9C=EC=95=BD=20=EC=A1=B0=EA=B1=B4=20=ED=8F=89=EA=B0=80=20?= =?UTF-8?q?=EA=B3=84=EC=95=BD=20=EC=A0=95=EC=9D=98=20=EB=B0=8F=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EB=82=B4=EC=9A=A9=20=EB=AC=B8=EC=84=9C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../architecture/legacy-engine-constraints.md | 111 +----------------- docs/architecture/rewrite-constraints.md | 110 +++++++++++++++++ docs/architecture/rewrite-plan.md | 5 + 3 files changed, 118 insertions(+), 108 deletions(-) create mode 100644 docs/architecture/rewrite-constraints.md diff --git a/docs/architecture/legacy-engine-constraints.md b/docs/architecture/legacy-engine-constraints.md index 365924f..418253f 100644 --- a/docs/architecture/legacy-engine-constraints.md +++ b/docs/architecture/legacy-engine-constraints.md @@ -48,115 +48,10 @@ Constraints are organized by domain: Each constraint sets a failure reason string used by UI and logs. -## Rewrite Constraint Contract (Draft) +## Rewrite References -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. +Rewrite constraint contracts and runtime split details live in +`docs/architecture/rewrite-constraints.md`. ## Open Questions / Follow-ups diff --git a/docs/architecture/rewrite-constraints.md b/docs/architecture/rewrite-constraints.md new file mode 100644 index 0000000..b5f35dc --- /dev/null +++ b/docs/architecture/rewrite-constraints.md @@ -0,0 +1,110 @@ +# 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) + +```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. diff --git a/docs/architecture/rewrite-plan.md b/docs/architecture/rewrite-plan.md index 7c0c7b1..e3a7615 100644 --- a/docs/architecture/rewrite-plan.md +++ b/docs/architecture/rewrite-plan.md @@ -23,6 +23,11 @@ Vue 3 frontends. - Data: PostgreSQL, Redis sessions - Testing: Vitest +## Constraint Evaluation Contract + +The shared constraint contract (daemon vs API precheck split) is documented in +`docs/architecture/rewrite-constraints.md`. + ## Legacy Data Migration - Legacy data is for migration only; the rewrite runtime does not depend on it.