- 옮김: 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
380 lines
13 KiB
JavaScript
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);
|
|
});
|