Commit Graph
17 Commits
Author SHA1 Message Date
DongHyeonkaandClaude Opus 5 ab59130196 chore: 이전 세션이 남긴 변경을 커밋한다
이번 파이프라인 작업과 무관하게 작업 트리에 남아 있던 것을 그대로 올린다.
사용자가 「전부 커밋」으로 정했고, 이번 작업과 섞이지 않게 커밋만 나눴다.

대부분은 clean-architecture-backend-template 의 그림 정본 재배치다 —
final/assets/diagrams/<이름>/ 에 있던 것이 CLAUDE.md 가 적은 배치인
final/assets/<이름>/ 로 옮겨졌고 .techviz/<이름>/ 이 함께 들어왔다.
삽입 줄의 대부분(3.15M)이 그 .techviz context.json 이다.

그 밖에 ca-tmpl·document-haness 의 정리, .claude/agents/ 열한 개,
writing-practitioner-guides 스킬, .playwright-mcp 세션 산출물,
scripts/check-ssot-facts.py 와 그 시험이 들어 있다.

이 커밋의 내용은 내가 만든 것이 아니라 이전 세션이 남긴 것이고 검증하지 않았다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 11:02:02 +09:00
DongHyeonkaandClaude Opus 5 d473609e0a fix(studio-save): 응답 껍데기를 벗기는 자리를 코드가 갖고, 계획마다 새 키가 나게 한다
① GET /documents/{id} 는 문서를 data.document 아래에 둔다(studio-v1.yaml:1831). A 가
   B-011 에서 data 를 문서로 보고 견줘 칸이 전부 「다르다」로 나왔다 — 벗기는 자리를
   코드가 갖고 있지 않아서 난 일이라 unwrap_document 를 둔다. 모양이 다르면 조용히
   넘기지 않고 거절한다. 잘못된 층을 견주는 것보다 멈추는 편이 낫다.

   그리고 updatedAt 을 서버가 붙이는 칸으로 옮겼다. 매 저장마다 「서버가 더 줬다」로
   나오면 그 칸을 아무도 안 읽게 된다. variantIds 는 안 옮겼다 — 계약이 입력으로 받는
   칸인데 이 어댑터가 안 보내므로 보여야 한다.

② CR-007 때문에 같은 키를 두 번 못 쓴다. 런 식별자를 계획 파일에 적고 키에 섞는다.
   같은 계획을 두 번 보내면 같은 키, 계획을 새로 만들면 다른 키다. 보낼 때 만들면
   재시도가 곧 중복 생성이 되므로 만드는 자리는 계획을 짜는 순간 하나뿐이다.
   --run-id 를 안 주면 예전 키 모양 그대로라 옛 계획을 안 깬다.

python3 -m unittest discover -s scripts/tests — Ran 291 · OK (skipped=13)
PIPELINE CONTRACT: PASS

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-11 13:18:30 +09:00
DongHyeonkaandClaude Opus 5 1be1c8ea6d fix(studio-save): 항목 id 를 다시 매기는지는 종류마다 다르다
A 가 B-011 운영 실행에서 봤다. PROJECT_DECISION 은 보낸 uuid5 여섯이 글자 그대로
돌아온다. QUESTION 은 서버가 uuid4 로 다시 발급한다(C · V-009 열 번째). 둘 다
OrderedText 배열인데 서버가 다르게 다룬다.

한 벌로 묶어 빼면 DECISION 에서 볼 수 있는 것을 안 보게 된다 — 서버가 언젠가
DECISION 도 재발급하기 시작해도 아무도 모른다. 그래서 종류로 갈랐다.

  QUESTION          다시 매긴다 → id 를 뺀다 (쟀다)
  PROJECT_DECISION  다시 안 매긴다 → 그대로 본다 (쟀다)
  그 밖              안 쟀다 → 빼지 않고 그대로 보고, 안 쟀다는 것을 값에 적는다

안 잰 채로 빼면 「안 봐도 되는 것」으로 굳는다. 「못 보는 것」과 「안 봐도 되는 것」은
다르다 — itemIdBehaviourUnmeasured 가 어느 칸을 왜 그대로 견줬는지 적는다.

