41 KiB
Webmaker 1.8.0 직접 편집(시각 편집기) 도입 — Ariweb Suite 0.2.0 TDD RED→GREEN 기록
- 작업 트리:
/home/master/work/Ariweb-suite-visual-editor-v020, 브랜치feature/visual-editor-v0.2.0, 기준origin/main 4b3714e37c543fcfa8862dbc68c76f62a9b0bf97 - 참조 저장소 작업 트리:
/home/master/work/WebHoting_AI_Agent-visual-editor-v180(feature/visual-editor-v1.8.0, 기준3b10859) — 그쪽 RED→GREEN 은modules/webmaker/../docs/verification/visual-editor-1.8.0-tdd.md(참조 저장소 문서)에 있다. - 날짜: 2026-10-28 · Node v24.22.1 · 커밋·push·병합·배포·운영 접근 없음(Neo 검토 전 인도). 출력은 실제 실행 결과를 그대로 옮겼다(스택 줄만 줄임).
- 서버를 띄우는 테스트는 모두
CLAUDE_BIN을 즉시 실패하는 가짜 실행 파일로 두고 실행했다(사이트 생성이 AI 작업을 시작하므로 개발 PC 의 진짜 claude 가 실행되지 않게). 첫 Suite 테스트 실행에서는 그 설정이 빠져 진짜 claude 프로세스가 하나 시작되어 즉시 종료시켰고, 테스트를 고쳤다.
RED — node --no-warnings test/webmaker-visual-editor.test.mjs (도입 전: 모듈·화면·경로 없음)
✘ 도입 모듈에 직접 편집 순수 모듈·런타임·화면 파일이 있고 Webmaker 1.8.0 · Suite 0.2.0 이다
Error: lib/visual-editor.js (실제 false)
✘ lib/visual-editor: 캔버스 변환은 스크립트를 실행하지 않게 하고, 저장 검증은 새 스크립트·핸들러를 거절한다
Error: Cannot find module '/home/master/work/Ariweb-suite-visual-editor-v020/modules/webmaker/lib/visual-editor.js'
✘ visual-editor-core: 실행 취소/다시 실행·저장 상태·메시지 검증·구성 요소가 Suite 체크아웃에서도 같다
Error: Cannot find module '/home/master/work/Ariweb-suite-visual-editor-v020/modules/webmaker/public/visual-editor-core.js'
✘ 화면(index.html)에 직접 편집 전환·저장·실행 취소·다시 실행·버리기·팔레트·속성 조작이 접근성 속성과 함께 있고 Suite 배너는 남아 있다
Error: 직접 편집 전환 버튼 (실제 false)
✘ server.js [Ariweb Suite 변경]: 직접 편집 저장도 동시 편집 rev 충돌(409)을 돌려주고, 맞는 rev 면 저장된다 (실제 서버)
Error: 기대 200 실제 404
✘ 통과 0 · 실패 5
도입 작업
- 참조 저장소에서 1.8.0 을 TDD 로 구현(위 참조 문서). 새 파일 6개는
cp로 그대로 가져옴(lib/visual-editor.js,public/visual-editor-core.js,public/wm-ve-canvas.js,public/visual-editor-ui.js,test/visual-editor.test.js,test/visual-editor-server.test.js),public/admin.js는 Suite 변경이 없어 upstream 그대로. - Suite 블록(
[Ariweb Suite 변경])이 있는server.js·public/app.js·public/index.html·public/style.css는 같은 변경을 손으로 적용(덮어쓰지 않음) +server.js직접 편집 PUT 에revConflict추가.diff로 upstream 과의 차이를 확인: server.js 107줄(모두 0.1.1 Suite 블록 + rev 검사 1줄), app.js 4줄, index.html 2줄, style.css 5줄(배너) — 1.8.0 공통 변경은 양쪽 동일. - 중간 실행 1 — 4/5: rev 충돌 테스트가 "AI 작업 실행 중" 409 를 받음(진짜 claude 가 생성 작업으로 실행 중) → 테스트에 가짜
CLAUDE_BIN·작업 종료 대기 추가. WEBMAKER_SRC=<참조 작업 트리> node scripts/import-sources.mjs --only webmaker --allow-dirty로IMPORTED_SOURCES.json/.md재생성(처음 182 파일 → 최종 184 파일: 0.1.1 의 176 + 신규 8, 수정 8 = 0.1.1 과 같은 Suite 블록 파일, upstream 1.8.0·workingTreeDirty).
GREEN — node --no-warnings test/webmaker-visual-editor.test.mjs
✔ 도입 모듈에 직접 편집 순수 모듈·런타임·화면 파일이 있고 Webmaker 1.8.0 · Suite 0.2.0 이다 (1ms)
✔ lib/visual-editor: 캔버스 변환은 스크립트를 실행하지 않게 하고, 저장 검증은 새 스크립트·핸들러를 거절한다 (13ms)
✔ visual-editor-core: 실행 취소/다시 실행·저장 상태·메시지 검증·구성 요소가 Suite 체크아웃에서도 같다 (2ms)
✔ 화면(index.html)에 직접 편집 전환·저장·실행 취소·다시 실행·버리기·팔레트·속성 조작이 접근성 속성과 함께 있고 Suite 배너는 남아 있다 (3ms)
✔ server.js [Ariweb Suite 변경]: 직접 편집 저장도 동시 편집 rev 충돌(409)을 돌려주고, 맞는 rev 면 저장된다 (실제 서버) (1323ms)
✔ 통과 5 · 실패 0
전체 묶음
modules/webmaker:CLAUDE_BIN=<가짜 exit 1> npm test→ℹ tests 81 · pass 81 · fail 0(0.1.1 의 59 + 새 22)- Suite:
CLAUDE_BIN=<가짜 exit 1> node test/run.mjs→✔ 테스트 파일 23개 중 실패 0개(agent 9·auth 7·backup 8·commerce 11·dashboard 4·domains 3·e2e 11·groups 5·hal-cli 3·hal-mailimport 7·hal-ui 7·hal 16·lib 10·lifecycle 6·mail-discover 2·mail 9·relocation 6·sites 11·transfer 4·usage 4·webmaker-adapter 2·webmaker-inventory 5·webmaker-visual-editor 5). 로컬 E2E 하네스가docs/verification/local-e2e-2026-10-28.md/.json을 새로 썼다(결함 0).
요구 ↔ Suite 테스트 대응
| 요구 | 테스트 |
|---|---|
| 도입 파일·버전(1.8.0/0.2.0)·IMPORTED_SOURCES 불변 기준 커밋·upstream 1.8.0 | 도입 모듈 파일·버전 |
| 스크립트 비실행 변환·저장 검증(새 스크립트 거절·머리 보존) | lib/visual-editor |
| 실행 취소/다시 실행·저장 상태·메시지 규약(세션 불일치 거절)·구성 요소 안전 | visual-editor-core |
| 필수 접근성 조작 + Suite 배너·SSO 변경 보존 | 화면(index.html) |
| Suite 전용 rev 충돌 409 → 맞는 rev 저장 200·버전 visual-edit·rev 증가(실제 서버) | server.js [Ariweb Suite 변경] |
| 권한·CSRF·허용 페이지·오래된 저장·AI 작업 중·캐시 미차감 | modules/webmaker/test/visual-editor-server.test.js(81개 묶음 안) |
검토 수정(Neo 독립 검토, 2026-10-28) — 동시 저장 경쟁(출시 차단) · 원자적 저장(안전 강화)
참조 저장소에서 RED→GREEN 으로 고친 뒤(참조 문서 "검토 수정" 절: 경쟁 테스트 actual 200/expected 409, 원자성 테스트 actual 200/expected 500, helper 없음 TypeError → 모두 GREEN) Suite 에 반영했다.
- 그대로 가져옴(
cp, diff 동일):modules/webmaker/lib/visual-editor.js(writeTextAtomic추가),test/visual-editor.test.js(16건),test/visual-editor-server.test.js(9건 — 경쟁·원자성 추가). - 손으로 반영:
modules/webmaker/server.js직접 편집 PUT — 본문을 읽은 뒤store.load로 사이트 기록과 파일을 다시 읽고(Suite 의revConflict도 다시 읽은 최신 rev 로 비교), hash 검사·검증·writeTextAtomic까지 await 없이 이어 간다. 쓰기 실패 500 한국어 안내. upstream 과의 차이는 여전히 Suite 블록만(107줄). - Suite 전용 추가 테스트는 두지 않았다: 공유 테스트 파일(
modules/webmaker/test/visual-editor-server.test.js)이 Suite 모듈 서버로도 실행되어 경쟁·원자성을 검증하고,test/webmaker-visual-editor.test.mjs의 rev 충돌 테스트가 다시 읽은 rev 경로를 그대로 지나기 때문이다.
GREEN(Suite 체크아웃)
cd modules/webmaker && node --test test/visual-editor-server.test.js test/visual-editor.test.js
ℹ tests 25
ℹ pass 25
ℹ fail 0
node --no-warnings test/webmaker-visual-editor.test.mjs
✔ server.js [Ariweb Suite 변경]: 직접 편집 저장도 동시 편집 rev 충돌(409)을 돌려주고, 맞는 rev 면 저장된다 (실제 서버) (1213ms)
✔ 통과 5 · 실패 0
전체 묶음(수정 뒤, 모두 CLAUDE_BIN=<가짜 exit 1>):
- 참조 저장소
npm test→ℹ tests 77 · pass 77 · fail 0 - Suite
modules/webmakernpm test→ℹ tests 84 · pass 84 · fail 0 - Suite
node test/run.mjs→✔ 테스트 파일 23개 중 실패 0개(webmaker-visual-editor 5/5 포함, 로컬 E2E 가local-e2e-2026-10-28.md/.json을 다시 씀, 결함 0)
실제 브라우저 시각 검증 수정(Neo 의 브라우저 게이트, 2026-10-28) — 직접 편집 중 배치(출시 차단)
- 발견: 독립 Webmaker 코드의 실제 브라우저 검증(1280×633·390×844)에서 직접 편집을 켜도 옛 작업 칸(
.left460px)이 남아 캔버스 iframe 이 114.6px, 390px 에서는 캔버스가 y≈2228 로 밀림. 원인은body.ve-on의 CSS 배치 규칙 부재(JS 전환은 정상). - 참조 저장소에서 RED(
test/visual-editor.test.js+3: CSS 계약 2건 실패직접 편집 중 옛 작업 칸 숨김·좁은 화면 body.ve-on .app 규칙, JS 전환 동작 1건은 통과 — 진단 확인) →public/style.css수정 → GREEN 19/19. Suite 에 동일하게 반영:modules/webmaker/public/style.css같은 블록을 손으로 추가(upstream 과의 차이는 여전히 배너 5줄), 테스트 파일은cp(diff 동일). Suite 모듈에서node --test test/visual-editor.test.js→ℹ tests 19 · pass 19 · fail 0. - 전체 묶음(배치 수정 뒤, 모두
CLAUDE_BIN=<가짜 exit 1>): 참조 저장소npm test→ℹ tests 80 · pass 80 · fail 0· Suitemodules/webmakernpm test→ℹ tests 87 · pass 87 · fail 0· Suitenode test/run.mjs→✔ 테스트 파일 23개 중 실패 0개(webmaker-visual-editor 5/5 포함). 하네스가 다시 쓴local-e2e-2026-10-28.*보고서 파일은 이번 작업의 산출물이 아니라 보존하지 않았다. - 문법·차이 확인:
node --check public/visual-editor-ui.js(두 저장소) 통과,style.cssupstream 대비 차이 5줄(포털 배너)만,test/visual-editor.test.js두 저장소 동일.
실제 브라우저 시각 검증 수정 2(배치 수정 뒤 게이트, 2026-10-28) — 선택 전 도구 줄 노출(출시 차단)
- 발견: 직접 편집을 켜고 아무 요소도 고르지 않았는데 어두운 선택 도구 줄
.wm-ve-tools가 페이지 위에 이미 그려짐(DOM 은tools.hidden = true, 속성 창은 "선택한 요소 없음"). 원인은 캔버스 런타임(wm-ve-canvas.js)의 작성자 규칙.wm-ve-tools{…display:flex…}가 UA 규칙[hidden]{display:none}을 덮어쓰는 것. - 참조 저장소에서 RED(
test/visual-editor.test.js+1: 런타임 소스의 CSS 블록을 파싱해hidden인.wm-ve-tools·.wm-ve-drop의 최종 display 를 cascade 모형으로 계산 —AssertionError: hidden 인 .wm-ve-tools 의 최종 display, actual 'flex' / expected 'none', 20건 중 19 통과 1 실패) →public/wm-ve-canvas.js런타임 CSS 에 한 줄 추가.wm-ve-tools[hidden],.wm-ve-drop[hidden]{display:none !important}→ GREEN 20/20. Suite 에는modules/webmaker/public/wm-ve-canvas.js·test/visual-editor.test.js를cp(두 파일 모두 upstream 과 byte-identical,cmp확인). Suite 모듈에서node --test test/visual-editor.test.js→ℹ tests 20 · pass 20 · fail 0. 다른 동작 변경 없음(JS 의 hidden 전환·보일 때 flex 배치 그대로, 테스트가 함께 확인). - 전체 묶음(도구 줄 수정 뒤, 모두
CLAUDE_BIN=<가짜 exit 1>): 참조 저장소npm test→ℹ tests 81 · pass 81 · fail 0· Suitemodules/webmakernpm test→ℹ tests 88 · pass 88 · fail 0· Suitenode test/run.mjs→✔ 테스트 파일 23개 중 실패 0개(webmaker-visual-editor 5/5 포함). 하네스가 다시 쓴local-e2e-2026-10-28.*보고서 파일은 보존하지 않았다. - 문법·차이 확인:
node --check public/wm-ve-canvas.js(두 저장소) 통과,wm-ve-canvas.js·test/visual-editor.test.js두 저장소 동일,style.cssupstream 대비 차이는 여전히 배너 5줄.
실제 브라우저 게이트 3(Playwright 자동 게이트, 2026-10-28) — 팔레트 → 캔버스 끌어다 놓기 실패(출시 차단)
- 발견: 실제 Chrome 에서 frameLocator 로 캔버스 iframe 을 직접 조작한 게이트가 선택·contenteditable 글 편집·실행 취소/다시 실행·Alt+↑/↓ 이동·팔레트 표시(19/19)를 통과시킨 뒤, 부모 팔레트 "문단" 의 실제 HTML5 끌어다 놓기에서 실패(
끌어다 놓기 뒤 요소 수 증가없음). 원인: 부모가 전용 MIMEtext/wm-ve-component만 넣는데 캔버스는sandbox="allow-scripts"(allow-same-origin 없음) 출처 'null' 문서라 Chromium 이 다른 출처의 전용 MIME 끌기 자료를 넘겨주지 않음. 제품 결함. - 참조 저장소에서 RED(
test/visual-editor.test.js+3 — 순수 추출 규칙core.dragComponentId미존재, 팔레트 dragstart 에 text/plain 없음, 동작 테스트: 가짜 DOM 위에서 실제wm-ve-canvas.js를 부트하고 전용 MIME 이 없는 DataTransfer 로 dragover/drop →dragover preventDefaultactual false; 23건 중 20 통과 3 실패) → 수정 → GREEN 23/23. 수정:public/visual-editor-core.js에COMPONENT_MIME·componentDragText·isComponentDrag·dragComponentId(전용 MIME 우선, 없으면wm-ve-component:<id>정확 일치 + 알려진 id 만),public/visual-editor-ui.jsdragstart 가 text/plain 도 넣음,public/wm-ve-canvas.jsdragover/drop 이 종류로 인식하고 drop 때 검증된 id 만 삽입(아니면 삽입·표식 없음, 기본 drop 차단). sandbox·CSP·메시지 검증·저장 검증·"추가" 버튼 대안 불변. - Suite 에는
modules/webmaker/public/visual-editor-core.js·visual-editor-ui.js·wm-ve-canvas.js·test/visual-editor.test.js를cp(네 파일 모두 upstream 과 byte-identical,cmp확인). Suite 모듈에서node --test test/visual-editor.test.js→ℹ tests 23 · pass 23 · fail 0. - 전체 묶음(끌어다 놓기 수정 뒤, 모두
CLAUDE_BIN=<가짜 exit 1>): 참조 저장소npm test→ℹ tests 84 · pass 84 · fail 0· Suitemodules/webmakernpm test→ℹ tests 91 · pass 91 · fail 0· Suitenode test/run.mjs→✔ 테스트 파일 23개 중 실패 0개(webmaker-visual-editor 5/5 포함). 하네스가 다시 쓴local-e2e-2026-10-28.*보고서 파일은 보존하지 않았다. - 문법·차이 확인:
node --check세 런타임 파일(두 저장소) 통과, 공유 파일 두 저장소 동일,style.cssupstream 대비 차이는 여전히 배너 5줄,server.js·app.js·index.html의 Suite 블록 차이 불변.
실제 브라우저 게이트 4(살균 DnD 진단, 2026-10-28) — HTML5 끌기가 sandbox iframe 으로 넘어가지 않음 → 포인터 끌기 다리(출시 차단)
- 발견: MIME 보완 뒤 같은 게이트 재실행도 끌어다 놓기 단계에서 실패. 부모·캔버스 양쪽 살균 리스너(종류·플래그만) 진단: 부모 dragstart 2건(text/plain·전용 MIME 모두 유효), 캔버스 dragenter·dragover·drop 0건. Chromium 은 HTML5 끌기 자체를 부모 → 출처 'null' sandbox iframe 으로 넘기지 않는다. 제품 결함.
- 참조 저장소에서 RED(
test/visual-editor.test.js+3 — 규약·좌표 순수 규칙paletteDragAt 는 부모→캔버스 종류, 부모 포인터 순서 동작pointerdown/move/up 처리기, 캔버스 수신 동작놓기 표시 보임; 26건 중 23 통과 3 실패) → 수정 → GREEN 26/26. 수정(sandbox 그대로, allow-same-origin 없음):public/visual-editor-core.js에 toCanvas 종류paletteDragAt·paletteDragCancel·paletteDropAt+isComponentId·POINTER_DRAG_THRESHOLD(4px)·pointInFrame(테두리·스크롤바 보정 상대 좌표)·canvasPoint(유한 숫자·뷰포트 안);public/visual-editor-ui.js팔레트 항목 포인터 다리(pointerdown — "추가" 버튼·터치·보조 버튼 제외 → 포인터 캡처 → 문턱 → iframe 안에서만 상대 좌표 메시지, 밖·Escape·pointercancel·끝내기 → 취소, 포인터 끌기 중 네이티브 dragstart 차단, HTML5 DnD 는 대체 경로);public/wm-ve-canvas.js가 메시지 규약 뒤 id·좌표·모드를 다시 검사해 스스로dropTargetAt→ 놓기 표시 /insertComponent(잘못된 입력은 삽입·표식 없이 표시만 숨김);public/style.css에.ve-comp { user-select:none; touch-action:pan-y }·끌기 중 class 2줄. - Suite 에는
modules/webmaker/public/visual-editor-core.js·visual-editor-ui.js·wm-ve-canvas.js·test/visual-editor.test.js를cp(네 파일 모두 upstream 과 byte-identical,cmp확인),modules/webmaker/public/style.css에는 같은 2줄을 손으로 추가(upstream 대비 차이 여전히 배너 5줄). Suite 모듈에서node --test test/visual-editor.test.js→ℹ tests 26 · pass 26 · fail 0. - 전체 묶음(포인터 다리 뒤, 모두
CLAUDE_BIN=<가짜 exit 1>): 참조 저장소npm test→ℹ tests 87 · pass 87 · fail 0· Suitemodules/webmakernpm test→ℹ tests 94 · pass 94 · fail 0· Suitenode test/run.mjs→✔ 테스트 파일 23개 중 실패 0개(webmaker-visual-editor 5/5 포함). 하네스가 다시 쓴local-e2e-2026-10-28.*보고서 파일은 보존하지 않았다. - 문법·차이 확인:
node --check세 런타임 파일(두 저장소) 통과, 공유 파일 두 저장소 동일,server.js·app.js·index.html의 Suite 블록 차이 불변.
독립 검토(읽기 전용) 차단 항목 12건 수정(2026-10-28) — Suite 쪽 기록
참조 저장소 쪽 상세(항목 16·810 의 결함·수정·RED 증거 표)는 modules/webmaker/docs/verification/visual-editor-1.8.0-tdd.md(upstream 과 동일). 여기에는 Suite 반영과 Suite 전용 항목(7·11)을 적는다.
-
공유 파일 반영:
lib/visual-editor.js·lib/gitstore.js·public/visual-editor-core.js·visual-editor-ui.js·wm-ve-canvas.js·test/visual-editor.test.js·test/visual-editor-server.test.js·새test/visual-editor-app.test.js를cp(모두 upstream 과 byte-identical,cmp). Suite 블록이 있는server.js·public/app.js·lib/store.js는 참조 저장소의 수정 전/후 diff 를patch로 적용하고(server.js 의 소스/직접 편집 PUT 두 hunk 는 Suite rev 줄 때문에 손으로 반영) Suite 블록을 보존했다. upstream 대비 차이:server.js115줄(107 → 115: 항목 7 의 rev 필수 2줄·잠금 안으로 옮긴revConflict줄·주석),app.js4줄·style.css5줄·index.html2줄 불변,lib/store.js4줄(rev 증가 + restore 때 rev 보존). -
항목 7(Suite 전용) — 직접 편집 저장은 유한한 숫자
rev필수:PUT /api/sites/<id>/visual/<페이지>는typeof rev === 'number' && Number.isFinite(rev)가 아니면 400{ revRequired: true }(없음·문자열 "3"·null·"NaN"·""). 잠금 안에서 다시 읽은 사이트 기록의 rev 와 다르면(다른 파일 저장으로 오른 rev 포함) 409conflict{ yourRev, currentRev }. 소스 편집기 PUT 의 옛 화면 호환 규칙(rev 없으면 통과)은 이 경로에 적용하지 않는다(fail-closed). 공유 서버 테스트는 GET 이 준 rev 를 모든 PUT 에 함께 보낸다(단독 Webmaker 는 0 을 주고 무시) — 처음 Suite 에서npm test를 돌리자 rev 없는 PUT 8건이 400 으로 떨어져 이를 드러냈고, 뒤처진 저장의 409 는 Suite 에서 hash(stale) 가 아니라 rev(conflict) 로 먼저 잡히므로 테스트가 둘 중 하나를 받는다(expectStale). -
항목 6 의 Suite 부분 — 되돌리기 저장은 rev 를 올리지 않는다:
lib/store.js save(site, { restore: true })는 rev 를 그대로 둔다. 실패한 직접 편집 저장(사이트 기록 폴더 읽기 전용 → store 실패)은 500 뒤 페이지·rev 가 저장 전 값 그대로이고 같은 rev·baseHash 로 재시도하면 200. -
항목 11(Suite 전용) — 도입 소스 출처 fail-closed: 새
lib/provenance.mjs checkProvenance(manifest): 소스마다 40자리commit·비어 있지 않은 파일 목록(from·to·origHash),upstream이 있으면 version·branch·headCommit(= commit)·workingTreeDirty === false를 요구하고, 모르는 형태·빈 목록은 불완전으로 거절.upstream칸이 없는 0.1.x 기록(AriMail)은 깨끗한 고정 커밋만 받던 스크립트가 만든 것이라 legacy 로 받아들인다.scripts/release.mjs build는 검사를 테스트보다 먼저 하고 깨끗하지 않으면 artifact 를 만들지 않고 종료 1(이유 출력), manifest.json 에checks.provenanceClean·provenanceProblems·imported[].upstream/legacy를 남긴다.register --approve는checks.provenanceClean !== true(false·옛 빌드의 없음)면 DB 를 열기 전에 종료 1.hal/core.approveRelease는checks.provenanceClean === false면release_checks_failed. 테스트용ARIWEB_IMPORTED_SOURCES로 manifest 경로를 바꿀 수 있다. 개발 중 기록(workingTreeDirty: true, --allow-dirty)으로는 build 가 멈췄다(의도). 참조 저장소 1.8.0 이4b8c4d0de0f69025001a9e0a28a58a94e1a1f088으로 커밋·검증된 뒤scripts/import-sources.mjs의 pin 을 그 커밋으로 올렸고, 그 pin 으로--allow-dirty없이 다시 만든 기록(commit = headCommit = 4b8c4d0,workingTreeDirty:false, 184 파일·수정 8)이면 build·approve 가 된다 — 이 작업 트리의IMPORTED_SOURCES파일은 아직 개발 중 기록이라 Neo 가 다시 만든다. -
항목 12 — 이 문서·VERSION_HISTORY·upstream 문서의 수치를 최종값으로, 가져온 파일 수는 생성된 manifest(
IMPORTED_SOURCES.json)에서 읽어 적었다(VERSION_HISTORY). -
수정 뒤 브라우저 게이트 재실행 조사(2026-10-28): Neo 의 재실행이
edit-text에서 "저장 상태 dirty" 시간 초과. 원인은 게이트 멱등성 결함(같은 저장 폴더에 이전 실행의 같은 제목이 이미 있어 변경 없음 = clean, 제품 규칙은 옳음)이며 로컬에서 결정적으로 재현·수정(실행마다 다른 제목 + 사전 단언). 같은 로컬 실행에서 실제 틈 하나(가운데 클릭auxclick이 sandbox 에서도 새 페이지를 만듦)를 찾아wm-ve-canvas.js에 auxclick 차단을 더했다(단위 RED→GREEN, upstream 과 byte-identical 로 반영). 로컬 Chrome 156 게이트 61/61(연속 2회), 이어서 Neo 호스트의 실제 Chrome 재실행 61/61 통과(2026-10-28). 상세:modules/webmaker/docs/verification/visual-editor-1.8.0-tdd.md.
독립 검토 2차(종결 검토) 차단 항목 3건 + 문서 수정(2026-10-28) — Suite 쪽 기록
참조 저장소 쪽 상세(항목 1·2 의 결함·수정·RED 증거)는 modules/webmaker/docs/verification/visual-editor-1.8.0-tdd.md(upstream 과 동일). 여기에는 Suite 반영과 Suite 전용 항목 3 을 적는다.
- 공유 파일 반영:
lib/visual-editor.js·lib/gitstore.js·public/visual-editor-core.js·test/visual-editor.test.js·test/visual-editor-server.test.js·docs/verification/visual-editor-1.8.0-tdd.md·README.md를cp(모두 upstream 과 byte-identical,cmp).server.js는 참조 저장소의 수정 전/후 diff 를patch로 적용하되 직접 편집 PUT 의 트랜잭션 hunk 는 Suite rev 주석 때문에 손으로 반영(되돌리기의store.save(prev,{restore:true})줄에[Ariweb Suite 변경]주석 — rev 보존). upstream 대비 차이:server.js115줄(변함없음),app.js4줄·style.css5줄·index.html2줄·lib/store.js4줄 불변. 도입 기록 재생성: 184 파일·수정 8·workingTreeDirty=true(의도). - 항목 1 — SVG SMIL 저장형 XSS(공유 코드): Suite 는 같은
lib/visual-editor.js를 쓰므로 그대로 적용된다 — 새 SMIL 요소(animate·set·animateMotion·animateTransform·animateColor·discard·mpath, 대소문자·svg:접두어 포함)는 400, 원본에 있던 것은 그 수까지만(삭제 허용), 캔버스는wm-ve-x-비활성 이름. Suite 모듈 단위 테스트 38/38. - 항목 2 — Git 저장 트랜잭션(공유 코드 + Suite rev):
gitstore.recordChangeStrict가 로컬 커밋 → 사이트 기록(Suite 는 여기서 rev +1 한 번) → 원격push --atomic(main + 변경 브랜치) 순서로 끝내고, 실패하면reset --mixed·브랜치 삭제/복원·페이지 원본·store.save(prev,{restore:true})(rev 보존) 뒤 검증한 결과로만rolledBack을 준다. 1차 수정 경로가 하던 두 번의store.save(manualEdits 저장 + gitRecord 안 저장)가 한 번으로 줄어 rev 는 성공한 저장마다 정확히 1 오른다(기존test/webmaker-visual-editor.test.mjs의currentRev >= …검사 그대로 통과). 푸시 전에 저장되는 기록은pushed:false, remote:'pending'이고(원격 반영 미주장 — 훅이 푸시 순간에 복사한 기록으로 확인), 트랜잭션 밖reconcileSite(잠금 안, 저장 뒤·기동 때)가ls-remote로 확인한 뒤에만verified로 바꾸며 그 저장은{restore:true}라 rev 를 올리지 않는다(재기동 조정 테스트가saved.rev === site.rev를 확인). 공유 서비스 테스트(bare 원격 + 훅 실패 주입, 첫 커밋 전 되감기, 기동 조정, 모호한 푸시 판정, 잠금 경쟁)가 Suite 모듈에서도 그대로 돈다(18/18). - 항목 3(Suite 전용) — 릴리스 승인 fail-open:
hal/core.approveRelease가checks.testsPassed === false·provenanceClean === false만 거절해 checks 가 없거나(옛 빌드·수동 등록)testsPassed: null(--skip-tests)·dirty미기록인 릴리스를 승인했고,registerRelease(approve:true)·운영자 APIPOST /ops/api/releases/:version/approve도 같은 함수를 지나 같은 구멍이 있었다. 새hal/core.releaseApprovalProblem(checks)한 규칙:testsPassed === true·dirty === false·provenanceClean === true가 모두 있어야 하며(없음·null·문자열 "true"·1·"false" 모두 거절) 이유를release_checks_failed메시지로 돌려준다.scripts/release.mjs register --approve도 같은 함수로 DB 를 열기 전에 종료 1(이유 출력).--skip-tests빌드(testsPassed null)는 등록만 되고 승인은 불가. 기존 테스트 픽스처(hal-cli 2곳·hal 1곳registerRelease, e2ebuildFakeReleasemanifest)는 깨끗한 checks 를 넣도록 고쳤다 — 현재 요구사항에 관용 정책이 없어 옛 정책은 남기지 않았다. - 문서 수정 — 이 문서·VERSION_HISTORY·upstream 문서의 "재실행 필요/미실행"·"자동화하지 않음"·옛 수치(103/110·182 파일·HTML5 주 경로·release.mjs 변경 없음)를 현재 증거(Neo 호스트 실제 Chrome 61/61, 106/113/24, 184 파일·수정 8, 포인터 다리 주 경로 + HTML5 대체, release.mjs·hal/core 변경됨)로 고쳤다.
독립 검토 3차(최종 검토) 차단 항목 4건(2026-10-28) — Suite 쪽 기록
- 항목 1(Suite 전용) — 승인된 릴리스 불변:
hal/core.registerRelease는 기존 행이 approved 면 같은 내용(main 커밋·빌드 시각·artifact·checks 를 키 순서 무관하게 비교, 메모는 준 경우)의 재등록만 멱등하게 돌려주고(approve:true 여도 승인자·시각 그대로), 다르면release_immutable로 거절하며 UPDATE 를 실행하지 않는다. 기존 행이 승인되지 않았으면 내용을 갱신하되status='registered', approved_by=NULL, approved_at=NULL로 두고, approve 는releaseApprovalProblem규칙을 지나야 한다.hal/index.mjs register-release는 거부를릴리스 등록 거부 (release_immutable): …로 출력하고 종료 1(DB 불변). 운영자 API 의 승인 경로(POST /ops/api/releases/:v/approve)는 같은 규칙(앞 절 테스트). RED → GREEN:test/release-provenance.test.mjs+2 — core(8가지 변경 × approve true/false 모두 거절·readback 불변, 미승인 버전의 승인 미승계) · CLI(같은 manifest 재등록 멱등, 불량 checks·다른 artifact 재등록 종료 1 + DB readback 불변). - 항목 2·3(공유 코드):
modules/webmaker/server.js에 라우터 수준 잠금·재적재(본문 메모 읽기 → 잠금 안 재적재 → 짧은 경로 전체 잠금 / 긴 경로 재적재)와 재진입 잠금,changeLanguages순서, 직접 편집 PUT 의remoteState:'ambiguous'응답을 upstream 과 같은 블록으로 반영(Suite 블록 보존).lib/gitstore.js는 byte-identical. Suite 의 직접 편집 rev 충돌 검사는 잠금 안에서 재적재된 최신 기록으로 이루어진다. - 항목 4: 이 문서·VERSION_HISTORY 의 합계(107/114 등)를 현재 증거로 바꿨다.
- 항목 5(공유 코드 + Suite 블록) — 배경 작업 저장 합치기:
modules/webmaker/lib/store.js의load가 읽은 상태를__base(열거 안 됨)로 기억하고save가 디스크saveId변화를 보고 이 객체가 바꾼 최상위 항목만 합친다. Suite 블록: 합칠 때rev는 디스크 값을 기준으로 올리고,keepRev(원격 반영 조정)는 합치되 rev 를 올리지 않는다(restore는 스냅샷 그대로, rev 불변). upstream 대비 차이 5줄. 되돌리기 검증(server.js)은updatedAt·saveId만 제외하고 기록을 비교한다. 공유 서비스 테스트 +1(작업 중 PATCH 제목·PUT 등급 → 중단 → 유지)이 Suite 모듈에서도 그대로 돈다(19/19). - 항목 6(공유 코드 + Suite 블록) — 같은 잎 동시 변경 fail-closed:
store.js합치기를 잎 단위 3자 병합으로(일반 객체는 안으로, 배열·기본값은 잎), 같은 잎을 둘 다 다르게 바꾼 경우만SaveConflict(codesite_save_conflict, status 409,fields)로 거절·디스크 보존.store.update(id, mutate, opts)·store.adopt(site, fresh)추가(Suite:update도keepRev를 받아 조정 저장은 rev 불변).server.js: 배경 작업 저장 전부saveJob(site, reapply)(충돌이면 최신 기록 위에 작업 항목만 재적용 + adopt),gitRecord충돌 때 버전만 추가·옛 실패분 재시도는 잠금 안 조정으로,/git/retry·reconcileSite는applyPushState를store.update로 적용. 그 밖의 화면 경로의 충돌은 409 로 드러난다(조용한 손실 없음). RED → GREEN: 공유 서비스 테스트 +2(저장소 규칙 6가지 / 실제 서버 — git 래퍼로 commit 2초 지연 → 작업 종료의 버전 기록 창에 다시 푸시 → 다시 푸시의 verifiedAt 그대로·작업의 새 버전도 기록); 중간 RED 에서 충돌이 다시 푸시 경로에 409 로 드러난 것을 확인한 뒤 그 경로를 update 로 옮겼다. Suite 모듈 21/21.
독립 검토 4차 차단 항목 4건(2026-10-28) — Suite 쪽 기록
-
항목 1(Suite 전용) — artifact 설치 불변·fail-closed:
hal/core.installArtifacts(d, {version, mainCommit, builtAt, checks, artifacts:{mod:{file, sha256, size, src}}})— ① 원본 바이트 sha256·크기를 선언값과 대조(불일치 →artifact_hash_mismatch, 아무것도 쓰지 않음) ② 승인된 버전이면releaseImmutableProblem(메타데이터) + 설치된 파일 해시 모두 같을 때만 멱등(복사 없음), 아니면release_immutable(기존 파일·DB 불변) ③ 새·미승인 설치는.<file>.tmp-…에 쓰고 해시를 다시 확인한 뒤 rename.hal/index.mjs register-release·scripts/release.mjs register는 설치 →registerRelease순서로 바뀌었고 거부는 종료 1(release.mjs 는 DB 를 닫고 종료). RED → GREEN:test/release-provenance.test.mjs+2 — HAL CLI(A 승인 → B 유효 해시·B 바이트에 A 해시 거짓 선언 × approve 유무 → 종료 1, 대상 바이트/해시/DB 가 A, 임시 파일 없음, A 재등록 멱등, 새 버전 해시 불일치 미설치) · release.mjs(같은 4 경우 + 멱등). -
항목 2·3·4(공유 코드):
modules/webmaker/server.js— 읽기 요청 부작용 제거(언어 저장은 잠금 안 재적재에서만), 폐기 규약(destroySite(siteId, by, {ownerId, confirm}): 잠금 안 재검사·표식 claim → 바깥 정리 → 같은 표식 확인 뒤 삭제; 라우터 재적재·claimJob의 폐기 중 409; 실패는store.update로 deleteFailed) — 사용자 경로는 Suite 의 포털 영향 확인 블록(ariweb.impactFromPortal·acknowledgeImpact·unlink)을 그대로 두고 마지막 호출만 바꿨다(upstream 대비 118줄 차이 — 기존 Suite 블록 + 폐기 전 검사 2줄).lib/visual-editor.js스캐너의 외래 콘텐츠 깊이는 byte-identical. -
잔여 위험 해소 — 폐기 때 포털 연결 보상(Suite 블록 + 공유 훅): 포털 연결 등록부(
lib/ariweb.js의ariweb-links.json, 로컬 파일)의 해제(unlink)는 되돌릴 수 있고(link로 되살림,linkedAt보존 인자 추가), 포털 자체에는 영향 조회(impactFromPortal, 읽기)만 한다. 공유destroySite의 최종 잠금 단계를 "임시 이름으로 옮김(rename) →beforeIrreversible→ 기록 unlink(되돌릴 수 없는 한 단계) → 최선의 뒷정리" 로 바꾸고, Suite 사용자 경로는beforeIrreversible에서ariweb.unlink+ readback(남아 있으면 502),compensate에서ariweb.link(원래 연결)+ readback 을 한다. 순서상 정리 실패·연결 해제 실패에는 되돌릴 수 없는 변경이 하나도 없고, 기록 삭제 실패에는 되살리기가 있으며 되살리지 못하면recoveryRequired(응답·deleteFailed·감사site.destroy.failed)로 운영자에게 보인다. 뒷정리 실패는 성공 응답의warnings+ 감사site.destroy.cleanup(남은.deleting-…폴더). RED → GREEN:modules/webmaker/test/ariweb.test.js+3(가짜 ssh·가짜 배포 대상·가짜 포털) — ① 정리 실패(SSH_FAIL): 연결·파일·기록 그대로 + deleteFailed, 정리가 되면 폐기 성공·연결 없음 ② 정리 성공 뒤 연결 등록부 쓰기 실패(DATA_ROOT 읽기 전용): ssh 호출 있음, 연결·파일·기록 그대로(임시 폴더 없음), 쓸 수 있게 되면 성공 ③ 되살리기 readback + 뒷정리 실패(지울 수 없는 하위 폴더): 200 + warnings, 연결·기록·원래 폴더 없음, 남은 폴더 1개 확인 뒤 정리.
RED — Suite(4차 검토)
node test/release-provenance.test.mjs(수정 전 CLI):✘ 통과 9 · 실패 2—B 유효 해시 + approve: 대상 바이트 A 그대로: 기대 "artifact-A-bytes" 실제 "artifact-B-bytes-different", release.mjs기대 "rel-A" 실제 "rel-B-other"(승인된 artifact 바이트가 덮임).- 공유 테스트(참조 저장소, 수정 전): 단위 38/39(
거절되어야 함: …<svg><title><script>alert(1)</script></title></svg>), 서비스 21/23(읽기 요청은 기록 파일을 바꾸지 않는다, 폐기 중 저장이 "폐기" 로 거절되지 않음). - Suite 모듈
test/ariweb.test.js(수정 전 순서 — unlink 먼저):ℹ tests 10 · pass 7 · fail 3—포털 연결이 풀리지 않았다(연결 해제는 정리 성공 뒤에만): null,정리는 먼저 수행됐다(연결 해제 실패가 정리 전에 남),연결이 되살아났다(readback): null.
GREEN — Suite(4차 검토 뒤, 현재 증거)
node test/release-provenance.test.mjs ✔ 통과 11 · 실패 0
modules/webmaker: CLAUDE_BIN=<가짜 exit 1> npm test ℹ tests 125 · pass 125 · fail 0 (단위 39 · 서비스 23 · 화면 전환 1 · 원본 59 + ariweb 폐기 보상 3)
CLAUDE_BIN=<가짜 exit 1> node test/run.mjs ✔ 테스트 파일 24개 중 실패 0개 (생성된 local-e2e-2026-10-28.{md,json} 은 삭제)
참조 저장소: npm test 115/115(단위 39 + 서비스 23 + 화면 전환 1 + 원본 52). 실제 Chrome 게이트(Neo 호스트) 61/61 그대로(게이트가 보는 화면 동작 불변).
RED — Suite(최종 검토)
node test/release-provenance.test.mjs(수정 전hal/core.mjs):✘ 통과 7 · 실패 2—Error: 승인자·시각도 그대로: 기대 {"status":"approved","by":"op",…} 실제 {"status":"approved","by":"op2",…}(승인된 행이 재등록으로 바뀜), CLI 재등록 뒤approved_by가 "test" → "test2".- 공유 서비스 테스트(수정 전 코드, 참조 저장소):
ℹ tests 18 · pass 15 · fail 3— 잠금 경쟁(설정 저장이 직접 편집 버전을 덮어쓰지 않는다: ["1:create"]), 모호한 푸시(원격이 받았으면 성공: {…"rolledBack":true,"stage":"push"}), 준비 단계 실패(stage·rollback없음, 페이지 미복원). 항목 5(수정 전 store.js):ℹ tests 19 · pass 18 · fail 1—작업의 종료 저장이 화면의 제목 변경을 덮어쓰지 않는다: {"title":"VE site","status":"idle"}.
GREEN — Suite(최종 검토 뒤)
node test/release-provenance.test.mjs ✔ 통과 9 · 실패 0
modules/webmaker: CLAUDE_BIN=<가짜 exit 1> npm test ℹ tests 119 · pass 119 · fail 0 (단위 38 · 서비스 21 · 화면 전환 1 · 원본 59)
CLAUDE_BIN=<가짜 exit 1> node test/run.mjs ✔ 테스트 파일 24개 중 실패 0개 (생성된 local-e2e-2026-10-28.{md,json} 은 삭제)
참조 저장소(그때): npm test 112/112(단위 38 + 서비스 21 + 화면 전환 1 + 원본 52). 현재 증거는 위 "4차 검토 뒤" 절.
RED — Suite(종결 검토)
node test/release-provenance.test.mjs(수정 전hal/core.mjs·scripts/release.mjs):✘ 통과 4 · 실패 3—Error: checks 없음 (approveRelease)(승인됨),Error: 7.7.0: 거절 200 {"ok":true,"release":{…}}(운영자 API 가 checks 없는 릴리스를 승인),Error: tests-null: HAL 릴리스 등록: 0.2.0 (approved) … 기대 1 실제 0(register --approve가 testsPassed null 을 승인).- 공유 서비스 테스트(수정 전 코드, 참조 저장소에서 실행):
ℹ tests 14 · pass 11 · fail 3— Git 실패 되돌리기rolledBack:false(옛 boolean 은 상태를 검증하지 않음), 원격 트랜잭션(옛 코드는 pre-receive 거부를 200 +rec.error로 삼킴). 단위: SMIL거절되어야 함: …<animate attributeName="href" values="javascript:alert(1)"….
GREEN — Suite(종결 검토 뒤)
node test/release-provenance.test.mjs ✔ 통과 7 · 실패 0 (checks 13가지 불량 × approveRelease/registerRelease 거절, 운영자 API 거절/승인, register --approve 4가지 거절 + 깨끗한 manifest 승인)
modules/webmaker: CLAUDE_BIN=<가짜 exit 1> npm test ℹ tests 114 · pass 114 · fail 0 (단위 38 · 서비스 16 · 화면 전환 1 · 원본 59)
CLAUDE_BIN=<가짜 exit 1> node test/run.mjs ✔ 테스트 파일 24개 중 실패 0개 (생성된 local-e2e-2026-10-28.{md,json} 은 삭제)
참조 저장소(그때): npm test 107/107(단위 38 + 서비스 16 + 화면 전환 1 + 원본 52). 현재 증거는 위 "최종 검토 뒤" 절.
RED — Suite(1차 검토)
node --no-warnings test/release-provenance.test.mjs(구현 전):✘ 통과 0 · 실패 4(lib/provenance.mjs없음 → 네 테스트 모두 실패).node --no-warnings test/webmaker-visual-editor.test.mjs를 rev 필수 줄과{ restore: true }를 뺀 임시 서버 복사본 으로 실행:✘ 통과 4 · 실패 1—Error: rev 없음: {"path":"index.html",…,"rev":11,…}(rev 없는 저장이 200 으로 파일을 바꿈). 실행 뒤 복사본은 수정본으로 되돌렸다(cmp확인).- Suite
modules/webmakernpm test(공유 테스트가 rev 를 보내기 전):ℹ tests 110 · pass 102 · fail 8— 모두{"error":"저장본 개정 번호(rev)가 없거나 올바르지 않습니다…","revRequired":true}(항목 7 이 동작함을 보임) → 공유 테스트에 rev 추가 →pass 108 · fail 2(409 가 stale 대신 conflict) →expectStale→ 110/110.
GREEN — Suite(1차 검토 뒤)
node --no-warnings test/release-provenance.test.mjs ✔ 통과 4 · 실패 0
node --no-warnings test/webmaker-visual-editor.test.mjs ✔ 통과 5 · 실패 0 (rev 없음/문자열/null/NaN/빈 → 400, 교차 파일 rev → 409, store 실패 → 500·rev 보존·재시도 200)
modules/webmaker: CLAUDE_BIN=<가짜 exit 1> npm test ℹ tests 110 · pass 110 · fail 0
CLAUDE_BIN=<가짜 exit 1> node test/run.mjs ✔ 테스트 파일 24개 중 실패 0개 (release-provenance 포함; 생성된 local-e2e-2026-10-28.{md,json} 은 삭제)
참조 저장소: npm test 103/103(단위 37 + 서비스 13 + 화면 전환 1 + 원본 52).
브라우저 검증 상태 · 자동화하지 않은 것
- 실제 브라우저 검증(Neo 의 게이트, 실제 Chrome)은 다섯 차례 수행되었다: 배치 결함 → 선택 전 도구 줄 노출 → 끌어다 놓기 실패 → 살균 진단으로 "HTML5 끌기가 sandbox iframe 에 도달하지 않음" 확정(3·4차는 선택·글 편집·실행 취소/다시 실행·키보드 이동 통과) → 포인터 다리 뒤 네이티브 포인터 순서 게이트 37/37. 독립 검토 수정 뒤 재실행은 게이트 멱등성 결함을 고친 뒤 Neo 호스트에서 61/61 통과(연결 규약·이동 활성화 6종·위조 창 메시지 단언 포함) — 캔버스 안 실제 포인터 조작(글 입력·실행 취소/다시 실행·이동·팔레트 끌어다 놓기·저장·저장 파일·390px)은 이 게이트로 자동 검증했다. Suite 쪽 화면은 독립 Webmaker 와 같은
style.css·visual-editor-ui.js·wm-ve-canvas.js를 쓰므로 같은 수정이 적용된다(style.css 포털 배너 5줄만 추가). 종결 검토 3건은 브라우저 동작을 바꾸지 않아 추가 재실행은 필요 없다. - 자동화하지 않은 것: 포털 SSO 로 들어간 실제 화면 확인, 운영 서버 배포, Chrome 외 브라우저. 사람 확인 목록은 Webmaker
Dev_Order_v1.8.0.md끝.