Files
Aislo/resources/tester/helper_344_4_design_pipeline.cjs
T
eomsangdonandClaude Opus 5.5 281c71b55b refactor(logic): 348-1 순수 계산 TS 49 파일 <페이지>/logic/ 로 옮김 — 이름 앞머리 그대로 · 동작 고침 없음
- 옮김: B04 2 · B05 23 · B06 11 · A00_Common/design 13 (9,299 줄) · 이미 logic/ 에 있던 16 파일 그대로
- 안 옮김: 묶음 입구(package.json build:*) · 파이썬 · 순수지만 계산 아닌 8 (보기 결선 · 콜백 · API 타입 · 갈아끼움 알림판)
- import 266 줄(142 파일) · 시험 · 주석 · 짝 표시 문자열 경로 고침 · B05 · B06 CLAUDE.md 짝 줄 logic/
- 시험 도우미 새 경로: 가짜 require 열쇠 · B05()/B06() 부름 · 형제 import 자리 · tsc 묶어 돌리는 시험 · 정규식 · rglob
- design_view_cross 줄 한도 시험 685 → 688 (긴 import 경로 줄바꿈 3줄)
- 확인: typecheck 0 · 서버 묶음 넷(corridor · server-calc · b07-extent · b07-note) 다시 지어 코드 같음(//#region 경로 주석만 다름) · 관련 시험 270 파일 — 남은 실패 19 는 옮기기 전과 같음

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014utrjfoW831Vc5wJUd13kL
2026-10-16 13:44:50 +09:00

380 lines
13 KiB
JavaScript

/* PLAN 344-4 단위검증 헬퍼 — 계산 묶음(`A00_Common/design/logic/design_pipeline.ts`) · 가게 칸
* (`design_store.ts`) · 등록표(`b_page_state.ts`)를 그 자리에서 트랜스파일해 Node 로 돌림.
* 단계 함수는 가짜 — 부른 수 · 받은 범위 · 차례 번호만 적음. 결과 = 표준출력 JSON 한 벌.
* 짝 = `test_344_4_design_pipeline.py`. */
const fs = require("fs");
const path = require("path");
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
const ROOT = path.join(__dirname, "..", "..");
function memoryStorage() {
const map = new Map();
return {
getItem: (key) => (map.has(key) ? map.get(key) : null),
setItem: (key, value) => map.set(key, String(value)),
removeItem: (key) => map.delete(key),
key: (index) => [...map.keys()][index] ?? null,
get length() {
return map.size;
},
};
}
const session = memoryStorage();
globalThis.window = { sessionStorage: session, localStorage: memoryStorage() };
globalThis.fetch = async () => ({ ok: false });
const cache = new Map();
function loadTs(file) {
const full = path.resolve(file);
if (cache.has(full)) return cache.get(full);
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
}).outputText;
const box = { exports: {} };
cache.set(full, box.exports);
const localRequire = (request) =>
request.startsWith(".")
? loadTs(path.join(path.dirname(full), `${request}.ts`))
: require(request);
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
cache.set(full, box.exports);
return box.exports;
}
const DESIGN = path.join(ROOT, "A00_Common", "design");
const pipeMod = loadTs(path.join(DESIGN, "logic/design_pipeline.ts"));
const storeMod = loadTs(path.join(DESIGN, "design_store.ts"));
const page = loadTs(path.join(ROOT, "A00_Common", "b_page_state.ts"));
const tick = (ms = 0) => new Promise((resolve) => setTimeout(resolve, ms));
const plain = (range) =>
range && { ...range, toM: Number.isFinite(range.toM) ? range.toM : "Infinity" };
/** 가짜 단계 — 부른 수 · 범위 · 번호를 적고 입력 지문을 돌려줌. hold[name] 이 있으면 그 약속을 기다림. */
function fakeStages(log, hold = {}) {
const stages = {};
for (const name of [...pipeMod.BROWSER_STAGES, ...pipeMod.AFTER_STAGES]) {
stages[name] = (ctx) => {
log.push({
name,
gen: ctx.generation,
range: plain(ctx.range),
aborted: () => ctx.signal.aborted,
});
const value =
name === "follow"
? null
: { name, gen: ctx.generation, inputs: JSON.stringify(ctx.inputs) };
if (name === "ground") value.stations = [{ key: "0", chainageM: 0 }];
return hold[name] ? hold[name](ctx).then(() => value) : value;
};
}
return stages;
}
const calls = (log, name) => log.filter((entry) => entry.name === name).length;
const BASE = {
route: { rev: 1 },
alignment: { station_offsets: {} },
cross: {},
pipes: [],
structures: [],
};
async function run() {
const out = {};
// ① 처음 · 같은 입력 다시 안 셈 · 범위 전달
{
const log = [];
const pipe = pipeMod.createDesignPipeline({ stages: fakeStages(log) });
const events = [];
pipe.subscribe((event) =>
events.push(event.kind === "stage" ? event.stage : `gen${event.generation}`),
);
const first = await pipe.start(BASE);
await pipe.settled();
const allOnce = Object.fromEntries(
[...pipeMod.BROWSER_STAGES, ...pipeMod.AFTER_STAGES].map((n) => [n, calls(log, n)]),
);
const firstRange = log.find((e) => e.name === "route").range;
const same = pipe.update("grade", { station_offsets: {} });
const genAfterSame = pipe.generation;
log.length = 0;
const cross = await pipe.update(
"cross",
{ "0+040": { mode: "both_fill" } },
{ fromM: 40, toM: 40, keys: ["0+040"] },
);
await pipe.settled();
const crossRan = log.map((e) => e.name);
const crossRange = log.find((e) => e.name === "cross").range;
log.length = 0;
await pipe.update("grade", { station_offsets: { "100.000": 0.5 } }, { fromM: 80, toM: 120 });
await pipe.settled();
const gradeRan = log.map((e) => e.name);
const gradeRange = log.find((e) => e.name === "profile").range;
log.length = 0;
await pipe.update("route", { rev: 2 }, { fromM: 120, toM: Infinity });
await pipe.settled();
const routeRan = log.map((e) => e.name);
const routeRange = log.find((e) => e.name === "stations").range;
out.basic = {
first: { outcome: first.outcome, skipped: first.skipped, allOnce, firstRange },
same: { result: same, generation: genAfterSame },
crossRan,
crossRange,
crossSkipped: cross.skipped,
gradeRan,
gradeRange,
routeRan,
routeRange,
events: events.slice(0, 3),
};
}
// ② 늦은 응답 버림 — 브라우저 단계(ground 늦음) · 뒤 단계(haul 늦음 · 끊김)
{
const log = [];
let releaseGround = null;
let holdGround = true;
const hold = {
ground: () =>
holdGround ? new Promise((resolve) => (releaseGround = resolve)) : Promise.resolve(),
};
const pipe = pipeMod.createDesignPipeline({ stages: fakeStages(log, hold) });
const commits = [];
pipe.subscribe((e) => e.kind === "stage" && commits.push(`${e.stage}@${e.generation}`));
const runA = pipe.start(BASE);
await tick(5);
const release1 = releaseGround;
holdGround = false;
const runB = pipe.update("route", { rev: 2 }, { fromM: 0, toM: Infinity });
const recB = await runB;
release1();
const recA = await runA;
await pipe.settled();
out.lateBrowser = {
a: recA.outcome,
b: recB.outcome,
groundGen: pipe.output("ground").gen,
commits,
};
// 뒤 단계 — haul 1 이 늦게 옴 · 그 사이 새 조작 → 끊김 · 버림
let releaseHaul = [];
const hold2 = { haul: () => new Promise((resolve) => releaseHaul.push(resolve)) };
const log2 = [];
const pipe2 = pipeMod.createDesignPipeline({ stages: fakeStages(log2, hold2) });
const commits2 = [];
pipe2.subscribe((e) => e.kind === "stage" && e.stage === "haul" && commits2.push(e.generation));
await pipe2.start(BASE);
const haul1 = log2.find((e) => e.name === "haul");
await pipe2.update("cross", { x: 1 }, { fromM: 0, toM: 0 });
const haul1Aborted = haul1.aborted();
releaseHaul[0]();
await tick(1);
releaseHaul[1]();
await pipe2.settled();
out.lateAfter = {
haul1Aborted,
commits: commits2,
haulGen: pipe2.output("haul").gen,
generation: pipe2.generation,
};
}
// ③ 범위 합침 — cross 가 끝나기 전 새 조작 → 다음 cross 는 두 범위를 함께 셈
{
const log = [];
let release = null;
let holdCross = false;
const hold = {
cross: () => (holdCross ? new Promise((r) => (release = r)) : Promise.resolve()),
};
const pipe = pipeMod.createDesignPipeline({ stages: fakeStages(log, hold) });
await pipe.start(BASE);
await pipe.settled();
log.length = 0;
holdCross = true;
const first = pipe.update(
"grade",
{ station_offsets: { "50.000": 1 } },
{ fromM: 40, toM: 60 },
);
await tick(10);
holdCross = false;
const second = pipe.update(
"grade",
{ station_offsets: { "210.000": 1 } },
{ fromM: 200, toM: 220 },
);
release();
const rec1 = await first;
await second;
await pipe.settled();
const profiles = log.filter((e) => e.name === "profile").map((e) => e.range);
const crosses = log.filter((e) => e.name === "cross").map((e) => e.range);
out.merge = { first: rec1.outcome, profiles, crosses };
out.mergeFn = plain(
pipeMod.mergeRange(
{ sources: ["grade"], fromM: 40, toM: 60, keys: [] },
{ sources: ["cross"], fromM: 10, toM: 20, keys: ["a"] },
),
);
}
// ④ 고리 없음 — 알림 · 단계 안 입력 쓰기 막음 · follow 만 한 번 · 노선 · 횡단은 못 고침
{
const log = [];
const stages = fakeStages(log);
let stageWrite = "unset";
let pipe = null;
stages.planline = (ctx) => {
stageWrite = pipe.update("pipe", [{ id: "p-loop" }]);
return { gen: ctx.generation };
};
stages.follow = (ctx) => {
log.push({ name: "follow", gen: ctx.generation });
return {
pipes: [{ id: "p1", followed: ctx.inputs.route.rev }],
route: { rev: 99 },
cross: { bad: 1 },
};
};
pipe = pipeMod.createDesignPipeline({ stages });
let notifyWrite = "unset";
pipe.subscribe((e) => {
if (e.kind === "stage" && e.stage === "profile")
notifyWrite = pipe.update("grade", { station_offsets: { loop: 1 } });
});
const warn = console.warn;
const warnings = [];
console.warn = (...args) => warnings.push(String(args[0]));
await pipe.start(BASE);
await pipe.settled();
const afterStart = {
generation: pipe.generation,
follow: calls(log, "follow"),
stations: calls(log, "stations"),
};
const stationsInputs = JSON.parse(pipe.output("stations").inputs);
log.length = 0;
await pipe.update("grade", { station_offsets: { "10.000": 1 } }, { fromM: 0, toM: 20 });
await pipe.settled();
const gradeRan = log.map((e) => e.name);
console.warn = warn;
out.loop = {
stageWrite,
notifyWrite,
blocked: pipe.blockedWrites,
afterStart,
stationsPipes: stationsInputs.pipes,
stationsRoute: stationsInputs.route,
stationsCross: stationsInputs.cross,
gradeRan,
warnings: warnings.length,
};
}
// ⑤ 단계 시간 기록 — 마지막 RUN_LOG_SIZE 번 · 단계별 ms
{
const pipe = pipeMod.createDesignPipeline({ stages: fakeStages([]) });
await pipe.start(BASE);
for (let i = 0; i < 25; i += 1)
await pipe.update("grade", { station_offsets: { i } }, { fromM: i, toM: i + 1 });
await pipe.settled();
const runs = globalThis.__aisloPipeline.runs;
const last = runs[runs.length - 1];
out.timing = {
size: runs.length,
limit: pipeMod.RUN_LOG_SIZE,
last: {
outcome: last.outcome,
sources: last.sources,
stageNames: Object.keys(last.stages),
totalIsNumber: typeof last.totalMs === "number",
},
};
}
// ⑥ 가게 — 결과 칸 메모리 우선 · 멈춘 뒤 한 번 · 지우면 메모리도 · 노선 초안 즉시 · clearDrafts
{
const P = "proj-344";
const store = storeMod.useDesignStore(P, null);
const key = (n, b = "result") => `aislo:${b}:${n}:${P}`;
const stations = [{ key: "0", chainageM: 0 }];
store.set("stations", stations);
const beforeFlush = session.getItem(key("design-stations"));
const memoryRead = store.get("stations") === stations;
await tick(storeMod.PERSIST_DELAY_MS + 50);
const afterDelay = JSON.parse(session.getItem(key("design-stations")));
store.set("profile", { schema_version: 1 });
storeMod.flushDesignStore();
const flushed = JSON.parse(session.getItem(key("design-profile")));
store.set("generation", 7);
const generationSession = [...Array(session.length).keys()]
.map((i) => session.key(i))
.filter((k) => k.includes("generation"));
page.clearResults(P);
const cleared = { stations: store.get("stations"), profile: store.get("profile") };
store.set("footprint", { roadLeft: [] });
page.clearResults(P);
await tick(storeMod.PERSIST_DELAY_MS + 50);
const footprintRevived = session.getItem(key("design-footprint"));
store.set("routeDraft", {
rev: 3,
nodes: [
[0, 0],
[10, 0],
],
});
const draftNow = JSON.parse(session.getItem(key("route-draft", "draft")));
const hasDraft = page.hasDrafts(P);
page.clearDrafts(P);
const draftCleared = {
session: session.getItem(key("route-draft", "draft")),
store: store.get("routeDraft"),
};
const pipe = pipeMod.createDesignPipeline({ stages: fakeStages([]) });
pipe.subscribe(pipeMod.publishToStore(store));
await pipe.start(BASE);
await pipe.settled();
out.store = {
beforeFlush,
memoryRead,
afterDelay,
flushed,
generation: store.get("generation"),
generationSession,
cleared,
footprintRevived,
draftNow,
hasDraft,
draftCleared,
published: {
generation: store.get("generation"),
stations: store.get("stations"),
profileGen: store.get("profile").gen,
},
registry: {
draft: page.STATE_REGISTRY["route-draft"],
stations: page.STATE_REGISTRY["design-stations"],
profile: page.STATE_REGISTRY["design-profile"],
footprint: page.STATE_REGISTRY["design-footprint"],
},
};
}
return out;
}
run()
.then((out) => {
process.stdout.write(JSON.stringify(out));
process.exit(0);
})
.catch((error) => {
process.stderr.write(String(error && error.stack ? error.stack : error));
process.exit(1);
});