python3 -m unittest discover -s scripts/tests — Ran 286 · OK (skipped=13)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-11 11:19:55 +09:00
DongHyeonkaandClaude Opus 5 325b6008ab fix(studio-save): 선택지 절의 세 가지 모양을 전부 읽는다
A 가 전수로 재서 알려 줬다. 내가 목록형 기록 하나를 보고 「이미 잰 값은 안 바뀐다」로
일반화한 것이 틀렸다 — 문단형인 openquestion-* 계열이 다섯 칸을 통째로 잃고 있었다.

`## 선택지` 를 쓰는 모양이 셋이다. `### N. 제목` · `**굵은 한 줄**` · 표시 없이 첫 줄이
제목인 문단. 첫째만 읽고 있었고, `_ordered` 의 「글이 있는데 항목 0 개면 거절」이 이 칸에는
안 걸려서 읽지도 않고 막지도 않는 상태였다.

전수로 다시 쟀다 (0d58881 대 지금):

  배열 칸이 있는 기록 150건 · 값이 달라진 것 83건 · 거절 0건
  종류별 — decision 29 · question 26 · reference 28

고친 뒤에도 0 인 칸이 24건 남는데 전부 REFERENCE 의 rules 다. 그것은 다음 배치 16번이고
설계 조건(「없는 규칙」과 「다른 모양으로 쓴 규칙」을 가른다)이 붙어 있어 안 건드렸다.

options 의 거절 가지는 이제 안 닿는다 — 세 번째 모양이 글이 있으면 언제나 항목을 하나는
만든다. MAX_KEY_LENGTH 와 같은 자리라 지우지 않고 그렇게 주석에 적었다.

python3 -m unittest discover -s scripts/tests — Ran 284 · OK (skipped=13)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-11 11:11:17 +09:00
DongHyeonkaandClaude Opus 5 252f14c88e fix(studio-save): 양쪽이 빈 칸을 「같다」에 섞지 않는다 (C 의 B-006 ② 수정안)
C 가 V-009 에서 걸렸다 — P-QUESTION-01 의 「되읽기 차이 0」이 배열 다섯이 전부 비어
견줄 것이 없어서 나온 값이었다. same: true 가 「13칸이 다 맞았다」로 읽히는데 실제로는
찬 칸만 맞은 것이다. R14 와 같은 모양이 대조기 안에 한 번 더 있었다.

- _is_empty 로 양쪽이 빈 칸을 갈라 emptyBoth 로 낸다. 한쪽만 비면 차이다
- comparedFields 에서도 빠지고 comparedCount 가 실제로 견준 수를 낸다
- 사람이 보는 한 줄에 「N칸 중 M칸을 견줬다. K칸은 양쪽이 비어 견줄 것이 없었다」

C 의 수정안은 comparedFields 를 개수로 바꿨는데 목록을 유지하고 개수를 따로 뒀다 —
이미 목록으로 읽는 회귀가 있고, 어느 칸을 못 봤는지는 이름이 있어야 안다.

C-B006 §4 의 대조를 돌렸다. 손으로 센 찬 칸과 도구 출력이 맞는다.

  CONCEPT   6 / 6      REFERENCE 10 / 10      QUESTION 11 / 11

묶음은 B 가 읽을 수 없어(계약 §3.2) 같은 종류의 기록으로 셌다.

python3 -m unittest discover -s scripts/tests — Ran 282 · OK (skipped=13)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-11 11:06:12 +09:00
DongHyeonkaandClaude Opus 5 f765a79d6f fix(studio-save): PROJECT_DECISION 의 칸을 계약에 맞추고 지울 수 있게 만든다
B-010 §4.1 에서 찾고 안 고친 것이다. 그때 안 고친 이유는 예산이 아니라 측정이었고,
이번에는 그 런에서 잰다.

- 결정문→statement · 영향→consequences(OrderedText 배열) · 판단 이유→rationale
- 근거→basis 를 뺐다. ProjectDecisionInput 에 그 칸이 없고 unevaluatedProperties:
  false 라 보내면 거절된다. 기록의 ## 근거 절은 그대로 둔다
