diff --git a/A00_Common/design/design_page.ts b/A00_Common/design/design_page.ts index 63fc84c16..2a466c75f 100644 --- a/A00_Common/design/design_page.ts +++ b/A00_Common/design/design_page.ts @@ -32,6 +32,7 @@ import { saveSharedDesign, type DesignPageShared } from "./design_shared"; import { guardDesignLeave } from "./design_leave"; import { usePipeIds } from "./design_pipe_ids"; import { setRouteShiftOpener } from "../../B06_Section/B06_Section_UI_Cross_Center"; +import { reconcilePagePipeline } from "./design_pipeline_page"; import "./design_page.css"; export type { DesignPageShared } from "./design_shared"; @@ -244,6 +245,7 @@ export function renderDesignPage(root: HTMLElement): MountedPage { if (slot) { slot.show(); applyView(view); + void reconcilePagePipeline(projectId); // 들어올 때 입력(세션 · 가게) 대 묶음 맞춤(365-2) } else mountView(view, true); // 검증용 — 보기 전환에 든 시간(ms)과 횟수를 화면 밖에서 수치로 본다. const probe = window as unknown as { __designPage?: { view: string; switchMs: number[] } }; diff --git a/A00_Common/design/design_pipeline_cross.ts b/A00_Common/design/design_pipeline_cross.ts index 4ef809138..382120c65 100644 --- a/A00_Common/design/design_pipeline_cross.ts +++ b/A00_Common/design/design_pipeline_cross.ts @@ -8,9 +8,11 @@ * 넘기는 칸 = 서버 노선 저장(`CARRY_INPUT_KEYS`)과 같은 일곱(지반 유형 · 측구 종류 · 측구 선택 · 2단 경사 · * 암 경계 · 암 경사 · 소단) + 얹힌 시설 맥락(포장 · 물넘이 기울임 · 세월교 노면 하강). 단면 유형 · 측구 * 방향은 새 지형이 정함(`logic/B05_Profile_Pipeline_Detail.ts` `crossWithUphill`) · 노견 확장 · 소단 - * 깊이는 안 넘김. 값 = 상세에 얹힌 설계(세션 고침은 종단도 · 횡단 보기가 다시 셈하며 이미 얹음). + * 깊이는 안 넘김. 값 = 상세에 얹힌 설계 위에 세션 고침(측점 열쇠 칸 · 지반 유형 · 측구 · 2단 경사 · 암 경계 · + * 암 경사) — 노선 초안 동안 B06 고침은 저장 상세에 안 얹혀도 입력에 들어감(365-2). 부를 때마다 모음. * ========================================================================== */ +import { readState } from "../b_page_state"; import type { CrossSection, SectionDetailResponse } from "../../B06_Section/B06_Section_Api_Fetch"; import { effectiveStandardCross, @@ -71,11 +73,45 @@ function carried(section: CrossSection): Record { }; } -/** 저장 상세 + 저장 노선 측점(열쇠) → cross 입력 · 시설 제원 — 표준단면 · 상세가 없으면 null. */ +/** 세션 칸 하나를 측점 열쇠마다 — 옛 열쇠(누가거리 · `at:`)는 읽는 쪽이 옮긴 뒤에 들어옴(빼고 봄). */ +function sessionCells( + name: "crossdesign" | "rockb" | "cutslope", + projectId: string, + routeId: number, +): Array<[string, unknown]> { + const raw = readState>(name, projectId, routeId); + if (!raw || typeof raw !== "object") return []; + return Object.entries(raw).filter(([key]) => key.includes(":") && !key.startsWith("at:")); +} + +/** 세션 횡단 고침 → 측점 열쇠마다 넘기는 입력 조각(`carried` 와 같은 칸 이름). */ +function sessionCarried(projectId: string, routeId: number): CrossInput["byKey"] { + const out: CrossInput["byKey"] = {}; + for (const [key, value] of sessionCells("crossdesign", projectId, routeId)) { + const choice = (value ?? {}) as Record; + const entry = (out[key] ??= {}); + if (typeof choice.ground_type === "string") entry.groundType = choice.ground_type; + if (typeof choice.ditch_type === "string") entry.ditchType = choice.ditch_type; + if ("ditch_choice" in choice) + entry.ditchChoice = typeof choice.ditch_choice === "boolean" ? choice.ditch_choice : null; + if ("two_stage_slope" in choice) entry.twoStageSlope = Boolean(choice.two_stage_slope); + } + for (const [key, value] of sessionCells("rockb", projectId, routeId)) + if (finite(value) !== null) (out[key] ??= {}).rockBoundaryOffsetM = value; + for (const [key, value] of sessionCells("cutslope", projectId, routeId)) { + const ratio = finite(value); + if (ratio !== null) (out[key] ??= {}).cutSlopeRatio = ratio > 0 ? ratio : null; + } + return out; +} + +/** 저장 상세 + 저장 노선 측점(열쇠) + 세션 고침(`routeId` 칸) → cross 입력 · 시설 제원 — 표준단면 · + * 상세가 없으면 null. */ export function pipelineCrossSource( projectId: string, detail: SectionDetailResponse | null, savedStations: ReadonlyArray, + routeId: number | null = null, ): PipelineCrossSource | null { const standard = detail && effectiveStandardCross(projectId); if (!detail || !standard) return null; @@ -94,6 +130,9 @@ export function pipelineCrossSource( for (const name of SPEC_KEYS) if (section[name] !== undefined) own[name] = section[name]; specs.set(best.key, own as SectionSpecs); } + if (routeId !== null) + for (const [key, entry] of Object.entries(sessionCarried(projectId, routeId))) + byKey[key] = { ...byKey[key], ...entry }; return { // 암 경계 기본값 = 서버 config(화면이 문맥에서 기억) — 입력 없는 측점(초안 새 측점)도 서버처럼(349-0p) input: { standard, byKey, rockBoundaryDefaultM: readRockBoundaryDefault(projectId) }, diff --git a/A00_Common/design/design_pipeline_page.ts b/A00_Common/design/design_pipeline_page.ts index 49a4f9d9d..f072f1c3d 100644 --- a/A00_Common/design/design_pipeline_page.ts +++ b/A00_Common/design/design_pipeline_page.ts @@ -12,7 +12,10 @@ * * 입력 — 노선 = 편집기 다리(`RouteDraftBridge` · 캐시 칸 route-draft) · 계획고 = 종단도 편집 * (`pipelineGrade` · 노선 초안 동안은 alignment-draft 노선 `DRAFT_ROUTE_ID`) · 횡단 = 저장 상세의 측점 - * 입력(`design_pipeline_cross.ts`) · 관 = 가게 초안 → 저장 관 목록(좌표 · id) · 구조물 = 가게 초안 → 저장분. + * 입력 + 세션 고침(`design_pipeline_cross.ts` · 부를 때마다 모음 · 횡단 고침 뒤 `pipelineCross` · 365-2) · + * 관 = 가게 초안 → 저장 관 목록(좌표 · id) · 구조물 = 가게 초안 → 저장분. + * 고리(365-2) — 열 때 · 페이지 들어올 때(`reconcilePagePipeline`) 입력(세션 · 가게)을 묶음이 쥔 입력과 견줌 → + * 다르면 한 바퀴 → 끝난 뒤 다시 견줌 · 최대 `RECONCILE_ROUNDS` · 넘으면 `onUnstable`(「계산 불안정」 한 줄). * 출력 — 가게 stations · profile · footprint · generation · 노선 초안이 있으면 종단도가 새 첫 계획선 · * 옮긴 계획고 · 새 노선 상세(`pipelineDetail` — 지반 · 측점 · 횡단 면적 → 유토곡선)를 그림. * 첫 계산 = 열 때 한 번(저장 노선 · 발자국 · 저장 안 한 노선 초안이 있으면 그 노선). @@ -22,15 +25,18 @@ * ========================================================================== */ import { createDesignPipeline, publishToStore, type DesignPipeline } from "./logic/design_pipeline"; +import { SOURCE_INPUT } from "./logic/design_pipeline_types"; export { pipelineWork } from "./logic/design_pipeline"; import { pipelineCrossSource, type PipelineCrossSource } from "./design_pipeline_cross"; import { createStationsStage } from "./logic/design_pipeline_stations"; import type { + ChangeSource, FollowPatch, PipelineInputs, PipelineOutputs, PipelineRunRecord, PipelineStation, + RangePart, RouteDraft, StageFn, Vertex, @@ -102,6 +108,8 @@ import type { /** 노선 초안 동안의 계획고 초안 칸 — alignment-draft 의 노선 0(노선 번호는 [저장] 때 남). */ export const DRAFT_ROUTE_ID = 0; +/** 입력 ↔ 묶음 입력 맞춤 상한(바퀴) — 넘으면 「계산 불안정」(365-2). */ +export const RECONCILE_ROUNDS = 3; /** 종단도(`design_profile`)가 묶음에 내주는 것. */ export interface ProfileLink { @@ -137,6 +145,8 @@ export interface PagePipelineOptions { routeId: number, saved: SectionDetailResponse | null, ) => (stations: PipelineStation[]) => Promise; + /** 고리가 `RECONCILE_ROUNDS` 바퀴 안에 안 맞음 — 「계산 불안정」 한 줄(365-2). */ + onUnstable?: () => void; } interface PagePipeline { @@ -145,6 +155,8 @@ interface PagePipeline { grade(edits: AlignmentEdits): void; restart(): void; earthwork(context: MassHaulStageContext | null): void; + cross(): void; + reconcile(): Promise; detail(): SectionDetailResponse | null; dispose(): void; } @@ -193,6 +205,13 @@ export const restartPagePipeline = (projectId: string): void => pages.get(projec export const pipelineEarthwork = (projectId: string, context: MassHaulStageContext | null): void => pages.get(projectId)?.earthwork(context); +/** 횡단 고침(지반 유형 · 측구 · 암 · 소단 · 표준 — B06 · 365-2) — 세션 횡단 입력을 다시 모아 cross 부터. */ +export const pipelineCross = (projectId: string): void => pages.get(projectId)?.cross(); + +/** 입력(세션 · 가게) 대 묶음 입력을 견줘 맞춤(365-2) — 돈 바퀴 수(0 = 같음 · -1 = 넘음 · 묶음 없음 = 0). */ +export const reconcilePagePipeline = (projectId: string): Promise => + pages.get(projectId)?.reconcile() ?? Promise.resolve(0); + /** 노선 초안 동안 종단도가 그릴 새 노선 상세 — 초안이 없으면 null(저장 상세). */ export const pipelineDetail = (projectId: string): SectionDetailResponse | null => pages.get(projectId)?.detail() ?? null; @@ -219,10 +238,9 @@ function createPagePipeline( let savedStructures: PlanStructure[] = []; let savedPipes: PlanPipe[] = []; let earthwork: MassHaulStageContext | null = null; - /** 저장 노선 측점(열쇠) · 그것으로 모은 횡단 입력 — 저장 상세가 바뀌면 다시. */ + /** 저장 노선 측점(열쇠) · 마지막으로 모은 횡단 입력(시설 제원 — 새 노선 상세가 씀). */ let savedStations: PipelineStation[] = []; - let crossSource: { detail: SectionDetailResponse; source: PipelineCrossSource | null } | null = - null; + let crossSource: PipelineCrossSource | null = null; /** 저장 정본(서버가 준 상세 · 노선 번호) — 측점 갈아끼움(swap)이 만든 새 상세는 지반 표본 배열을 * 그대로 둠 → 정본 아님(묶음 출력 → 상세 → 다시 묶음 고리를 끊음). */ let canon: { routeId: unknown; samples: unknown; detail: SectionDetailResponse | null } | null = @@ -286,12 +304,17 @@ function createPagePipeline( emptyEdits() ); }; + /** 횡단 입력 — 부를 때마다 저장 상세 + 세션 고침 · 표준을 모음(상세 객체마다 기억하면 고침이 안 감 · 365-2). */ const crossNow = (): PipelineCrossSource | null => { - const detail = canon?.detail ?? null; - if (!detail) return null; - if (crossSource?.detail !== detail || !crossSource.source) - crossSource = { detail, source: pipelineCrossSource(projectId, detail, savedStations) }; - return crossSource.source; + const routeId = canon?.routeId; + const source = pipelineCrossSource( + projectId, + canon?.detail ?? null, + savedStations, + typeof routeId === "number" ? routeId : null, + ); + if (source) crossSource = source; + return source; }; const groundDeps = groundStageDeps(projectId); const crossStage = createCrossStage({ grid: groundDeps.grid }); // 미교차 넓힘 = 같은 지형 격자(349-0j) @@ -460,7 +483,7 @@ function createPagePipeline( ground, profile, pipe.output("cross"), - crossSource?.source?.specs ?? new Map(), + crossSource?.specs ?? new Map(), options.stationIntervalM ?? profile.policy.station_interval_m, ) : null; @@ -517,11 +540,48 @@ function createPagePipeline( // 초안 둔 채 열기(새로고침 · B06 바로 열기 · 365-1) — 초안 전 = 지금 저장 상세(세션 그대로) · 측점은 가게에 냄 held = inputs.route ? (held ?? holdNow()) : null; dropping = null; - void pipe.start(inputs); + Object.assign(given, inputs); + const run = (lastRun = pipe.start(inputs)); openGeneration = pipe.generation; quietGeneration = inputs.route ? -1 : pipe.generation; + void run.then(() => reconcile()); // 열 때 셈하는 사이 바뀐 입력(세션 · 가게)도 맞춤 }; + /** 묶음에 넘긴 입력(follow 가 고치기 전) · 마지막으로 띄운 차례 — 고리가 견줌 · 기다림. */ + const given: Partial = {}; + let lastRun: Promise = Promise.resolve(); + const feed = (source: ChangeSource, value: unknown, part: RangePart | null = null) => { + given[SOURCE_INPUT[source]] = value as never; + const run = pipe.update(source, value, part); + if (run) lastRun = run; + return run; + }; + let reconciling: Promise | null = null; + /** 입력 지문 대 묶음 입력 — 넘긴 값 · follow 뒤 값 둘 다와 다르면 한 바퀴(노선 = 다리 길) · 끝난 뒤 다시 견줌. */ + function reconcile(): Promise { + if (!started || disposed) return Promise.resolve(0); + return (reconciling ??= (async () => { + for (let round = 0; ; round += 1) { + let seen: Promise | null = null; + while (seen !== lastRun) await (seen = lastRun); // 기다리는 사이 뜬 차례까지 + await pipe.settled(); + if (disposed || canon?.routeId !== savedSource().routeId) return round; // 정본 바뀜 = restart 몫 + const now = inputsNow(); + const stale = (Object.entries(SOURCE_INPUT) as Array<[ChangeSource, keyof PipelineInputs]>) + .filter(([, key]) => !same(now[key], pipe.input(key)) && !same(now[key], given[key])) + .map(([source, key]): [ChangeSource, unknown] => [source, now[key]]); + if (!stale.length) return round; + if (round >= RECONCILE_ROUNDS) { + options.onUnstable?.(); + return -1; + } + for (const [source, value] of stale) + if (source === "route") bridge.edited(value as RouteDraft | null); + else feed(source, value); + } + })().finally(() => (reconciling = null))); + } + /** 저장 노선(그린 선 · 곡선표 · 기본 반경) · 그 측점 열쇠 — [저장] 뒤 다시 받음(`restart`). */ const loadPlan = (): Promise => fetchRoutePlan(projectId) @@ -555,13 +615,10 @@ function createPagePipeline( .catch(() => undefined); const unsubscribe = [ - store.subscribe( - "pipes", - (pipes) => started && !echo && void pipe.update("pipe", pipes ?? savedPipes), - ), + store.subscribe("pipes", (pipes) => started && !echo && void feed("pipe", pipes ?? savedPipes)), store.subscribe( "structures", - (list) => started && !echo && void pipe.update("structure", list ?? savedStructures), + (list) => started && !echo && void feed("structure", list ?? savedStructures), ), ]; @@ -582,7 +639,7 @@ function createPagePipeline( if (!started) return start(); const before = pipe.output("route")?.line ?? null; const after = draft ? draftLine(draft, plan?.min_radius_m ?? 0).line : savedLine(); - const run = pipe.update("route", draft, routeRange(before, after)); + const run = feed("route", draft, routeRange(before, after)); if (run && !draft && wasDraft) { quietGeneration = pipe.generation; dropping = run.then(finishDrop); @@ -598,7 +655,7 @@ function createPagePipeline( if (same(edits, pipe.input("alignment"))) return; const nodes = pipe.output("planline")?.basePvi.map((node) => node.chainage_m) ?? []; const before = pipe.input("alignment") as AlignmentEdits | null; - void pipe.update("grade", edits, gradeRange(before, edits, nodes)); + void feed("grade", edits, gradeRange(before, edits, nodes)); }, restart() { if (!started) return start(); @@ -607,8 +664,13 @@ function createPagePipeline( earthwork(context) { earthwork = context; const cross = crossNow()?.input ?? null; - if (started && cross) void pipe.update("cross", cross); + if (started && cross) void feed("cross", cross); }, + cross() { + const cross = started ? (crossNow()?.input ?? null) : null; + if (cross) void feed("cross", cross); + }, + reconcile, detail: () => shown, dispose() { disposed = true; diff --git a/A00_Common/design/design_shared.ts b/A00_Common/design/design_shared.ts index 1ca8333de..275b58cfc 100644 --- a/A00_Common/design/design_shared.ts +++ b/A00_Common/design/design_shared.ts @@ -209,6 +209,7 @@ function startPagePipeline(shared: DesignPageShared, profile: DesignProfile): vo stationIntervalM: SECTION_STATION_INTERVAL_M, onCorridorStale: () => each((hooks) => hooks.onCorridorStale?.()), onFacilitiesFollowed: (far) => each((hooks) => hooks.onFacilitiesFollowed?.(far)), + onUnstable: () => showToast(L("Design_Pipeline_Unstable"), "warning"), structureAreas: ({ cross_sections: rows }) => applyStructureAreaRows(rows, structureAreaRows(rows)), // 초안 시작 — 초안 전 상태를 떠 둠 · 버리면 그대로(측점이 달라도 · 349-0o) · 상세가 없으면 묶음 측점으로 갈아끼움 diff --git a/B06_Section/B06_Section_UI_Page_Design_Sync.ts b/B06_Section/B06_Section_UI_Page_Design_Sync.ts index fa04f3326..590837950 100644 --- a/B06_Section/B06_Section_UI_Page_Design_Sync.ts +++ b/B06_Section/B06_Section_UI_Page_Design_Sync.ts @@ -7,6 +7,7 @@ * - `reconcileStaleDesigns`: 낡은 design 일괄 재계산(B05 와 같은 창구) * - `syncBermSpans` : 구조물 목록의 소단을 세션 사본으로 펴기 * - `applyPanelToAll` : 표준 횡단면 설정 [전체 반영] + * 고친 뒤 계산 묶음 cross 입력도 다시(`pipelineCross` · 노선 초안 동안 카드 = 묶음 새 노선 상세 · 365-2). * ========================================================================== */ import { @@ -28,6 +29,7 @@ import type { CrossDesignChange, SectionViewController } from "./B06_Section_UI_ import type { StructureInstance } from "../B05_Profile/B05_Profile_Api_Structures"; import { setCrossDetailHost } from "./B06_Section_UI_Cross_Detail"; import { useStructureIds } from "./B06_Section_Station_Keys"; +import { pipelineCross } from "../A00_Common/design/design_pipeline_page"; export interface DesignSyncContext { projectId: string | null; @@ -125,6 +127,7 @@ export function createDesignSync(ctx: DesignSyncContext): DesignSyncController { .detail() ?.cross_sections.find((section) => Math.abs(section.chainage_m - chainageM) < 0.01)?.design; if (design && design.ground_type !== "soil") void reconcileStaleDesigns({ force: true }); + else if (projectId) pipelineCross(projectId); } /** @@ -184,6 +187,7 @@ export function createDesignSync(ctx: DesignSyncContext): DesignSyncController { const detail = error instanceof Error ? ` ${error.message}` : ""; showToast(`${L("B06_Design_Failed")}${detail}`, "error"); } + pipelineCross(projectId); // 세션 고침 · 표준 → 묶음(같은 입력이면 안 셈) } /** diff --git a/resources/tester/helper_365_page_pipeline.cjs b/resources/tester/helper_365_page_pipeline.cjs index ae558e96a..c65ca075d 100644 --- a/resources/tester/helper_365_page_pipeline.cjs +++ b/resources/tester/helper_365_page_pipeline.cjs @@ -199,6 +199,7 @@ async function run() { const savedBase = await savedBaseOf(); const shop = store.useDesignStore(P, null); const holds = []; + let unstable = 0; const link = () => ({ pipelineSource: () => ({ base: savedBase, @@ -215,6 +216,7 @@ async function run() { holds.push(saved === job.detail ? "saved" : saved ? "other" : "null"); return async () => "saved"; }, + onUnstable: () => (unstable += 1), }; const open = (label) => { const bridge = pageMod.routeDraftBridge(P, link(), options); @@ -249,6 +251,56 @@ async function run() { stages: Object.keys(reopenRun.stages), }; + // ⑤ 초안 동안 B06 고침 — 세션 칸(측점 열쇠) → pipelineCross → 카드(묶음 새 노선 상세) + const pipe = pageMod.pagePipeline(P); + const target = pipe.output("ground").stations.find((s) => s.kind === "grid" && s.chainageM > 30); + const cell = (ground) => ({ [target.key]: { ground_type: ground, section_mode: "left_cut" } }); + const groundAt = () => + pageMod + .pipelineDetail(P) + .cross_sections.find((s) => Math.abs(s.chainage_m - target.chainageM) < 1e-3)?.design + ?.ground_type ?? null; + const before = groundAt(); + page.writeState("crossdesign", cell("blasting_rock"), P, 7); + let mark = runs().length; + pageMod.pipelineCross(P); + await settle(mark + 1); + await tick(20); + const edit = { + key: target.key, + before, + after: groundAt(), + input: pipe.input("cross").byKey[target.key]?.groundType ?? null, + runs: runs().length - mark, + sources: runs()[runs().length - 1].sources, + }; + // ⑥ 고리 — 같은 입력 = 0 바퀴 · 세션만 바뀜(알림 없음) = 1 바퀴 · 안 맞음 = 3 바퀴 뒤 알림 + mark = runs().length; + const same = await pageMod.reconcilePagePipeline(P); + const sameRuns = runs().length - mark; + page.writeState("crossdesign", cell("ripping_rock"), P, 7); + mark = runs().length; + const once = await pageMod.reconcilePagePipeline(P); + const loop = { + same, + sameRuns, + once, + onceRuns: runs().length - mark, + onceGround: groundAt(), + }; + let n = 0; + const realStandard = STUBS["B06_Section_UI_Standard_Panel"].effectiveStandardCross; + STUBS["B06_Section_UI_Standard_Panel"].effectiveStandardCross = () => ({ + ...job.standard, + n: (n += 1), + }); + mark = runs().length; + loop.unstable = await pageMod.reconcilePagePipeline(P); + loop.unstableRuns = runs().length - mark; + loop.alerts = unstable; + STUBS["B06_Section_UI_Standard_Panel"].effectiveStandardCross = realStandard; + await pageMod.reconcilePagePipeline(P); + // ④ 초안 버림 — 열 때 맡긴 초안 전 상세로 되돌림(저장 정본) again.bridge.edited(null); const dropRun = await settle(runs().length + 1); @@ -260,7 +312,7 @@ async function run() { }; pageMod.disposePagePipeline(P); page.clearDrafts(P); - return { kept, b05, b06, dropped }; + return { kept, b05, b06, dropped, edit, loop }; } run() diff --git a/resources/tester/test_365_page_pipeline.py b/resources/tester/test_365_page_pipeline.py index fa8c67afc..45924176d 100644 --- a/resources/tester/test_365_page_pipeline.py +++ b/resources/tester/test_365_page_pipeline.py @@ -5,6 +5,9 @@ - 노선 초안 둔 채 다시 열기(새로고침 · B06 바로 열기 · 편집기 없음) = B05 길과 같은 초안 상세 (측점 · 계획고 · 면적) · 측점을 가게에 다시 냄 · 초안 전 = 저장 상세(열 때 맡김) → 버리면 saved. - 초안 동안 측점 갈아끼움은 메모리만 — 세션 상세(저장 노선)를 새 측점 + 옛 설계로 덮지 않음. +못박는 것(365-2) + - 초안 동안 B06 고침(세션 칸 · 측점 열쇠) → `pipelineCross` → 묶음 cross 입력 · 카드에 보임. + - 고리 — 같은 입력 = 0 바퀴 · 세션만 바뀜 = 1 바퀴 · 안 맞으면 3 바퀴 뒤 「계산 불안정」 한 번. 짝: `helper_365_page_pipeline.cjs`(진짜 TS · 서버 창구만 가짜 · 틀 = 346-2 시험) · `helper_365_swap_entry.ts` + `helper_365_swap.mjs`(진짜 상세 가게 · vite ssr). @@ -99,6 +102,20 @@ def test_365_1_초안_버림_저장_정본(r): assert d["restoreWhy"] == "saved" +def test_365_2_초안_동안_횡단_고침_카드에(r): + e = r["edit"] + assert e["before"] != "blasting_rock" + assert e["input"] == "blasting_rock" and e["after"] == "blasting_rock" + assert e["runs"] == 1 and e["sources"] == ["cross"] + + +def test_365_2_고리_바퀴_수(r): + loop = r["loop"] + assert loop["same"] == 0 and loop["sameRuns"] == 0 # 같은 입력 = 안 돎 + assert loop["once"] == 1 and loop["onceRuns"] == 1 and loop["onceGround"] == "ripping_rock" + assert loop["unstable"] == -1 and loop["unstableRuns"] == 3 and loop["alerts"] == 1 + + @pytest.fixture(scope="module") def swap(tmp_path_factory) -> dict: work = tmp_path_factory.mktemp("b365") diff --git a/ui_template/ui_template_locale_b2.ts b/ui_template/ui_template_locale_b2.ts index 3470286fa..35c8f42dc 100644 --- a/ui_template/ui_template_locale_b2.ts +++ b/ui_template/ui_template_locale_b2.ts @@ -1047,4 +1047,8 @@ export const ui_locales_b2 = { ], B06_Std_Note_Rock: ["암", "Rock"], B06_Std_Note_Paved: ["포장", "Paved"], + Design_Pipeline_Unstable: [ + "계산 불안정 — 입력과 결과가 세 바퀴 안에 안 맞음", + "Calculation unstable — inputs and results did not settle in three rounds", + ], } as const satisfies Record;