docs: 계약 없이 남아 있던 두 프로젝트에 주제를 갈라 계약을 세운다

keycloak-session-store 와 TechLog 는 SSOT 만 있고 분해 계약이 없었다. 두 프로젝트
모두 후보를 처분과 함께 남기고 PROMOTE 만 주제로 올린다.

  keycloak-session-store  주제 6 · 글감 33 · 후보 42 (제외 9)
  TechLog                 주제 13 · 글감 56 · 후보 91 (제외 35)

TechLog 는 저장소가 셋이라 sourceRepository 를 목록으로 적는다. 설계 패키지는 이
기계에 체크아웃이 없고 반입한 쪽의 MANIFEST.sha256 이 세 계약의 원본 리비전을
고정하므로 그것을 리비전으로 적었다. 검사기 둘이 그 모양을 읽게 고친다 —
verify-tech-log-tree.py 는 목록의 항목마다 path·revision·verified 를 보고,
check_evidence.mjs 는 체크아웃이면 git 으로, 매니페스트면 그 파일이 리비전을
적고 있는지로 대조한다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-09-07 13:30:12 +09:00
co-authored by Claude Opus 5
parent 4d50bb939a
commit a0ca2bb72a
4 changed files with 4028 additions and 34 deletions
@@ -107,17 +107,28 @@ for (const topicDir of readdirSync(studio)) {
// 4. (--repo) 저장소가 실재하고 리비전이 맞는가
if (withRepo) {
const repo = tree.sourceRepository || {};
if (!repo.path) findings.push(["tech-log-tree.json", "sourceRepository.path 가 없다", ""]);
else if (!existsSync(repo.path)) findings.push(["tech-log-tree.json", "저장소 경로가 없다", repo.path]);
else {
// 저장소가 여럿인 프로젝트는 목록으로 적는다
const declared = tree.sourceRepository || {};
const repos = Array.isArray(declared) ? declared : [declared];
for (const repo of repos) {
const name = repo.name || project;
if (!repo.path) { findings.push(["tech-log-tree.json", "sourceRepository.path 가 없다", name]); continue; }
if (!existsSync(repo.path)) { findings.push(["tech-log-tree.json", "저장소 경로가 없다", `${name}${repo.path}`]); continue; }
// 갈래가 여럿이면 revisions 로 적는다. 둘 다 없으면 verify-tech-log-tree.py 가 warn 을 낸다
const revs = repo.revision ? { revision: repo.revision } : (repo.revisions || {});
// 체크아웃이 없는 저장소는 반입한 쪽의 매니페스트가 리비전을 고정한다. 그럴 때는
// path 가 그 파일이고, git 대신 그 파일이 리비전을 적고 있는지 본다
const isCheckout = statSync(repo.path).isDirectory();
const manifest = isCheckout ? "" : readFileSync(repo.path, "utf8");
for (const [label, rev] of Object.entries(revs)) {
if (isCheckout) {
try {
execSync(`git -C ${JSON.stringify(repo.path)} cat-file -e ${rev}^{commit}`, { stdio: "ignore" });
} catch {
findings.push(["tech-log-tree.json", "그 리비전이 저장소에 없다", `${label} = ${rev}`]);
findings.push(["tech-log-tree.json", "그 리비전이 저장소에 없다", `${name} · ${label} = ${rev}`]);
}
} else if (!manifest.includes(rev)) {
findings.push(["tech-log-tree.json", "매니페스트가 그 리비전을 적고 있지 않다", `${name} · ${label} = ${rev}`]);
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+14 -7
View File
@@ -142,20 +142,27 @@ def verify(project: str) -> Report:
"— 칸마다 error 를 내지 않는 대신 미채택 자체를 여기서 한 번 센다")
# ── 분석한 저장소 ──────────────────────────────────────────────
# 저장소가 여럿인 프로젝트는 목록으로 적는다. 하나면 객체 하나로 적어도 된다
repo = index.get("sourceRepository") or {}
repos = repo if isinstance(repo, list) else [repo]
if has_contract:
if not repo.get("path"):
if not repos:
rep.error("sourceRepository.path 가 없다",
f"{project}: 어느 저장소를 읽고 쓴 글인지 적혀 있지 않다")
elif not os.path.isdir(repo["path"]) and "://" not in repo["path"]:
rep.warn("sourceRepository.path 가 이 기계에 없다", repo["path"])
for r in repos:
name = r.get("name") or project
if not r.get("path"):
rep.error("sourceRepository.path 가 없다",
f"{project}: 어느 저장소를 읽고 쓴 글인지 적혀 있지 않다")
elif not os.path.exists(r["path"]) and "://" not in r["path"]:
rep.warn("sourceRepository.path 가 이 기계에 없다", f"{name}: {r['path']}")
# 갈래가 여럿이면 단일 커밋으로 표현되지 않는다. revisions 로 적는다
if not repo.get("revision") and not repo.get("revisions"):
if not r.get("revision") and not r.get("revisions"):
rep.warn("sourceRepository 에 리비전이 없다",
f"{project}: 문서가 서술한 상태의 커밋을 고정하지 않았다")
elif not repo.get("verified"):
f"{project}/{name}: 문서가 서술한 상태의 커밋을 고정하지 않았다")
elif not r.get("verified"):
rep.warn("sourceRepository.verified 가 없다",
f"{project}: 그 리비전이 맞다고 판단한 근거가 없다")
f"{project}/{name}: 그 리비전이 맞다고 판단한 근거가 없다")
# ── 후보를 찾는 범위 ───────────────────────────────────────────
scope = index.get("candidateScope") or {}