- decisionStatus·decidedOn 을 frontmatter 에서 읽는다. enum 밖의 값은 지어내지 않고
  거절한다 — 운영에 초안을 만들어 놓고 422 를 받는 것보다 낫다

그리고 절의 모양이 둘이었다. 사실·가정은 `- ` 목록이고 DECISION 의 영향은 빈 줄로 나뉜
문단이다. 목록만 읽어서 문단으로 쓴 절이 조용히 [] 가 되고 있었다 — minItems 가 없어
그대로 저장되고 내용만 사라진다. 목록을 먼저 보고 없으면 문단으로 나누며, 글이 있는데
항목이 0 개면 거절한다. QUESTION·REFERENCE 의 이미 잰 값은 안 바뀐다(목록이라 첫 갈래에서
끝난다).

--harness-test 의 PROJECT_DECISION 거절은 우회하지 않고 값을 받게 했다. --project-id 는
사람이 Studio 목록에서 읽은 uuid 다 — 어댑터가 이름을 uuid 로 바꾸지 않는다. 없으면
여전히 거절한다. projectId 는 [string, "null"] 이고 계약이 저장 시점에는 강제하지 않으니
null 로도 만들어지고, 만들어지면 지울 수 없다. 되읽기가 그 값을 확인한다.

python3 -m unittest discover -s scripts/tests — Ran 278 · OK (skipped=13)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-11 11:03:03 +09:00
DongHyeonkaandClaude Opus 5 0d588814cc fix(studio-save): CONCEPT·REFERENCE 의 칸이 계약과 달랐다
C 가 V-009 에서 운영에 직접 걸어 찾았다. QUESTION 에서 고친 것과 같은 결함족이다.

① CONCEPT — basisVersion 이 required 인데(studio-v1.yaml:979) 어댑터가 절만 봤다.
   값은 frontmatter 에 있다. 상한 120자를 넘으면 조용히 자르지 않고 거절한다.

② REFERENCE — 셋이 한꺼번에 틀렸다(studio-v1.yaml:955-963).
   이름: appliesWhen 이 아니라 applyWhen 이다 — nextValidation 과 같은 자리다
   모양: rules 는 ReferenceRule 배열, applyWhen·exceptions·examples 는 OrderedText
         배열이다. purpose 만 문자열이다
   없는 칸: verifiedOn 이 required 인데 아예 없었다. 기록이 안 적었으면 null 로 둔다

③ 되읽기 비교가 항목 id 를 견주고 있었다. 보내는 것은 uuid5 이고 서버는 저장하면서
   uuid4 를 새로 발급한다 — 그대로 두면 모든 QUESTION·REFERENCE 저장이 「차이 있음」이
   된다. 칸 전체를 빼지 않고 id 만 뺐다. text·title·body·order 는 계속 대조하므로
   항목이 빠지거나 순서가 바뀌는 것은 여전히 걸린다. 무엇을 왜 안 보는지는
   itemIdsNotCompared 에 값으로 적는다.

python3 -m unittest discover -s scripts/tests — Ran 273 · OK (skipped=13)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-11 09:35:19 +09:00
DongHyeonkaandClaude Opus 5 0f753c3be5 fix: TechLog 리비전 셋을 매니페스트에 맞추고, 「경고 0건」을 두 가지로 가른다
부채 ④. TechLog 의 tech-log-design-package 가 ca1bbfe·1aae8dc·ffa088b 를 적고 있었는데
매니페스트는 그 뒤 다시 만들어져 세 파일 모두를 tech-log-frontend @ 0d4d1e5 로 적는다.
옛 셋은 tech-log-frontend 에 없다 — 옛 매니페스트가 안 남아 그 사이 계약 내용이 바뀌었는지는
대조할 수 없고, 그 사실을 verified 에 적었다. 기록의 「검증 환경」이 적은 리비전은 그때 잰
조건이라 고치지 않는다. 파일 sha256 셋을 함께 적어 리비전 문자열이 아니라 내용에 못박는다.

  check_evidence --repo TechLog     exit 0 「문제 없음」 (3 → 0)
  review-package.py TechLog         exit 0 · 관문 10 전부 exit 0

