사용에서 경험으로
리포트는 증상을 이름 붙입니다. 작업은 그 뒤에 있는 경험입니다. "대화가 주제로 옮겨가면 클라이언트가 사라진다"는 버그입니다. "대화는 그 주제를 모은다: 주제가 추가되면 클라이언트가 남는다"가 구축하고, 확인하고, 촬영할 내용입니다. 먼저 그 문장을 작성하세요. 증상을 가능하게 한 구조를 수정한 다음, 제품을 직접 사용하여 그 문장을 증명하세요.
반복 주기
- 사용 기록 수집. 리포트를 읽고, 실제 사용 흔적(대화, 일지, 로그, 접속 여부, 건드린 파일)을 읽습니다. 그 흔적에는 아무도 문서화하지 않은 기대가 담겨 있습니다("ils en sont où?"는 요약이 아니라 포인트 자체를 기대합니다).
- 경험을 작성하세요. 리포트당 한 문장, 현재 시제, 사용자 관점에서, 구현 관련 단어는 쓰지 않습니다. 목록을 유지하세요. 그것이 영상의 내레이션이자 클레임의 메시지가 됩니다.
- 구조적 원인을 찾으세요. 왜 제품이 이런 일을 할 수 있었는지 물어보세요. 하나의 원인이 여러 리포트를 설명하는 경우가 많습니다(표시 이름에서 가져온 신원; 단어를 공유하는 두 개의 카운트; 삽입 시 클래스를 버리는 렌더러). 증상을 보여준 화면이 아니라 원인을 수정하세요.
- 직접 사용해 보세요. 브라우저 네임스페이스(아래 참조)를 통해 관련된 각 사용자로서 데스크톱 및 휴대폰 너비에서 실제 앱을 구동하세요. 스크린샷을 확인하세요. 코드가 의도한 것이 아니라 페이지가 말하는 것을 세세요.
- 보관하세요. 수동 단계를 시나리오로 전환하세요: 액션은 이벤트가 되고, 확인은 클레임이 되며, 경험 문장은 장면의 내레이션이 됩니다. 모델 턴을 테이프에 녹화하여 영상이 비용 없이 몇 초 만에 재생되도록 하세요.
- 패치가 아닌 경험을 보고하세요. 요약에서는 사용자가 이제 무엇을 찾을 수 있는지, 그 다음 원인, 그리고 영상 및 재생 방법을 먼저 제시하세요.
하우스 취향 (Othman이 반복적으로 요청한 사항)
- 복사를 줄이세요. 상태는 그려집니다: 어조, 무게, 배치, 링, 마크. 화면을 설명하는 문장은 화면이 잘못되었음을 의미합니다.
- 아이디어당 한 단어. 용어집을 유지하고 지키세요: "critère"(절차에 따라 판단됨)는 절대 "point"(사람이 결정하는 것)가 아닙니다. 두 카운트가 단어를 공유하면 누군가 그것들을 더할 것입니다.
- 것들은 누적됩니다. 태그는 쌓입니다(종류, 그 다음 클라이언트, 그 다음 주제). 현재 태그는 채워지고 나머지는 윤곽선으로 남습니다. 사용자가 이미 본 컨텍스트를 절대 대체하지 마세요.
- 텍스트보다 숫자가 먼저 그려집니다. 내역은 차트로 열리며, 각 시리즈마다 하나씩, 값이 레이블로 표시됩니다. 텍스트는 그 아래에 있으며, 이벤트당 한 줄씩, 요청 시 열립니다.
- 모든 참조는 해당 위치로 연결됩니다. 단락 번호는 해당 단락으로 스크롤되고 강조됩니다. 인용문은 해당 구절을 찾습니다. 스크롤바는 것들의 위치를 색상으로 표시합니다.
- 기본적으로 라이브. 전체 새로고침 없음. 한 사람이 변경한 내용이 모든 열린 페이지에 즉시 표시됩니다. 느린 페이지에서 반복 탭이 효과를 반복해서는 안 됩니다: 제스처는 멱등적이어야 합니다.
- 컨트롤은 해당 항목이 있는 곳에, 그리고 그것을 사용할 수 있는 사람에게만 표시됩니다. 읽기 전용 사용자는 컨트롤 없이 상태를 봅니다.
- 누가 있는지 알 수 있어야 합니다. 신원과 역할이 모든 화면과 모든 에이전트에 도달해야 합니다. 파트너에게 "파트너가 확인해야 합니다"라고 말하지 마세요.
- 정직한 가장자리. 제품이 아직 하지 못하는 것을 솔직하게 말하세요. 그것을 할 수 없는 화면을 가리키지 마세요.
- 휴대폰 너비는 일급 독자입니다: 16px 거터, 수평 스크롤 없음(
minmax(0,1fr),1fr아님), 계정은 항상 접근 가능해야 합니다. - 제품 언어로 된 목업과 내레이션 (Knowledger의 경우 프랑스어).
브라우저 네임스페이스로 수동 테스트
Beam Lisp의 네이티브 브라우저(beam-lisp 트리의 optional/browser-port`)는 신뢰할 수 있는 입력과 연속 스크린캐스트로 프라이빗 헤드리스 Sway에서 헤드형 Chromium을 실행합니다. 시나리오가 구동하는 것과 동일한 객체이므로, 수동 세션이 다시 작성되지 않고 테스트로 성장합니다. 빠른 확인에는 agent-browser를 사용하세요. 보관할 가치가 있는 것은 브라우저 네임스페이스를 사용하세요.
머신당 한 번
cd ~/projects/beam-lisp/optional/browser-port
nix develop --command cargo build --release --locked --features native-browser
nix build .#runtime-wayland --out-link ~/.local/share/bl-browser/runtime-wayland # chromium, sway, wayvnc
command -v ffmpeg || nix profile add nixpkgs#ffmpeg
bl run -p ~/projects/beam-lisp/optional/browser-port/bl …(또는 bl test -p …)로 사용하는 모든 것을 실행하세요.
프로브: 파일 하나, 사람 하나, 실제 입력
templates/probe.bl이 시작점입니다. 핵심:
(ns probe (:require [browser.page :as page]))
(def rt (str (System/get_env "HOME") "/.local/share/bl-browser/runtime-wayland/bin/"))
(def p (page/open! {:executable (str (System/get_env "HOME") "/projects/beam-lisp/optional/browser-port/target/release/bl-browser-port")
:width 1440 :height 860
:launch {"profile" "/tmp/probe/profile" "runtime" "/tmp/probe/runtime"
"chromium" (str rt "chromium") "compositor" (str rt "sway") "vnc" (str rt "wayvnc")
"width" 1440 "height" 960}}))
(try
;; 누가 보고 있는지: 프록시가 추가할 신원(여기서는 Tailscale 헤더); 라이브 소켓에도 도달합니다
(page/cdp! p "Network.enable" {})
(page/cdp! p "Network.setExtraHTTPHeaders" {"headers" {"tailscale-user-login" "[email protected]" "tailscale-user-name" "timinou"}})
(page/record! p "/tmp/probe/take.mp4" {:fps 24}) ; 선택 사항: 전체 세션을 영화로
(page/navigate! p "http://127.0.0.1:24025/a/sahel-…")
(page/wait! p "document.documentElement.dataset.live==='1'") ; JS 조건자, 기본 30초 제한
(page/click! p ".rv-rail a.para[data-goto]") ; 신뢰할 수 있는 포인터: 이동, 누름, 놓음
(println (page/evaluate! p "scrollY")) ; 사람이 볼 내용을 페이지에 묻기
(page/screenshot! p "/tmp/probe/after.png") ; 그런 다음 확인
(page/stop-recording! p)
(finally (page/close! p)))
동사는 navigate! wait! click! hover! fill! key! drag! evaluate! screenshot! record! stop-recording! show-cursor! cdp!입니다. cdp!는 Chromium이 제공하는 다른 모든 것(뷰포트, 네트워크, 쿠키, 에뮬레이션(휴대폰용 Emulation.setDeviceMetricsOverride))에 도달합니다.
프로브를 보관할 가치가 있게 만드는 습관
- 코드가 아닌 페이지에 물어보세요. 모든 확인은 사람이 해당 사항을 볼 때
true를 반환하는 JS 표현식입니다: 행 수, 태그의 텍스트,scrollY, 포인터 아래 요소의 클래스. 해당 표현식은 변경되지 않고 클레임이 됩니다. - 의미로 선택하세요. 테스트에 필요한 곳에 안정적인 데이터 속성을 앱에 제공하세요(
button.cell[data-person='Karim Saadi'][data-team='Associés']), 절대 "다섯 번째 버튼"이 아닙니다. - 각 사람이 되어 보세요. 각 사람의 신원으로 페이지를 하나씩 엽니다. 두 페이지를 나란히 두는 것이 "라이브"를 증명하는 방법입니다: 하나는 액션을 취하고, 다른 하나는 새로고침 없이 변경됩니다.
- 첫 페인트와 패치된 페이지를 모두 확인하세요. 라이브 앱은 HTML을 한 번 렌더링한 다음 데이터에서 패치합니다. 버그는 차이점에 숨어 있습니다(삽입 시 클래스가 드롭됨, 텍스트로 시작하는 목록이 태그 이름으로 읽힘). 모든 화면에 URL로 한 번, 그리고 탐색으로 한 번씩 도달하세요.
- 연결뿐만 아니라 페이지가 라이브가 될 때까지 기다리세요. 라이브 클라이언트는 서버에서 렌더링된 페이지를 소켓의 첫 번째 렌더링으로 대체할 수 있습니다. 그 사이에 입력된 텍스트는 테스트와 로드 후 빠르게 입력하는 사람 모두에게 손실됩니다. 앱은 마운트 전에 입력된 내용을 유지하고 라이브 상태임을 알려야 합니다(
data-mounted). 테스트는 이 플래그를 기다립니다. - 휴대폰 너비를 확인하세요. 390px에서
document.documentElement.scrollWidth === innerWidth를 확인한 다음 스크린샷을 확인하세요. - 실제 데이터를 절대 건드리지 말고, 증명하세요. 앱을 데이터의 리플링크 복사본(
cp -a --reflink=auto)과 여분의 포트로 지정하세요. 그런 다음 신뢰하기 전에 격리를 증명하세요: 복사본에서 한 가지 이름을 바꾸고(카나리아: 팀 "AssociésX"), 그것에 대해 액션을 취하고, 복사본이 변경되었고 실제 파일의 타임스탬프가 변경되지 않았는지 확인하세요. 쓰는 프로세스 유형(페이지 핸들러, 라이브 소켓, 백그라운드 작업)당 한 번씩 수행하세요. Beam Lisp에서 테스트의System/put_env는 페이지 핸들러에 도달하지만 라이브 소켓에는 도달하지 않습니다: 구성된 데이터 폴더를 읽지 않는 소켓은 기본값(실제 데이터)으로 폴백됩니다. 앱은 서버가 시작될 때 구성(persistent_term)을 VM 전체에 고정하고 거기에서 읽어야 합니다. 라이브 데이터를 건드려야 하는 경우 파일을 스냅샷하고, 액션을 취하고, 변경 사항만 확인하기 위해 diff를 수행하고, 복원하세요.
프로브에서 영화로
scenario.demo/defdemo는 일반 테스트입니다(bl test). 단계는 일대일로 매핑됩니다:
| 프로브 | 시나리오 |
|---|---|
(page/navigate! p url) | (s/does :othman [:navigate url]) |
(page/wait! p js) | (s/does :othman [:wait js]) |
(page/click! p sel) / hover! / fill! / key! | [:click sel] [:hover sel] [:fill sel text] [:key "Enter"] |
| 눈을 위한 일시 중지 | [:pause ms] |
true여야 하는 (page/evaluate! p js) | (s/sees :othman (fn [p _] (= true (page/evaluate! p js))) "the experience") |
| 사람 | browser.film/owned-worlds의 배우, 열 때 신원을 설정하도록 래핑됨 |
| 내레이션 단락 | (demo/scene :id {:title … :say … :capture :video} events…) |
templates/feature-demo.bl은 완전한 파일입니다: 앱 데이터의 격리된 복사본, 테스트 VM의 여분 포트에서 시작된 앱, 신원이 있는 녹화된 월드, 음성 키가 없을 때 무음 트랙, 세 개의 장면이 있는 하나의 데모. 작업 예제: knowledger-interface-review/apps/practice/test/features-demo.bl (다섯 개의 영화: 리뷰, 내역, 조각, 두 사람 간에 라이브로 변경된 팀, 테이프에 녹화된 대화).
장면을 수정이 아닌 경험으로 작성하세요: "§-링크 핸들러 수정"이 아니라 "각 포인트는 해당 위치로 연결됩니다". 내레이션이 문서입니다.
모델 턴을 테이프에 녹화
에이전트가 수행하는 모든 것은 앱 자체의 도구 도어를 통해 이루어져야 하므로, 모델의 선택만 모델의 것입니다. 테이프는 하나의 모델 실행에 대해, 모델, 시스템 프롬프트 및 프롬프트의 해시(시계, 에포크와 같은 숫자 및 16진수 ID는 마스킹됨)로 키가 지정된, 순서대로 도어 호출과 최종 답변을 보관합니다.
record: 테이프에 있는 것을 재생하고, 없는 것은 모델을 실행하고, 보관합니다.replay(기본값): 테이프에 없는 실행은 오류 턴입니다. 리플레이는 절대 조용히 모델로 폴백되지 않습니다.- 리플레이는 동일한 도어를 통해 동일한 호출을 속도 조절하여 수행하므로, 제품이 다시 작업을 수행하고 화면에 표시되며, 몇 초 만에 무료로 이루어집니다.
프롬프트에 도달하는 모든 것(회사 이름, 날짜가 있는 경우)을 테스트 데이터에 고정하세요. 그렇지 않으면 키가 달라집니다. 테이프를 커밋하세요. 참조 구현: apps/practice/src/practice/cassette.bl 및 conversation.bl의 두 훅(턴 및 도어).
실행 및 렌더링
bl test -p ~/projects/beam-lisp/optional/browser-port/bl app/test/features-demo.bl # 리플레이, 증명, 캡처
DEMO_RECORD=1 bl test -p … # 누락된 모델 턴을 테이프에 넣기 (모델 자격 증명 필요)
BL_DEMO_EXPORT=1 bl test -p … # 또한 영화당 MP4 + 자막 + 챕터 렌더링
각 영화는 픽스처에서 마지막 테이크를 지웁니다. 녹화는 절대 덮어쓰지 않기 때문입니다. 각 내레이션을 두 개의 자막 줄(약 130자) 이내로 유지하세요. 그렇지 않으면 렌더러가 거부합니다. 로드된 머신은 스크린캐스트 인코더를 굶길 수 있습니다: frames/recordings.json을 확인하세요. 여기서 encodedFrames는 약 초 × fps여야 하며, 테이크가 짧으면 다시 실행하세요.
렌더링(scenario.film/export!)에는 Python 3과 libass가 있는 FFmpeg가 필요합니다. DEEPGRAM_API_KEY가 설정되면 내레이션이 음성으로 출력됩니다. 그렇지 않으면 각 장면은 자막을 읽는 데 걸리는 시간만큼의 무음 트랙을 얻습니다. 음성 키가 존재한다고 가정하지 마세요. 루프백에서 (scenario.demo/serve! dir {:port 4163})로 영화를 탐색하거나, work serve로 게시하세요.
완료되었다고 말하기 전에
- 각 보고된 항목에는 경험 문장, 원인, 그리고 실제 브라우저에서 각 관련 사용자로서 이를 증명하는 클레임이 있습니다.
- 데스크톱 및 휴대폰 너비가 스크린샷에서 확인되었으며, 단언만 한 것이 아닙니다.
- 터치한 모든 화면에 대해 첫 페인트와 패치된 페이지가 모두 확인되었습니다.
- 라이브 동작이 중요한 경우 두 사람으로 증명되었습니다.
- 테스트로 인해 변경된 실제 데이터가 없습니다. 건드린 모든 것은 복원되었으며, diff가 표시됩니다.
- 영화는 네트워크 없이 테이프에서 재생됩니다. 테이프와 시나리오는 커밋되며, 렌더링은 커밋되지 않습니다.
- 요약은 사용자가 이제 무엇을 찾을 수 있는지로 시작합니다.