feat(478-2 문서 사이트): 묶음 규칙 · 원고 생성 · mkdocs 설정 · 손글 옮김 · 검사 시험(가짜 추출)

- docs/tools/groups.yml · group.py — 함수를 로직 묶음에 자동 배정(계산 등록부 id · 파일 이름 무늬 · 안 맞음 = 묶음 없음 · 함수 목록 지문)
- docs/tools/build.py — 추출 JSON + 손글로 페이지 · 묶음 화면 · 함수 칸 · mermaid 흐름 · 세션 칸 · 서버 주소 · 설명 확인 필요 표시 · 함수 화면은 TypeDoc(markdown) · mkdocstrings(griffe)
- docs/mkdocs.yml · nav_hook.py — Material · 한국어 · 검색 · mermaid · 왼쪽 메뉴 페이지 → 묶음 → 함수
- docs/guide/B05_B06 — 8 파일을 page.md(개요 · 규칙 · 사용자 결정) + groups/*.md 17 개로 줄여 옮김
- resources/tester/test_478_docs.py · fixtures/478_docs — 가짜 추출로 배정 · 사이트 strict · 도구 대 추출 · 진짜 추출은 있을 때만

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-19 18:53:43 +09:00
co-authored by Claude Opus 5.5
parent bbc0f53f50
commit 2e6086c5ee
40 changed files with 1865 additions and 415 deletions
+6
View File
@@ -0,0 +1,6 @@
# 문서 사이트 도구 (PLAN 478)
- 설치 — `npm --prefix docs/tools install` · `venv/Scripts/python.exe -m pip install -r docs/tools/requirements.txt`
- 원고 — `venv/Scripts/python.exe docs/tools/build.py` (추출 `docs/_data` → `docs/_build/src` · `--site` = 사이트까지 · `--stamp` = 손글 지문 고침)
- 띄우기 — `cd docs && ../venv/Scripts/python.exe -m mkdocs serve -a 127.0.0.1:8090`
- 묶음 규칙 — `groups.yml` · 손글 — `docs/guide/<페이지>/page.md` · `groups/<묶음>.md`
+457
View File
@@ -0,0 +1,457 @@
"""문서 사이트 원고 만들기(PLAN 478-2).
추출(docs/_data/*.json) + 손글(docs/guide/<페이지>/page.md · groups/<묶음>.md) → docs/_build/src
index.md 처음
<페이지>/index.md 개요 · 묶음 상자 · 묶음 사이 흐름 그림 · 손글(규칙 · 사용자 결정)
<페이지>/<묶음>.md 손글 설명 · 묶음 안 흐름 그림 · 세션 칸 · 서버 주소 · 함수 칸
<페이지>/none.md 묶음 없음
ts/ · py/ 함수 화면 — TypeDoc(markdown) · mkdocstrings(griffe)가 만듦
.nav.json 왼쪽 메뉴(nav_hook.py 가 읽음)
함수 목록 지문이 손글 지문과 다른 묶음 = 「설명 확인 필요」 · 맞으면 --stamp.
venv python docs/tools/build.py [--data D] [--out O] [--site] [--stamp]
"""
from __future__ import annotations
import argparse
import hashlib
import json
import re
import shutil
import subprocess
import sys
import time
from collections import Counter
from pathlib import Path
import griffe
import yaml
sys.path.insert(0, str(Path(__file__).resolve().parent))
import group as grouping # noqa: E402
TOOLS = grouping.TOOLS
DOCS = TOOLS.parent
ROOT = DOCS.parent
REF = re.compile(r"\[\[([^\]]+)\]\]")
MAX_GRAPH = 40
# ── 읽기 ────────────────────────────────────────────────────────────────────
def load_data(data: Path) -> dict:
out = {}
for name in ("functions", "session_keys", "api", "registry"):
path = data / f"{name}.json"
out[name] = json.loads(path.read_text(encoding="utf-8")) if path.exists() else []
if not out["functions"]:
raise SystemExit(f"추출 없음: {data / 'functions.json'}")
return out
def read_hand(path: Path) -> tuple[dict, str]:
if not path.exists():
return {}, ""
text = path.read_text(encoding="utf-8")
if text.startswith("---\n"):
head, _, body = text[4:].partition("\n---\n")
return yaml.safe_load(head) or {}, body.strip()
return {}, text.strip()
def stamp(path: Path, print_: str) -> None:
meta, body = read_hand(path)
meta["지문"] = print_
head = yaml.safe_dump(meta, allow_unicode=True, sort_keys=False).strip()
path.write_text(f"---\n{head}\n---\n\n{body}\n", encoding="utf-8")
# ── 함수 화면(기성 도구) ───────────────────────────────────────────────────
def run_typedoc(ts_files: list[str], out: Path) -> dict[tuple[str, str], str]:
"""TypeDoc 으로 ts/ 함수 화면 · (파일, 이름) → 원고 안 경로."""
if not ts_files:
return {}
opts = {
"entryPoints": [(ROOT / f).as_posix() for f in ts_files],
"tsconfig": (ROOT / "tsconfig.json").as_posix(),
"skipErrorChecking": True,
"plugin": ["typedoc-plugin-markdown"],
"out": (out / "src" / "ts").as_posix(),
"json": (out / "typedoc.json").as_posix(),
"readme": "none",
"entryFileName": "index.md",
"logLevel": "Error",
}
opt_file = out / "typedoc.options.json"
opt_file.write_text(json.dumps(opts, ensure_ascii=False), encoding="utf-8")
subprocess.run(
["node", str(TOOLS / "node_modules/typedoc/bin/typedoc"), "--options", str(opt_file)],
cwd=ROOT,
check=True,
)
return typedoc_pages(out)
def typedoc_pages(out: Path) -> dict[tuple[str, str], str]:
project = json.loads((out / "typedoc.json").read_text(encoding="utf-8"))
modules = [m for m in project.get("children", []) if m["kind"] == 2] or [
{**project, "name": ""}
]
pages = {}
for mod in modules:
base = f"ts/{mod['name']}/" if mod["name"] else "ts/"
for c in mod.get("children", []):
folder = {64: "functions", 32: "variables"}.get(c["kind"])
if folder and c.get("sources"):
rel = f"{base}{folder}/{c['name']}.md"
if (out / "src" / rel).exists():
pages[(c["sources"][0]["fileName"], c["name"])] = rel
return pages
def py_ident(file: str) -> str:
return file.removesuffix(".py").replace("/", ".")
def griffe_names(file: str) -> set[str]:
"""griffe 가 보는 함수 이름(모듈 함수 · 클래스 메서드 `Class.name`)."""
mod = griffe.load(py_ident(file), search_paths=[str(ROOT)], allow_inspection=False)
names: set[str] = set()
def walk(obj, prefix: str) -> None:
for name, m in obj.members.items():
if m.is_alias:
continue
if m.is_function:
names.add(prefix + name)
elif m.is_class:
walk(m, f"{prefix}{name}.")
walk(mod, "")
return names
def write_py_pages(py_files: list[str], src: Path) -> dict[tuple[str, str], str]:
"""mkdocstrings 원고 · (파일, 이름) → autorefs 식별자."""
refs = {}
lines = ["# 파이썬 함수 화면", ""]
for file in py_files:
ident = py_ident(file)
(src / "py" / f"{ident}.md").write_text(f"# {ident}\n\n::: {ident}\n", encoding="utf-8")
lines.append(f"- [{file}]({ident}.md)")
refs.update({(file, n): f"{ident}.{n}" for n in griffe_names(file)})
(src / "py" / "index.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
return refs
# ── 원고 ───────────────────────────────────────────────────────────────────
def anchor(fid: str) -> str:
return "f-" + hashlib.sha1(fid.encode("utf-8")).hexdigest()[:10]
def first_line(doc: str) -> str:
return next((s.strip() for s in (doc or "").splitlines() if s.strip()), "")
def owner_of(item: str, fns_by_id: dict, by_file: dict) -> dict | None:
"""세션 칸 · 서버 주소의 `id` · `파일:줄` · `파일` → 함수."""
if item in fns_by_id:
return fns_by_id[item]
if item[:3] in ("ts:", "py:"):
item = item[3:]
file, _, line = item.split("#")[0].partition(":")
cands = by_file.get(file, [])
if not cands:
return None
if line.isdigit():
before = [f for f in cands if f.get("line", 0) <= int(line)]
return before[-1] if before else cands[0]
return cands[0]
class Site:
def __init__(self, data: dict, rules: dict, out: Path):
self.data, self.rules, self.out = data, rules, out.resolve()
self.src = self.out / "src"
self.fns = data["functions"]
self.by_id = {f["id"]: f for f in self.fns}
self.by_file: dict[str, list] = {}
for f in sorted(self.fns, key=lambda f: f.get("line", 0)):
self.by_file.setdefault(f["file"], []).append(f)
self.gid: dict[str, str] = {}
self.missing_refs: list[str] = []
self.stale: list[str] = []
self.ts_pages: dict = {}
self.py_refs: dict = {}
# 링크
def fn_link(self, fid: str, label: str | None = None) -> str:
f = self.by_id.get(fid)
if f is None:
return f"`{label or fid.split('#')[-1]}`"
return f"[`{label or f['name']}`]({self.gid[fid]}.md#{anchor(fid)})"
def hand_refs(self, text: str, where: str) -> str:
def sub(m: re.Match) -> str:
ref = m.group(1).strip()
hit = [
f
for f in self.fns
if f["name"] == ref or f"{grouping.stem(f['file'])}#{f['name']}" == ref
]
if not hit:
self.missing_refs.append(f"{where}: {ref}")
return f"`{ref}` ⚠"
return self.fn_link(hit[0]["id"])
return REF.sub(sub, text)
def tool_link(self, f: dict) -> str:
if f["lang"] == "ts":
rel = self.ts_pages.get((f["file"], f["name"]))
return f"[자세히](../{rel})" if rel else ""
ref = self.py_refs.get((f["file"], f["name"]))
return f"[자세히][{ref}]" if ref else ""
# 함수 칸
def fn_block(self, f: dict) -> str:
params = ", ".join(
p["name"] if isinstance(p, dict) else str(p) for p in f.get("params", [])
)
bits = [f"`{f['file']}:{f.get('line', 0)}`"]
if params:
bits.append(f"입력 `{params}`")
if f.get("returns"):
bits.append(f"출력 `{f['returns']}`")
if link := self.tool_link(f):
bits.append(link)
lines = [
f"### `{f['name']}` {{ #{anchor(f['id'])} }}",
"",
first_line(f.get("doc")) or "*설명 없음*",
"",
]
lines += [" · ".join(bits), ""]
callers = f.get("called_by", [])
if callers:
shown = " · ".join(self.fn_link(c) for c in callers[:8])
more = f" 외 {len(callers) - 8}" if len(callers) > 8 else ""
lines += [f"부르는 곳: {shown}{more}", ""]
return "\n".join(lines)
def fn_graph(self, fns: list[dict]) -> str:
ids = {f["id"] for f in fns}
edges = [(f["id"], c) for f in fns for c in f.get("calls", []) if c in ids and c != f["id"]]
if not edges:
return ""
degree = Counter(a for e in edges for a in e)
keep = {i for i, _ in degree.most_common(MAX_GRAPH)}
node = {i: f"n{k}" for k, i in enumerate(sorted(keep))}
lines = ["```mermaid", "flowchart LR"]
lines += [f' {node[i]}["{self.by_id[i]["name"]}"]' for i in sorted(keep)]
lines += [
f" {node[a]} --> {node[b]}" for a, b in sorted(set(edges)) if a in keep and b in keep
]
lines.append("```")
note = f"\n\n많이 이어진 함수 {MAX_GRAPH} 개만 그림." if len(degree) > MAX_GRAPH else ""
return "\n".join(lines) + note
def group_uses(self, gid: str) -> tuple[list[str], list[str]]:
def mine(item: str) -> bool:
o = owner_of(item, self.by_id, self.by_file)
return o is not None and self.gid[o["id"]] == gid
keys = []
for k in self.data["session_keys"]:
reads, writes = (
[r for r in k.get("reads", []) if mine(r)],
[w for w in k.get("writes", []) if mine(w)],
)
if reads or writes:
keys.append(f"| `{k['key']}` | {len(reads)} | {len(writes)} |")
apis = []
for a in self.data["api"]:
calls = [c for c in a.get("callers", []) if mine(c)]
if mine(a.get("handler", "")) or calls:
handler = (
self.fn_link(a["handler"])
if a.get("handler") in self.by_id
else f"`{a.get('handler', '')}`"
)
apis.append(f"| `{a['method']} {a['path']}` | {handler} | {len(calls)} |")
return keys, apis
# 화면
def write_group(self, page: dict, g: dict, fns: list[dict], hand_dir: Path) -> str:
meta, body = read_hand(hand_dir / f"{g['id']}.md")
now = grouping.fingerprint(fns)
out = [f"# {g['title']}", ""]
if meta.get("지문") != now:
self.stale.append(g["id"])
out += [
'!!! warning "설명 확인 필요"',
f" 함수 목록이 바뀜(손글 지문 `{meta.get('지문', '없음')}` · 지금 `{now}`).",
"",
]
out += [self.hand_refs(body, f"groups/{g['id']}.md") or "*설명 없음*", ""]
if graph := self.fn_graph(fns):
out += ["## 묶음 안 흐름", "", graph, ""]
keys, apis = self.group_uses(g["id"])
if keys:
out += ["## 쓰는 세션 칸", "", "| 칸 | 읽는 곳 | 쓰는 곳 |", "|---|---|---|", *keys, ""]
if apis:
out += [
"## 서버 주소",
"",
"| 주소 | 받는 함수 | 부르는 곳 |",
"|---|---|---|",
*apis,
"",
]
out += [f"## 함수 {len(fns)}", "", *(self.fn_block(f) for f in fns)]
(self.src / page["id"] / f"{g['id']}.md").write_text("\n".join(out), encoding="utf-8")
return first_line(body)
def write_page(self, page: dict, groups: dict[str, list[dict]]) -> list:
hand_dir = DOCS / page["guide"]
(self.src / page["id"]).mkdir(parents=True, exist_ok=True)
cards, nav = [], [{"개요": f"{page['id']}/index.md"}]
for g in page["groups"]:
fns = groups[g["id"]]
lead = self.write_group(page, g, fns, hand_dir / "groups")
warn = " · ⚠ 설명 확인 필요" if g["id"] in self.stale else ""
cards += [
f"- **[{g['title']}]({g['id']}.md)** · 함수 {len(fns)}{warn}",
"",
f" {lead}",
"",
]
nav.append({g["title"]: f"{page['id']}/{g['id']}.md"})
none = groups[grouping.NONE]
none_md = [f"# 묶음 없음 {len(none)}", "", "groups.yml 규칙에 안 맞는 함수.", ""]
(self.src / page["id"] / "none.md").write_text(
"\n".join(none_md + [self.fn_block(f) for f in none]), encoding="utf-8"
)
nav.append({"묶음 없음": f"{page['id']}/none.md"})
_, body = read_hand(hand_dir / "page.md")
md = [
f"# {page['title']}",
"",
'<div class="grid cards" markdown>',
"",
*cards,
"</div>",
"",
]
md += [f"묶음 없음 함수 {len(none)} — [목록](none.md)", ""]
if graph := self.group_graph(page, groups):
md += ["## 묶음 사이 흐름", "", graph, ""]
md.append(self.hand_refs(body, "page.md"))
(self.src / page["id"] / "index.md").write_text("\n".join(md) + "\n", encoding="utf-8")
return nav
def group_graph(self, page: dict, groups: dict[str, list[dict]]) -> str:
edges = Counter(
(self.gid[f["id"]], self.gid[c])
for fns in groups.values()
for f in fns
for c in f.get("calls", [])
if c in self.gid and self.gid[c] != self.gid[f["id"]]
)
if not edges:
return ""
titles = {g["id"]: g["title"] for g in page["groups"]} | {grouping.NONE: "묶음 없음"}
used = sorted({a for e in edges for a in e})
lines = ["```mermaid", "flowchart LR"] + [f' {g}["{titles[g]}"]' for g in used]
lines += [f" {a} -->|{n}| {b}" for (a, b), n in sorted(edges.items())]
return "\n".join(lines + ["```"])
def build(self, typedoc: bool = True) -> dict:
if self.src.exists():
shutil.rmtree(self.src)
(self.src / "py").mkdir(parents=True)
files = sorted(self.by_file)
if typedoc:
self.ts_pages = run_typedoc([f for f in files if f.endswith(".ts")], self.out)
self.py_refs = write_py_pages([f for f in files if f.endswith(".py")], self.src)
nav = [{"처음": "index.md"}]
home = ["# Aislo 프로그램 문서", "", "페이지 → 로직 묶음 → 함수 차례로 눌러 내려감.", ""]
for page in self.rules["pages"]:
groups = grouping.assign(self.fns, self.data["registry"], page["groups"])
self.gid.update({f["id"]: gid for gid, fns in groups.items() for f in fns})
nav.append({page["title"]: self.write_page(page, groups)})
home.append(f"- [{page['title']}]({page['id']}/index.md)")
ts_nav = [{"TypeScript": "ts/index.md"}] if (self.src / "ts" / "index.md").exists() else []
nav.append({"함수 화면": [*ts_nav, {"파이썬": "py/index.md"}]})
(self.src / "index.md").write_text("\n".join(home) + "\n", encoding="utf-8")
(self.src / ".nav.json").write_text(json.dumps(nav, ensure_ascii=False), encoding="utf-8")
return self.report()
def report(self) -> dict:
exported = [f for f in self.fns if f.get("exported")]
return {
"functions": len(self.fns),
"groups": sum(len(p["groups"]) for p in self.rules["pages"]),
"ungrouped": sum(1 for v in self.gid.values() if v == grouping.NONE),
"session_keys": len(self.data["session_keys"]),
"api": len(self.data["api"]),
"undocumented_exports": sorted(
f["id"] for f in exported if not first_line(f.get("doc"))
),
"missing_refs": self.missing_refs,
"stale_groups": self.stale,
}
def build_site(out: Path, strict: bool = True) -> None:
"""mkdocs build(--strict) — docs_dir = out/src · site_dir = out/site."""
from mkdocs.commands.build import build
from mkdocs.config import load_config
cfg = load_config(
str(DOCS / "mkdocs.yml"),
docs_dir=str(out / "src"),
site_dir=str(out / "site"),
strict=strict,
)
build(cfg)
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--data", type=Path, default=DOCS / "_data")
ap.add_argument("--out", type=Path, default=DOCS / "_build")
ap.add_argument("--site", action="store_true", help="mkdocs build --strict 까지")
ap.add_argument("--stamp", action="store_true", help="지금 함수 목록 지문을 손글에 적음")
args = ap.parse_args()
t0 = time.time()
data, rules = load_data(args.data), grouping.load_rules()
if args.stamp:
for page in rules["pages"]:
groups = grouping.assign(data["functions"], data["registry"], page["groups"])
for g in page["groups"]:
path = DOCS / page["guide"] / "groups" / f"{g['id']}.md"
if path.exists():
stamp(path, grouping.fingerprint(groups[g["id"]]))
print("지문 적음")
return
args.out.mkdir(parents=True, exist_ok=True)
out = args.out.resolve()
rep = Site(data, rules, out).build()
if args.site:
build_site(out)
summary = {k: (len(v) if isinstance(v, list) else v) for k, v in rep.items()}
print(json.dumps(summary, ensure_ascii=False), f"{time.time() - t0:.1f}s")
for ref in rep["missing_refs"]:
print("손글 함수 없음:", ref)
if __name__ == "__main__":
main()
+81
View File
@@ -0,0 +1,81 @@
"""함수 → 로직 묶음 자동 배정(PLAN 478-2).
규칙 = docs/tools/groups.yml · 위에서부터 첫 맞음:
registry — 계산 등록부 id 무늬 → 그 줄의 ts · pair 함수
files — 파일 이름(확장자 뺀) 무늬
어디에도 안 맞는 함수 = 「묶음 없음」(None).
"""
from __future__ import annotations
import hashlib
from fnmatch import fnmatchcase
from pathlib import Path, PurePosixPath
import yaml
TOOLS = Path(__file__).resolve().parent
NONE = "none"
def load_rules(path: Path = TOOLS / "groups.yml") -> dict:
return yaml.safe_load(path.read_text(encoding="utf-8"))
def stem(file: str) -> str:
return PurePosixPath(file.replace("\\", "/")).stem
def registry_ref_id(ref: str | None) -> str | None:
"""등록부 칸 `<경로>#<함수>` → 함수 id."""
if not ref or "#" not in ref:
return None
lang = "py" if ref.split("#", 1)[0].endswith(".py") else "ts"
return f"{lang}:{ref}"
def registry_owners(registry: list[dict], groups: list[dict]) -> dict[str, str]:
"""등록부로 정해지는 함수 id → 묶음 id."""
owners: dict[str, str] = {}
for row in registry:
rid = row.get("id", "")
group = next(
(g["id"] for g in groups if any(fnmatchcase(rid, p) for p in g.get("registry", []))),
None,
)
if group is None:
continue
for key in ("ts", "pair"):
fid = registry_ref_id(row.get(key))
if fid:
owners.setdefault(fid, group)
return owners
def file_group(file: str, groups: list[dict]) -> str | None:
name = stem(file)
for g in groups:
if any(fnmatchcase(name, p) for p in g.get("files", [])):
return g["id"]
return None
def assign(
functions: list[dict], registry: list[dict], groups: list[dict]
) -> dict[str, list[dict]]:
"""묶음 id → 함수 목록(파일 · 줄 차례) · 「묶음 없음」 = NONE."""
owners = registry_owners(registry, groups)
out: dict[str, list[dict]] = {g["id"]: [] for g in groups}
out[NONE] = []
for fn in functions:
gid = owners.get(fn["id"]) or file_group(fn["file"], groups) or NONE
out[gid].append(fn)
for fns in out.values():
fns.sort(key=lambda f: (f["file"], f.get("line", 0)))
return out
def fingerprint(fns: list[dict]) -> str:
"""함수 목록 지문 — 손글에 적힌 지문과 다르면 「설명 확인 필요」."""
text = "\n".join(sorted(f["id"] for f in fns))
return hashlib.sha1(text.encode("utf-8")).hexdigest()[:10]
+218
View File
@@ -0,0 +1,218 @@
# 문서 사이트 묶음 규칙 — 페이지 · 범위 · 로직 묶음
# 함수 하나 = 묶음 하나 · 위에서부터 첫 맞음
# registry — 계산 등록부 id 무늬(그 줄의 ts · pair 함수)
# files — 파일 이름 무늬(확장자 뺀 이름 · fnmatch)
# 어디에도 안 맞음 = 「묶음 없음」
pages:
- id: B05_B06
title: B05 · B06 종단 · 횡단 설계
guide: guide/B05_B06
scope:
- B05_Profile/
- B06_Section/
- A00_Common/design/
- common_util/
groups:
- id: recommend
title: 추천 노선
files:
- "*RouteInitial*"
- B05_Profile_UI_RouteEdit_Initial*
- B05_Profile_UI_RouteEdit_Recommend*
- B05_Profile_UI_Panel_Recommend
- B05_Profile_Engine_InitialProfile
- B05_Profile_Engine_RidgeValley*
- B05_Profile_Engine_Skeleton
- B05_Profile_Engine_Solver
- id: route_edit
title: 노선 편집
registry: ["route.*"]
files:
- B05_Profile_UI_RouteEdit*
- B05_Profile_RouteEdit_*
- B05_Profile_Route_NodeEdits
- B05_Profile_ContourLock
- design_plan_peek
- common_util_route_polyline
- common_util_route_geometry
- id: route_save
title: 노선 저장 · 시설 따라감
registry: ["stations.carry", "facility.follow*", "facility.plus_follow"]
files:
- B05_Profile_Route_Save*
- B05_Profile_Router_Replan
- B05_Profile_Api_Replan
- B06_Section_Router_DesignSave_Route
- B06_Section_Route_Carry*
- B05_Profile_Pipeline_Follow
- B05_Profile_Structure_Follow
- design_calc_node_carry
- design_calc_node_follow
- common_util_route_follow
- id: stations
title: 측점
registry: ["stations.*"]
files:
- B05_Profile_Sections_Stations
- B05_Profile_Engine_Sections*
- B05_Profile_UI_Page_Stations
- B05_Profile_UI_IrregularStations
- B05_Profile_Util_Station
- B05_Profile_Plus_Stations
- B05_Profile_Api_PlusStations
- B06_Section_Station_*
- B06_Section_Swap_Bus
- B06_Section_Router_Stations
- B06_Section_UI_Missing_Stations
- B06_Section_UI_Page_Station_Swap
- design_pipeline_stations
- design_pipeline_probe_station
- common_util_station_key
- id: ground
title: 지반 표본
registry: ["ground.*"]
files:
- B05_Profile_Api_TerrainGrid
- B05_Profile_Sections_Ground
- B05_Profile_Pipeline_RouteGround
- B05_Profile_Router_Terrain
- B06_Section_Engine_SampleExtend
- B06_Section_Router_Widen
- common_util_surface_sampler
- id: earthwork
title: 토량 · 유토 배분
registry: ["earth.*", "haul.*"]
files:
- "*MassHaul*"
- "*HaulPlan*"
- B05_Profile_UI_Profile_Balance
- common_util_mass_haul*
- id: profile
title: 종단 계획고
registry: ["profile.*"]
files:
- B05_Profile_UI_Profile_*
- B05_Profile_Profile_*
- B05_Profile_Grade_*
- B05_Profile_Engine_Grade*
- B05_Profile_Pipeline_Grade
- B05_Profile_EndOffsets
- B05_Profile_UI_Panel*
- B05_Profile_PageSettings
- B05_Profile_Api_PageSettings
- design_profile*
- id: cross
title: 횡단 설계 계산
registry: ["cross.*"]
files:
- B06_Section_Cross_*
- B06_Section_Engine_*
- B06_Section_Design_Keys
- B06_Section_Router_Design
- B06_Section_Router_Design_Ranges
- B06_Section_Router_Regenerate_Options
- B06_Section_Server_Calc*
- common_util_cross_*
- id: cross_view
title: 횡단 화면 · 조정창
files:
- B06_Section_UI_Cross_*
- B06_Section_UI_Standard*
- B06_Section_UI_Section_*
- B06_Section_UI_Longitudinal
- B06_Section_UI_Adjust_Dock
- B06_Section_Label_Tiers
- B06_Section_Display_Width
- B06_Section_Session_Cells
- B06_Section_Section_Adjusts
- B06_Section_UI_Page_Design_Sync
- B06_Section_UI_Page_Station_Controls
- B06_Section_UI_Page_Span_Control
- B06_Section_UI_Page_Ford_Controls
- B06_Section_UI_Page_Link_Session
- B06_Section_UI_Page_Pipe_Options
- B06_Section_UI_Page_Patches
- id: drainage
title: 관 · 배수
registry: ["drainage.*", "facility.pipe*"]
files:
- "*Drainage*"
- B05_Profile_Api_Pipes_Draft
- B06_Section_Api_Culvert_Options
- design_pipe_ids
- design_structure_pipe*
- common_util_drainage*
- common_util_culvert*
- id: structures
title: 구조물
registry: ["facility.*"]
files:
- "*Structures*"
- "*Structure_*"
- B05_Profile_UI_Structure_Pick_Session
- B06_Section_Structure_Layouts
- design_structure*
- design_facility_lane
- common_util_structure_*
- common_util_berm*
- common_util_excavation
- id: corridor
title: 3D 길 모형
registry: ["corridor.*", "view3d.*"]
files:
- "*Corridor*"
- B05_Profile_UI_Viewer*
- design_view_profile_3d*
- id: footprint
title: 평면 발자국
registry: ["plan.*", "footprint.*"]
files:
- "*Footprint*"
- id: pipeline
title: 계산 묶음 · 가게
files:
- design_pipeline*
- design_calc*
- design_store
- B05_Profile_Pipeline_*
- B06_Section_Section_*
- B06_Section_UI_Page_Persist
- common_util_calc_runner
- id: save
title: 저장 · 확정 · 초기화 · 떠남
files:
- design_save*
- design_reset
- design_leave
- design_shared
- B06_Section_Router_DesignSave
- B06_Section_Router_Confirm
- B05_Profile_Router_Confirm
- B05_Profile_Router_Lifecycle
- common_util_initial_snapshot
- common_util_save_guard
- id: screen
title: 페이지 틀 · 보기
files:
- design_page*
- design_view*
- design_panel
- design_dock
- design_pick*
- design_selection
- design_user_pick
- design_sections
- B05_Profile_UI*
- B06_Section_UI*
- id: server
title: 서버 창구 · 저장소
files:
- "*_Router*"
- "*_Repository*"
- "*_Schema"
- "*_Api_*"
- B05_Profile_Engine*
- B05_Profile
- B06_Section
- B05_Profile_Debug
+11
View File
@@ -0,0 +1,11 @@
"""mkdocs 훅 — build.py 가 쓴 왼쪽 메뉴(<docs_dir>/.nav.json)를 얹음."""
import json
from pathlib import Path
def on_config(config):
nav = Path(config.docs_dir) / ".nav.json"
if nav.exists():
config.nav = json.loads(nav.read_text(encoding="utf-8"))
return config
+356
View File
@@ -0,0 +1,356 @@
{
"name": "aislo-docs-tools",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "aislo-docs-tools",
"dependencies": {
"ts-morph": "^28.0.0",
"typedoc": "^0.28.20",
"typedoc-plugin-markdown": "^4.14.0",
"typescript": "6.0"
}
},
"node_modules/@gerrit0/mini-shiki": {
"version": "3.23.0",
"resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.23.0.tgz",
"integrity": "sha512-bEMORlG0cqdjVyCEuU0cDQbORWX+kYCeo0kV1lbxF5bt4r7SID2l9bqsxJEM0zndaxpOUT7riCyIVEuqq/Ynxg==",
"license": "MIT",
"dependencies": {
"@shikijs/engine-oniguruma": "^3.23.0",
"@shikijs/langs": "^3.23.0",
"@shikijs/themes": "^3.23.0",
"@shikijs/types": "^3.23.0",
"@shikijs/vscode-textmate": "^10.0.2"
}
},
"node_modules/@shikijs/engine-oniguruma": {
"version": "3.23.0",
"resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.23.0.tgz",
"integrity": "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==",
"license": "MIT",
"dependencies": {
"@shikijs/types": "3.23.0",
"@shikijs/vscode-textmate": "^10.0.2"
}
},
"node_modules/@shikijs/langs": {
"version": "3.23.0",
"resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.23.0.tgz",
"integrity": "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==",
"license": "MIT",
"dependencies": {
"@shikijs/types": "3.23.0"
}
},
"node_modules/@shikijs/themes": {
"version": "3.23.0",
"resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.23.0.tgz",
"integrity": "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==",
"license": "MIT",
"dependencies": {
"@shikijs/types": "3.23.0"
}
},
"node_modules/@shikijs/types": {
"version": "3.23.0",
"resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.23.0.tgz",
"integrity": "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==",
"license": "MIT",
"dependencies": {
"@shikijs/vscode-textmate": "^10.0.2",
"@types/hast": "^3.0.4"
}
},
"node_modules/@shikijs/vscode-textmate": {
"version": "10.0.2",
"resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz",
"integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==",
"license": "MIT"
},
"node_modules/@ts-morph/common": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.29.0.tgz",
"integrity": "sha512-35oUmphHbJvQ/+UTwFNme/t2p3FoKiGJ5auTjjpNTop2dyREspirjMy82PLSC1pnDJ8ah1GU98hwpVt64YXQsg==",
"license": "MIT",
"dependencies": {
"minimatch": "^10.0.1",
"path-browserify": "^1.0.1",
"tinyglobby": "^0.2.14"
}
},
"node_modules/@types/hast": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz",
"integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==",
"license": "MIT",
"dependencies": {
"@types/unist": "*"
}
},
"node_modules/@types/unist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
"license": "MIT"
},
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"license": "Python-2.0"
},
"node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
"license": "MIT",
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/brace-expansion": {
"version": "5.0.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz",
"integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==",
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "20 || >=22"
}
},
"node_modules/code-block-writer": {
"version": "13.0.3",
"resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz",
"integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==",
"license": "MIT"
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"license": "MIT",
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/linkify-it": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz",
"integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/puzrin"
},
{
"type": "github",
"url": "https://github.com/sponsors/markdown-it"
}
],
"license": "MIT",
"dependencies": {
"uc.micro": "^2.0.0"
}
},
"node_modules/lunr": {
"version": "2.3.9",
"resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz",
"integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==",
"license": "MIT"
},
"node_modules/markdown-it": {
"version": "14.3.3",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.3.tgz",
"integrity": "sha512-L//tDAkjTjJCU6pBUSBTgs3Kg4InVaZuUZog/b6V12UKhcDQ7S78877ibbCanRExwPuYP1CZD7Gd2dxSy1GXRg==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/puzrin"
},
{
"type": "github",
"url": "https://github.com/sponsors/markdown-it"
}
],
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1",
"entities": "^4.5.0",
"linkify-it": "^5.0.2",
"mdurl": "^2.0.0",
"punycode.js": "^2.3.1",
"uc.micro": "^2.1.0"
},
"bin": {
"markdown-it": "bin/markdown-it.mjs"
}
},
"node_modules/mdurl": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz",
"integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==",
"license": "MIT"
},
"node_modules/minimatch": {
"version": "10.2.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
"integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==",
"license": "BlueOak-1.0.0",
"dependencies": {
"brace-expansion": "^5.0.8"
},
"engines": {
"node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/path-browserify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
"license": "MIT"
},
"node_modules/picomatch": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
"integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/punycode.js": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/tinyglobby": {
"version": "0.2.17",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.4"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/ts-morph": {
"version": "28.0.0",
"resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-28.0.0.tgz",
"integrity": "sha512-Wp3tnZ2bzwxyTZMtgWVzXDfm7lB1Drz+y9DmmYH/L702PQhPyVrp3pkou3yIz4qjS14GY9kcpmLiOOMvl8oG1g==",
"license": "MIT",
"dependencies": {
"@ts-morph/common": "~0.29.0",
"code-block-writer": "^13.0.3"
}
},
"node_modules/typedoc": {
"version": "0.28.20",
"resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.20.tgz",
"integrity": "sha512-uSKqkh8Cr48vllnEy+jdaAgOeR6Y+QCBW7usgUsKj7gJEfR7stw9U/fE49LBnj2tPRKPY0c0EBJSWe9Appmplg==",
"license": "Apache-2.0",
"dependencies": {
"@gerrit0/mini-shiki": "^3.23.0",
"lunr": "^2.3.9",
"markdown-it": "^14.3.0",
"minimatch": "^10.2.5",
"yaml": "^2.9.0"
},
"bin": {
"typedoc": "bin/typedoc"
},
"engines": {
"node": ">= 18",
"pnpm": ">= 10"
},
"peerDependencies": {
"typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x || 6.0.x"
}
},
"node_modules/typedoc-plugin-markdown": {
"version": "4.14.0",
"resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-4.14.0.tgz",
"integrity": "sha512-Y2a3oDV9ekrbkk6RBSGH8GyNkh0xIdemIOOTr3tiRN/33nvFGStOs9nw+6Q84gF1XFkcVZrgM4Ckcsf6e/YpKQ==",
"license": "MIT",
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"typedoc": "0.28.x"
}
},
"node_modules/typescript": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/uc.micro": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
"license": "MIT"
},
"node_modules/yaml": {
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz",
"integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==",
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
},
"funding": {
"url": "https://github.com/sponsors/eemeli"
}
}
}
}
+1
View File
@@ -0,0 +1 @@
{"name":"aislo-docs-tools","private":true,"type":"module","dependencies":{"ts-morph":"^28.0.0","typedoc":"^0.28.20","typedoc-plugin-markdown":"^4.14.0","typescript":"6.0"}}
+4
View File
@@ -0,0 +1,4 @@
mkdocs>=1.6
mkdocs-material>=9.5
mkdocstrings[python]>=0.26
pyyaml>=6