R14. 이 묶음의 막는 경고는 전부 편집 전후 보존 비교에서 나오고(review-package.py 의
_warn 자리 둘이 모두 if preservation.get("available") 안이다) 그 비교는 --before 를 줘야
돈다. 빼면 검토 관문이 아무 줄도 안 남기고 통과한다.

- --before 를 필수로 만들지 않는다. 새로 쓴 기록엔 윤문 전 사본이 없어 첫 기록이 막힌다.
  없다는 것을 적고 출력한다
- preservation.reason 으로 두 원인을 가른다 — NO_BEFORE · CHECKER_UNREADABLE
- 묶음이 자기 입력을 적는다. 줬으면 before·beforeSha256, 안 줬으면 명시적 null.
  빠진 칸과 null 은 다르다
- 종료 코드는 양쪽 다 0 이다. 새 관문이 아니라 읽는 계약이라 갈리는 것은 문구다.
  _review_gate 의 stderr 와 계획 한 줄 요약 둘 다 가른다
- B-B004c-plans/verdict.json 을 지웠다. 옛 묶음 53ce6a5b… 에 묶여 있는데 지금 계획은
  0beb5420… 이라 읽히지 않는다 — 검토를 받은 것처럼 보이는 파일이 남는다

python3 -m unittest discover -s scripts/tests — Ran 266 · OK (skipped=13)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-11 09:18:45 +09:00
DongHyeonkaandClaude Opus 5 d2855d1e6c fix(studio-save): 정상 입력이 저장까지 못 가던 이유 셋을 고친다
C 가 운영 Studio 에 직접 걸어 찾았다. 셋 다 서버가 아니라 이 어댑터가 만든 요청의 문제다.

① 422 IDEMPOTENT_REQUEST_MISMATCH 가 어디에도 안 적혀 있었다. 코드만 보면 본문이
   틀렸다고 읽는다 — 서버 문구도 무슨 일이 있었는지만 말한다. SERVER_ERRORS 표를
   만들어 계획의 expect 에 실었다. 표에 없는 코드는 Refused 다.
   키 설계는 안 건드렸다. 본문 해시를 넣으면 「재시도가 문서를 둘 만들지 않는다」가
   깨진다 — idempotency_key 주석에 이 설계가 막는 것을 적어 두었다.

② --harness-test 가 제목만 바꾸고 slug 는 원본 그대로였다. slug 에 유일성 제약이
   있어 409 DB_UNIQUE_VIOLATION 이다. 시험 초안은 반드시 원본에서 나오므로 이 충돌은
   필연이다. 접두사는 두 번 붙지 않고, 길이 상한을 넘기면 자르지 않고 거절한다.

③ QUESTION 의 칸이 문자열이 아니었다. facts·assumptions·unknowns·constraints 는
   OrderedText 배열, options 는 QuestionOption 배열, 마지막 칸 이름은 nextValidation
   이다(studio-v1.yaml:1013-1040). id 는 uuid5 로 만든다 — 난수면 같은 기록을 다시
   계획할 때 본문이 달라져 저장 멱등 키가 흔들린다.

곁다리로 relations 가 [] 이지 None 이 아닌 이유를 주석에 남기고, 정리 단계의 값을
계약이 적은 400 이 아니라 C 가 관측한 422 로 고쳤다. 404 는 지워졌다는 뜻이 아니다 —
종류를 어긋나게 보내면 404 뒤 GET 이 200 이다.

python3 -m unittest discover -s scripts/tests — Ran 257 · OK (skipped=13)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-11 09:07:38 +09:00
DongHyeonkaandClaude Opus 5 96d89ec6da fix(studio-save): 정리 단계가 본문 없이 나가고 종류를 안 봤다
DELETE 가 본문을 요구한다. requestBody: required: true 이고 컨트롤러가 @RequestBody
ExpectedVersionRequest 를 받는다. 본문 없이 보내면 400 이고 초안이 남는다. expectedVersion
은 verify 가 읽은 값이어야 한다 — 만든 뒤 한 번 더 저장하므로 만들 때 version 이 아니다.

경로가 종류를 안 보고 무조건 cases 로 나갔다. 서버는 종류를 조회 조건에 넣고 그 이유를
코드에 적어 두었다 — 그렇게 하지 않으면 Case 경로로 Reference 를 지울 수 있게 되기
때문이다. 경로가 종류와 어긋나면 DOCUMENT_NOT_FOUND 가 나고 초안이 남는다.

시험 초안을 CASE 로 쓴 판단은 맞았지만 코드가 그 판단에 기대고 있었다. 종류별 경로를
표로 만들고, Decision 처럼 프로젝트 id 가 필요한 것과 모르는 종류는 거절한다 — 조용히
받으면 못 지우는 초안이 운영에 남는다.

CSRF 헤더 이름과 404 의 뜻도 계획에 적었다. X-XSRF-TOKEN 이면 403 이다.

이 고침은 계약에 맞춘 것이지 걸어 본 것이 아니다. 계획대로 보내 본 사람은 아직 없다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-11 00:36:29 +09:00
DongHyeonkaandClaude Opus 5 4e9226a57d fix(studio-save): 확인 없이 옮긴 문장을 고치고, 안 닿는 가지에 유효 범위를 적는다
「Decision 은 계약에 삭제 경로가 없다」고 적었는데 틀렸다. ManagementDocumentController
:123 에 DELETE /v1/studio/projects/{id}/decisions/{decisionId} 가 있다. 스킬의 문장을
확인 없이 옮겼다. 시험 초안을 CASE 로 고른 이유는 삭제 경로 때문이 아니라 그 기록이 이
배치가 만든 것이라 남의 것이 아니어서다.

멱등 키 길이 검사는 지금 정책에서 안 닿는다. 키가 studio-{op}-{32자}(+{32자})라 길이가
사실상 고정이고 저장 키가 77자다. 죽은 코드가 아니라 키 정책이 바뀌면 살아나는 방어선이라
지우지 않고, 언제 살아나는지를 주석에 적었다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-10 19:16:55 +09:00
DongHyeonkaandClaude Opus 5 c66eee6cb2 feat(studio-save): 시험 초안 계획에 두 번째 저장과 정리 단계를 넣는다
만들기만 하면 이 런에서 expectedVersion·VERSION_CONFLICT 경로가 한 번도 안 돈다. 새로
만든 뒤 한 번 더 저장해 낙관적 락을 태운다. expectedVersion 은 만들기 응답이 준 version
이고, 모른 채 보내면 서버가 0 으로 채워 늘 충돌한다.

만든 시험 초안을 그 자리에서 지운다. DELETE /api/v1/studio/cases/{id} 가 실재하는 것을
소스와 openapi 에서 확인했고 게시 가드에 안 걸리는 것도 확인했다. 시험 초안의 종류를
CASE 로 고른 이유가 그것이다 — Decision 은 계약에 삭제 경로가 없다.

정리 단계는 --harness-test 일 때만 낸다. 진짜 기록에는 삭제 요청을 만들지 않는다.

삭제가 409 를 낼 수 있다 — 공개된 기록이거나 참조하는 곳이 있는 경우다. 둘 다 사람이
화면에서 처리하고, 계획에 그렇게 적었다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-10 19:15:07 +09:00
DongHyeonkaandClaude Opus 5 f16785b93e feat(studio-save): 게시된 기록을 코드가 저장 대상에서 뺀다
사용자가 studio:publish 권한 분리를 유예하고 write 권한을 가진 실계정으로 저장하기로
했다. 설계의 명시적 예외이고, 설계가 프롬프트 통제를 인정하지 않는 이유가 여기 그대로
적용된다 — 「이미 게시된 게시물은 건드리지 않는다」를 문장이 아니라 코드로 만든다.

한 번이라도 게시한 문서는 게시를 취소해도 삭제가 409 로 거절된다. public 이 찼거나
status 가 게시 중이면 저장 대상에서 뺀다. 저장소에서 두 표시가 같은 17건을 가리키지만
하나만 차 있어도 게시로 본다 — 한쪽이 뒤늦게 채워지는 경우를 놓치지 않는다.

frontmatter 는 저장소가 아는 것이지 서버가 아는 것이 아니다. 그래서 계획의 첫 단계가
서버에 게시 상태를 묻고, PUBLISHED 면 저장 단계로 넘어가지 않는다. 새 문서를 만드는
계획에는 그 단계가 없다 — 아직 없는 문서에는 물어볼 게시 상태가 없다.

시험 초안에 [HARNESS-TEST] 접두사를 붙일 수 있게 했다. 나중에 사람이 눈으로 가린다.

무인 저장은 그대로 꺼져 있다. CR-001 이 유예됐다는 것은 무인 저장을 켠다는 뜻이 아니다 —
사람이 보는 앞에서 저장하는 것과 사람 없이 저장하는 것은 다른 이야기다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-10 19:11:25 +09:00
DongHyeonkaandClaude Opus 5 16cbe141a0 fix: 고친 자리에 같은 버그를 다시 넣었고, 게이트가 관측 창을 덮었다
한글 수사의 뒤 경계가 세는 말 다음의 조사를 낱말의 일부로 봤다. 「다섯 개다」·「다섯 건을」·
「여섯 장이」가 전부 안 걸린다. 아라비아 숫자 쪽에서 같은 이유로 「500행」·「5개다」를
놓쳤던 것을 고쳤는데, 그 고침을 한글로 옮기면서 다시 넣었다. 회귀가 초록이었던 건 시험
문구가 전부 조사 없이 끝나서다.

뒤 경계를 풀었더니 채택된 편집 둘이 새로 막혔다. 「여덟 자리 → 여덟 곳」이다. 숫자가 안
바뀌었고 세는 말이 바뀌었다 — spatial-metaphor 를 고치는 정상 편집이다. 그래서 잡는 것을
수사로 좁히고 세는 말은 문맥으로만 본다. 대안도 긴 것부터로 정렬했다.

대조군을 주변이 바뀌는 쌍으로 다시 짰다. 같은 문자열은 어떤 검사기든 조용해서 대조가 되지
않는다. 그 대조군이 없었으면 위 오탐을 못 봤다.

그리고 저장 게이트가 그림 붙은 기록을 전부 막고 있었다. 저장소의 그림에 종류 표시가 하나도
없어서 그림이 붙으면 무조건 경고가 하나 붙는다. 게이트가 틀린 게 아니라 그 경고가 어느
기록에 대한 정보도 아니다 — 모든 기록에 걸리는 경고는 어느 기록에 대해서도 아무 말을 하지
않는다. 저장소 전체의 미비는 세고 보고하되 저장을 막지 않는다. 조용히 빼지도 않는다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-10 16:15:04 +09:00
DongHyeonkaandClaude Opus 5 20c8535169 feat(studio-save): 검토로 보낸다는 것은 저장이 막힌다는 뜻이어야 한다
시도 1 이 「8건이 PASS 로 나가지 않는다」를 세웠는데 코드 3건에 대해서만 성립했다. 경고는
실렸는데 그것을 읽고 막는 코드가 없었다. 검토로 라우팅한다는 것이 실제로는 경고를 붙여서
통과시키는 것이었다. 막지 않으면 라우팅이 아니라 주석이다.

경고가 있으면 항목마다 판정을 받고 전부 PASS 일 때만 저장이 나간다. 판정 파일이 없으면
거절한다 — 검토를 안 받은 것과 검토가 통과시킨 것은 같은 결과일 수 없다. 판정이 안 붙은
경고가 있어도 거절한다 — 빠뜨린 것과 통과시킨 것은 다르다.

FAIL 과 UNKNOWN 은 둘 다 막되 문구가 갈린다. 근거가 모자라 판정을 못 한 것과 근거를 읽고
틀렸다고 본 것은 다음에 할 일이 다르다.

판정이 어느 경고에 붙은 것인지 정하려고 경고에 키를 붙였고, 그 키를 읽는 코드를 같은
변경에 넣었다. 내용이 바뀌면 키도 바뀌어 옛 판정이 다른 경고에 붙지 않는다.

경고가 없으면 판정 파일 없이 그대로 나간다. 정상은 이 게이트에 안 걸린다 — 채택 편집
100쌍에서 91쌍이 판정 없이 나가고 9쌍이 판정을 받아야 한다. 판정을 받아야 한다는 것은
막힌다는 뜻이 아니다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-10 15:53:26 +09:00
DongHyeonkaandClaude Opus 5 a5215eda31 feat(studio-save): 그림 쪽에도 다리를 놓는다
문장 쪽에는 놨는데 그림 쪽에는 안 놨다. 화살표를 뒤집어도 관문이 전부 0 이고 경고도 비어
자동 통과했다.

설계가 「SVG 의 스크립트·외부 리소스는 저장 단계의 허용 정책으로 제한한다」고 적었는데 그
정책이 코드로 어디에도 없었다. 화살표 방향은 사람이 봐야 하지만 스크립트가 들어 있는지는
문자열로 판정된다 — 그림 쪽에서 기계가 할 수 있는 유일한 일이라 error 로 막는다.
script·foreignObject·이벤트 처리기·바깥 href·바깥 리소스·@import 여섯이다.
#fragment 와 data: 는 바깥으로 안 나가므로 막지 않는다.

그림이 검토 뒤에 바뀌면 경고로 올린다. 무엇이 바뀌었는지는 기계가 못 말하지만 바뀌었으니
보라는 말할 수 있다. 이것이 들어가면서 읽기 계약이 그림에도 걸린다.

실제 흐름과 개념 설명을 data-figure-kind 로 가른다. 표시가 없으면 error 가 아니라
warning 이다 — 지금 저장소의 그림 272장에 이 표시가 하나도 없고, 없다고 전부 막으면
정상을 막는 쪽으로 넘어간다. 설명용이라 밝힌 그림도 막지 않는다.

검사기를 조일 때마다 정상이 통과하는 대조를 같이 넣는다. 이번 대조군은 평범한 그림,
설명용이라 밝힌 그림, 내부·data 참조, 그리고 이 저장소의 그림 272장 전부다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-10 11:32:51 +09:00
DongHyeonkaandClaude Opus 5 32a369f335 feat(scripts): Studio 저장 어댑터 — 게시를 코드가 막고 무인 저장은 꺼 둔다
서버 계약은 tech-log-backend @ a000f87 의 openapi 와 컨트롤러에서 읽었다.
Idempotency-Key 는 필수이고 200자 이하다. expectedVersion 은 스키마가 minimum 1 로
못 박으므로 빈 값은 「검사 안 함」이 아니라 「반드시 충돌」이다.

멱등 키를 기록 경로에서 유도한다. 프런트는 호출마다 새 uuid 를 만들어서 재시도가 문서를
둘 만든다. 만들기는 경로만으로 키를 만들어 몇 번을 다시 시도해도 문서가 하나이고,
저장은 보낼 내용을 키에 넣는다 — 경로만 쓰면 두 번째 저장이 첫 결과로 조용히 재생된다.

게시 요청을 만드는 코드가 없고, 계획이 게시 경로를 가리키면 거절한다. 지금까지 게시를
막고 있던 것은 스킬 문서의 문장 하나였다.

무인 저장은 꺼져 있다. 서버 권한이 studio:read·studio:write 둘뿐이라 저장 계정이 게시도
할 수 있다. 켜는 것은 studio:publish 가 갈라진 뒤의 판단이지 이 파일의 기본값이 아니다.

저장 뒤 되읽어 견줄 때 문자열 하나로 판정하지 않는다. 정규화가 지운 차이를 통과로 세면
비교 틀이 결함을 만들어 내거나 지운다. 정규화 뒤에만 같아지는 것은 whitespaceOnly 로,
되읽은 값이 보낸 값의 앞부분인 것은 truncated 로 따로 낸다. 못 보는 칸은 사유와 함께
적는다 — 조용히 건너뛰면 「전부 같다」가 「본 것만 같다」를 가린다.

회귀의 첫 줄은 대조군이다. 손대지 않은 쌍이 「같다」로 나오지 않으면 나머지 「잡았다」가
전부 틀 탓이다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-10 11:13:47 +09:00