dataworks
요약. dataworks: 자율 개선 회차 29회, 릴리즈 16건. 최근 릴리즈 v0.9.53 (자산 1개). 건강 B
- 29회차
- 1프로젝트
- 16배포 준비 완료
- 0릴리즈 진행 중
- 1병합 완료
- 1검토 대기
- 1검증 실패
- 9변경 없음
- 1실행 오류
- $79.53비용
- 3시간 12분에이전트 시간
현황
- 저장소
- https://github.com/hkjang/dataworks
- 마지막 회차
- 2026-09-12 16:09 KST — 🚀 릴리즈 merged PR #18, released v0.9.53
- 최근 릴리즈
- v0.9.53 — released · 자산 1개 (이전 v0.9.52: 1개) 전체 릴리즈 →
회차 이력
| 일시 | 프로젝트 | 결과 |
|---|---|---|
| 2026-09-12 16:09 | dataworks | 배포 준비 완료 merged PR #18, released v0.9.53 |
| 2026-09-11 02:07 | dataworks | 검토 대기 review held, PR open PR #17 |
| 2026-09-10 19:36 | dataworks | 배포 준비 완료 merged PR #16, released v0.9.51 |
| 2026-09-10 11:56 | dataworks | 배포 준비 완료 merged PR #15, released v0.9.50 |
| 2026-09-09 15:36 | dataworks | 배포 준비 완료 merged PR #14, released v0.9.49 |
| 2026-09-09 15:21 | dataworks | 실행 오류 hold: budget |
| 2026-09-09 07:46 | dataworks | 배포 준비 완료 merged PR #13, released v0.9.48 |
| 2026-09-09 00:37 | dataworks | 배포 준비 완료 merged PR #12, released v0.9.47 |
| 2026-09-08 19:47 | dataworks | 배포 준비 완료 merged PR #11, released v0.9.46 |
| 2026-09-08 17:29 | dataworks | 배포 준비 완료 merged PR #10, released v0.9.45 |
| 2026-09-07 12:01 | dataworks | 배포 준비 완료 merged PR #9, released v0.9.44 |
| 2026-09-07 10:00 | dataworks | 배포 준비 완료 release-only, released v0.9.43 |
| 2026-09-07 00:54 | dataworks | 병합 완료 merged PR #8, release blocked (secrets) |
| 2026-09-06 13:08 | dataworks | 검증 실패 verify failed: secrets in diff |
| 2026-09-05 02:05 | dataworks | 배포 준비 완료 merged PR #7, released v0.9.42 +1 assets |
| 2026-09-04 11:13 | dataworks | 변경 없음 assets-only v0.9.36, assets for v0.9.36 +1 assets |
| 2026-09-04 11:11 | dataworks | 변경 없음 assets-only v0.9.37, assets for v0.9.37 +1 assets |
| 2026-09-04 11:09 | dataworks | 변경 없음 assets-only v0.9.38, assets for v0.9.38 +1 assets |
| 2026-09-04 11:08 | dataworks | 변경 없음 assets-only v0.9.39, assets for v0.9.39 +1 assets |
| 2026-09-04 11:06 | dataworks | 변경 없음 assets-only v0.9.40, assets for v0.9.40 +1 assets |
| 2026-09-04 10:42 | dataworks | 변경 없음 assets-only, assets for v0.9.41 +1 assets |
| 2026-09-04 07:00 | dataworks | 변경 없음 assets-only, assets failed |
| 2026-09-04 06:20 | dataworks | 배포 준비 완료 merged PR #6, released v0.9.41 |
| 2026-09-03 22:35 | dataworks | 변경 없음 no change |
| 2026-09-03 14:18 | dataworks | 배포 준비 완료 merged PR #5, released v0.9.40 |
| 2026-09-03 08:26 | dataworks | 배포 준비 완료 merged PR #4, released v0.9.39 |
| 2026-09-03 08:16 | dataworks | 배포 준비 완료 merged PR #3, released v0.9.38 |
| 2026-09-03 02:29 | dataworks | 배포 준비 완료 merged PR #2, released v0.9.37 |
| 2026-09-02 14:00 | dataworks | 변경 없음 no change |
비용·사용량
| 시각 | 프로젝트 | 단계 | 시간 | 턴 | 비용 | 토큰 입력/출력 | 종료 |
|---|---|---|---|---|---|---|---|
| 16:07 | dataworks | 릴리즈 | 4분 | 31 | $1.59 | 1.6M / 11K | success |
| 16:02 | dataworks | review | 4분 | 22 | $1.62 | 1.2M / 13K | success |
| 15:57 | dataworks | 개선 | 19분 | 93 | $11.25 | 14.1M / 81K | success |
| 04:02 | dataworks | 릴리즈 | 6분 | 42 | $2.49 | 2.7M / 14K | success |
| 02:07 | dataworks | review | 7분 | 49 | $3.21 | 2.9M / 24K | success |
| 02:00 | dataworks | 개선 | 21분 | 112 | $12.05 | 16.2M / 76K | success |
| 19:34 | dataworks | 릴리즈 | 5분 | 30 | $1.46 | 1.4M / 11K | success |
| 19:29 | dataworks | review | 3분 | 21 | $0.92 | 742K / 9K | success |
| 19:25 | dataworks | 개선 | 5분 | 35 | $1.66 | 1.6M / 13K | success |
| 11:53 | dataworks | 릴리즈 | 5분 | 38 | $2.05 | 2.2M / 12K | success |
| 11:48 | dataworks | review | 2분 | 17 | $0.70 | 559K / 6K | success |
| 11:45 | dataworks | 개선 | 4분 | 39 | $1.91 | 1.8M / 16K | success |
| 15:33 | dataworks | 릴리즈 | 4분 | 31 | $1.57 | 1.5M / 12K | success |
| 15:28 | dataworks | review | 2분 | 19 | $0.64 | 351K / 8K | success |
| 15:25 | dataworks | 개선 | 4분 | 32 | $1.63 | 1.3M / 16K | success |
| 07:44 | dataworks | 릴리즈 | 5분 | 33 | $1.59 | 1.4M / 13K | success |
| 07:38 | dataworks | review | 3분 | 19 | $0.69 | 351K / 9K | success |
| 07:35 | dataworks | 개선 | 5분 | 42 | $2.00 | 1.8M / 18K | success |
| 00:35 | dataworks | 릴리즈 | 4분 | 27 | $1.19 | 1.0M / 10K | success |
| 00:30 | dataworks | review | 4분 | 16 | $1.07 | 653K / 11K | success |
| 00:26 | dataworks | 개선 | 6분 | 34 | $2.08 | 1.6M / 23K | success |
| 19:44 | dataworks | 릴리즈 | 4분 | 26 | $1.38 | 1.1M / 10K | success |
| 19:40 | dataworks | review | 3분 | 11 | $0.82 | 422K / 9K | success |
| 19:37 | dataworks | 개선 | 7분 | 47 | $2.86 | 2.8M / 27K | success |
| 17:26 | dataworks | 릴리즈 | 4분 | 27 | $1.38 | 1.3M / 9K | success |
| 17:22 | dataworks | review | 3분 | 20 | $1.00 | 787K / 9K | success |
| 17:18 | dataworks | 개선 | 8분 | 62 | $3.83 | 4.2M / 30K | success |
| 11:59 | dataworks | 릴리즈 | 3분 | 22 | $1.22 | 964K / 9K | success |
| 11:55 | dataworks | review | 3분 | 22 | $1.02 | 824K / 9K | success |
| 11:51 | dataworks | 개선 | 6분 | 39 | $2.43 | 2.2M / 22K | success |
아이디어 백로그 — 대기 7 / 전체 8
| 아이디어 | 가치/위험/크기 | 상태 | 메모 | 갱신 |
|---|---|---|---|---|
| internal/dataworks 도메인 함수 테스트 커버리지 보강 | 3/1/M | 대기 | domain.go 851줄 대비 domain_test.go 220줄. EvaluatePublishGateV2 의 strict/non-strict 분기(비strict 는 QueryCost 만, strict 는 OpsCost·DataProcessingCost 까지 보는 불일치 포함)와 EvaluateRetirementCandidate 가 단위 테스트 없이 HTTP 경유로만 간접 검증된다. 이번 회차 기준 여전히 유효. | 2026-09-12 |
| Playwright 캡처·e2e 를 서비스 컨테이너 기반 CI 잡으로 편입 | 3/2/M | 대기 | web/e2e 와 web/capture 스펙이 .github/workflows/ci.yml 에서 실행되지 않는다. 최소한 capture:docs 성공 여부만이라도 CI 에서 확인. 이번 회차 기준 여전히 유효. | 2026-09-12 |
| 방문 추적 탭 가이드 캡처와 레거시 콘솔 provider 별 필드 표시 | 2/1/S | 대기 | web/capture 픽스처에 /admin/tracking/status·violations 를 추가하면 ADMIN_GUIDE 4.5절에 화면을 실을 수 있다. 레거시 /admin 설정 화면은 카테고리 표를 그대로 보여 주므로 provider 와 무관한 필드가 모두 노출된다(동작에는 문제 없음). | 2026-09-12 |
| action center 가 상품이 사라진 고아 Contract Scope·Entitlement 도 그대로 집계 | 2/1/S | 대기 | handleDataWorksActionCenter 는 ListContractScopes/ListAPIEntitlements 를 상품 목록과 대조하지 않아 삭제·개명된 상품을 가리키는 행이 존재하지 않는 product_key 로 액션에 실린다. 상품 목록과 조인해 별도 정리 액션으로 보고하거나 제외. 이번 회차 기준 여전히 유효. | 2026-09-12 |
| 동일 API 키에 활성 엔타이틀먼트가 둘 이상일 때 운영 화면에서 경고 | 2/1/S | 대기 | 런타임은 계약까지 유효한 후보를 결정적으로 고르지만 활성 계약이 둘 다 살아 있는 상태는 대개 발급 실수. action center 활성 판정이 런타임과 같아졌으므로(store.EntitlementActive) 중복 탐지를 같은 헬퍼로 얹을 수 있다. 이번 회차 기준 여전히 유효. | 2026-09-12 |
| npm run build 가 추적 파일 web/dist/.gitkeep 을 삭제하는 문제를 vite 설정으로 해결 | 2/1/S | 대기 | vite build.emptyOutDir 조정이나 .gitkeep 재생성 스크립트로 정리 가능. 이번 회차에도 재현했고 커밋 전에 git checkout 으로 복원했다. | 2026-09-12 |
| 가이드 캡처 스펙이 하드코딩한 버전 문자열이 릴리즈마다 수동 갱신을 요구 | 2/1/S | 대기 | web/capture/pages.spec.ts 가 '서비스 버전 v0.9.51' 을 문자열로 단언하고 dataworks-demo-fixtures.ts 의 DEMO_VERSION 도 같은 값을 박아 둔다(현재 AppVersion 은 v0.9.52 라 이미 어긋남). package.json 이나 태그에서 한 곳으로 읽는 편이 맞다. | 2026-09-12 |
| 캠페인 tracking-2026-09: 관리자가 화면에서 붙이는 방문 추적 스크립트 체계(nonce CSP·출처 추출·차단 기록·Momento 같은 오리진 프록시) | 5/2/M | 완료 | internal/tracking 패키지 + tracking.* 설정 13개 + /tracking/csp-report·/admin/tracking/* + /momento/* 프록시 + SPA '방문 추적' 탭 + ADMIN_GUIDE 4.5절·PDF. 기본 꺼짐. 레거시 /admin 은 인라인 핸들러 455개라 CSP 없이 include_admin 시 삽입만. 실제 SPA 빌드로 Chromium 검증 18/18. | 2026-09-12 |
원장 (에이전트가 남긴 기록)
2026-09-02
- 선택: API surface audit 복구 + 미문서화 라우트 86건 카탈로그 등록 (가치 4 / 위험 2 / 작업량 M)
- 결과: 성공
- 요약:
cmd/api-surface-audit가 리브랜딩으로 사라진cmd/vibe/main.go·sdk/typescript/vibe.ts를 읽으며 경고 후 빈 문자열을 반환해, CLI/SDK 계약 검사가 0건 기준으로 항상 통과하는 무력 상태였다. 소스 경로를 정정하고 읽기 실패를 즉시 실패로 바꿨으며, mux 에 등록됐지만apiEndpoints에 없어/openapi.json에서 빠져 있던 라우트 86건을 핸들러가 실제 허용하는 메서드대로 추가했다. 같은 불변식을go test ./...안의 회귀 테스트(라우트 커버리지·죽은 항목·항목 형식)로 고정했고, 가짜 라우트를 넣어 가드가 실제로 실패하는 것까지 확인했다. 검증:go build ./...,go test ./...전체 통과,go run ./cmd/api-surface-auditgap 0 종료. - 보류 아이디어: GitHub Actions CI 부재(build/vet/test/api-surface-audit 게이트 추가) /
go vet ./...잔여 4건(테스트의 err 미검사 3건, json 태그 중복 1건) 정리 /gofmt -l미정렬 64개 파일 일괄 포맷 /parseWindow가 음수·과대 duration 을 그대로 받아 미래 시각 since 를 만드는 엣지케이스 /internal/dataworks패키지 테스트 커버리지 보강 - 릴리즈: v0.9.36 (2026-09-02)
2026-09-03
- 선택: GitHub Actions CI 게이트 추가 +
go vet ./...잔여 4건 정리(그중 1건은 실제 응답 버그) (가치 4 / 위험 1 / 작업량 M) - 결과: 성공
- 요약: 저장소에 워크플로가 하나도 없어 README와
cmd/api-surface-audit주석이 전제하는 검사들이 아무것도 강제되지 않았다..github/workflows/ci.yml에 Go 잡(build·vet·test·api-surface-audit)과 Web 잡(npm ci·lint·test·build)을 추가하고 README 에 백엔드 검증 명령을 문서화했다. vet 를 하드 게이트로 만들기 위해 잔여 4건을 정리했는데,admin_k8s_collect_slo.go의 json 태그 중복은 실제 버그였다:failView가store.K8sCollectRun과analyzer.CollectGap을 같은 깊이로 임베드해 두category태그가 충돌, encoding/json 이recent_failures[].category를 통째로 누락시켜 Collect Gap RCA 화면에 원인이 표시되지 않았다. 분류 필드를 평탄하게 명시하고 회귀 테스트를 추가했으며, 옛 구조로 되돌려 테스트가category=nil로 실제 실패하는 것까지 확인했다. 검증:go build ./...·go vet ./...(0건)·go test ./...전체 통과,go run ./cmd/api-surface-auditgap 0, web 에서npm ci && npm run lint && npm test && npm run build전부 통과(vitest 14건). - 보류 아이디어:
gofmt -l미정렬 64개 파일 일괄 포맷(현재 CI 게이트 제외) /parseWindow가 음수·과대 duration 을 그대로 받아 미래 시각 since 를 만드는 엣지케이스(30여 개 엔드포인트 영향) /internal/dataworks패키지 테스트 커버리지 보강 / Playwright e2e 를 서비스 컨테이너 기반 CI 잡으로 편입 /npm run build가 추적 파일web/dist/.gitkeep을 삭제하는 문제를 vite 설정으로 해결 - 릴리즈: v0.9.37 (2026-09-03)
2026-09-03 (2)
- 선택:
parseWindow가 음수·0 duration 을 그대로 받아 미래 시각 since 를 만드는 엣지케이스 수정 (가치 4 / 위험 1 / 작업량 S) - 결과: 성공
- 요약:
internal/proxy/admin_analytics.go의parseWindow가time.ParseDuration결과를 부호 검사 없이 적용해?window=-24h·?window=0s같은 값이 since 를 현재 시각 이후로 밀어냈고, 이 함수를 쓰는 70여 개 호출부(analytics·cost·MCP·personalization·xview 등)가 기본 윈도우로 폴백하는 대신 빈 데이터를 반환했다. 양수 lookback 만 허용하고 그 외에는 호출부 fallback 을 쓰도록 고쳤으며, 유일하게 fallback 0(“무제한” 의도)을 넘기는mcpFilterFromQuery를 위해 비양수 결과는 store 계층 관례대로 zero time 을 반환하게 했다. 검증: 단위 테스트 2개 +/admin/timeseries?window=-24hHTTP 회귀 테스트를 추가하고 옛 코드로 되돌려 3개 모두 실제 실패하는 것을 확인,go build ./...·go vet ./...·go test ./...전체 통과,go run ./cmd/api-surface-auditgap 0. - 보류 아이디어:
gofmt -l미정렬 64개 파일 일괄 포맷(현재 CI 게이트 제외) /internal/dataworks패키지 테스트 커버리지 보강 / Playwright e2e 를 서비스 컨테이너 기반 CI 잡으로 편입 /npm run build가 추적 파일web/dist/.gitkeep을 삭제하는 문제를 vite 설정으로 해결 /parseWindow의 미사용bucket인자 제거(호출부 70곳 일괄 정리) - 릴리즈: v0.9.38 (2026-09-03)
2026-09-03 (3)
- 선택:
tokenSet이 한글 단어를 전부 구분자로 버려 한국어 제품의 고객 적합도 점수가 낮게 나오는 버그 수정 (가치 4 / 위험 2 / 작업량 S) - 결과: 성공
- 요약:
internal/dataworks/domain.go의tokenSet이[a-z0-9_]이외 모든 룬을 구분자로 취급해, 한글로 작성된name_ko·description·pain_points는 토큰이 하나도 생성되지 않았다(실측tokenSet("여신 승인 신용 위험 스코어") == map[]). 그 결과ComputeCustomerFitScore의 “shared positioning terms” 항목(최대 +35 및product_positioning·segment_pain_pointsevidence ref)이 한국어 우선 제품에서는 세그먼트 pain point 가 글자 그대로 일치해도 절대 발동하지 않았고, 동일 내용의 영어 제품은 70+, 한국어 제품은 66 을 받았다. 구분자 판정을unicode.IsLetter/IsDigit기반으로 바꾸고 최소 토큰 길이를 바이트가 아닌 룬 수로 세도록 해(1음절 조사·단일 ASCII 문자를 동일하게 제외) ASCII 동작은 그대로 유지했다. 검증: 한국어 적합도 회귀 테스트(무관한 세그먼트는 여전히 더 낮게 나오는지 포함)와tokenSet단위 테스트를 추가하고 옛 코드로 되돌려 둘 다 실제 실패하는 것을 확인,go build ./...·go vet ./...(0건)·go test ./...전체 통과,gofmt -l internal/dataworks클린,go run ./cmd/api-surface-auditgap 0. - 보류 아이디어:
gofmt -l미정렬 64개 파일 일괄 포맷(현재 CI 게이트 제외) / Playwright e2e 를 서비스 컨테이너 기반 CI 잡으로 편입 /npm run build가 추적 파일web/dist/.gitkeep을 삭제하는 문제를 vite 설정으로 해결 /parseWindow의 미사용bucket인자 제거(호출부 70곳 일괄 정리) /bestApprovalStatus는ExpiresAt파싱 실패를 expired 로,expiredAt은 not-expired 로 처리하는 불일치 정리 - 릴리즈: v0.9.39 (2026-09-03)
2026-09-03 (4)
- 선택: 데이터 상품 접근 창(access window) 타임스탬프 검증 부재와 admin/runtime 판정 불일치 수정 (가치 4 / 위험 2 / 작업량 M)
- 결과: 성공
- 요약:
POST /admin/dataworks/products/{key}/contract-scopes와.../entitlements는valid_from·valid_to·expires_at를 형식 검사 없이 그대로 저장했는데(바로 옆 approval trace 핸들러는 RFC3339 검증을 함), 런타임 게이트entitlementActive/contractScopeActive는 파싱 실패를 inactive 로 처리한다. 그래서"2026-12-31"같은 자연스러운 입력이 200 으로 저장된 뒤/v1/data-products/{key}/query가 전부 403(inactive_entitlement/contract_scope_inactive)이 되었다. 반대로 admin 쪽entitlementExpired·dataworks.expiredAt은 파싱 실패를 “만료 아님” 으로, action center 는 파싱 불가valid_to를 아예 건너뛰어, 런타임이 거부하는 규칙이 화면에서는 정상으로 보이고 retirement 점수에서도 활성 사용으로 집계됐다. 쓰기 경로에 기존 approval trace 와 동일한 메시지·에러코드(invalid_valid_to등) 검증을 추가하고 trim 후 저장하며, admin 판독기 3곳을 런타임과 같은 fail-closed 로 맞춰 레거시 행이 만료로 노출되게 했다. 검증: HTTP 회귀 테스트 2개(형식 거부, action center 노출)와 도메인 단위 테스트 1개를 추가하고 옛 동작으로 되돌려 각 단언이 실제로 실패하는 것을 확인,go build ./...·go vet ./...(0건)·go test ./...전체 통과, 수정 파일gofmt -l클린,go run ./cmd/api-surface-auditgap 0. - 보류 아이디어:
gofmt -l미정렬 64개 파일 일괄 포맷(현재 CI 게이트 제외) / Playwright e2e 를 서비스 컨테이너 기반 CI 잡으로 편입 /npm run build가 추적 파일web/dist/.gitkeep을 삭제하는 문제를 vite 설정으로 해결 /parseWindow의 미사용bucket인자 제거(호출부 70곳 일괄 정리) / 계약·엔타이틀먼트 만료 임박 기준(30일 고정)을 쿼리 파라미터로 노출 - 릴리즈: v0.9.40 (2026-09-03)
2026-09-04
- 선택: Contract Scope
rate_limit(계약별 분당 호출 한도)이 런타임에서 전혀 강제되지 않던 문제 수정 (가치 5 / 위험 2 / 작업량 M) - 결과: 성공
- 요약:
dw_contract_scopes.rate_limit은 저장되고, 런타임 응답rate_limit필드로 반환되고,BuildDynamicOpenAPIDocument가 상품 OpenAPI 에429 Rate limit exceeded로 광고까지 하는데POST /v1/data-products/{key}/query는 값을 읽기만 할 뿐 검사하지 않아, 분당 60회로 계약한 고객도 무제한 호출이 가능했고dw_usage_metering.over_limit_calls는 설계만 되고 영원히 0 이었다. 계약 키별 고정 1분 창(벽시계 분 경계 정렬) 카운터를internal/proxy/dataworks_ratelimit.go에 추가해 허용 호출에는X-DataWorks-RateLimit-{Limit,Used,Reset}를, 초과 호출에는429 contract_rate_limited+Retry-After를 반환하게 했다. 거부된 호출은 창을 소모하지 않고rate_limit_exceeded:<contract>로 감사 로그에 남으며failed_calls가 아니라over_limit_calls로 집계된다(IncrementUsageMetering에overLimit파라미터 추가). 음수rate_limit은 런타임에서 “무제한” 으로 읽히므로 쓰기 경로에서400 invalid_rate_limit으로 거부하도록 했다. 검증: 리미터 창 롤오버·Retry-After 올림 단위 테스트와 HTTP 회귀 테스트 2개(3번째 호출 429·메터링 집계, 음수 입력 거부)를 추가하고 enforcement 를 꺼서 둘 다 실제 실패하는 것을 확인,go build ./...·go vet ./...(0건)·go test ./...전체 통과, 수정 파일gofmt -l클린(기존 CRLF 파일 제외),go run ./cmd/api-surface-auditgap 0. README 에 Contract Rate Limit 절 추가. - 보류 아이디어:
gofmt -l미정렬 64개 파일 일괄 포맷(현재 CI 게이트 제외) / Playwright e2e 를 서비스 컨테이너 기반 CI 잡으로 편입 /npm run build가 추적 파일web/dist/.gitkeep을 삭제하는 문제를 vite 설정으로 해결 / 런타임 entitlement scope 검사가strings.Contains(scope,"query")라no-query같은 값도 통과하는 문제 / 계약·엔타이틀먼트 만료 임박 기준(30일 고정)을 쿼리 파라미터로 노출 - 릴리즈: v0.9.41 (2026-09-04)
2026-09-05
- 선택: 런타임 entitlement
scope를 부분 문자열로 검사해 조회 권한 없는 계약도 통과하던 문제 수정 (가치 4 / 위험 1 / 작업량 S) - 결과: 성공
- 요약:
internal/proxy/dataworks_runtime.go의POST /v1/data-products/{key}/query게이트가strings.Contains(strings.ToLower(ent.Scope), "query")로 권한을 판정해,no-query·query:denied·data_product:subquery처럼 조회를 허용하지 않거나 명시적으로 거부하는 scope 가 그대로 통과해 상품 데이터가 반환됐다(fail-open). 반대로 문서화된 와일드카드data_product:*는*와 정확히 같지도 “query” 를 포함하지도 않아 403scope_denied로 막혔다. scope 를 쉼표·세미콜론·파이프·공백 구분 권한 목록으로 읽고 각 권한을*·query·data_product:*·data_product:query와 정확히 비교하는entitlementAllowsQuery로 분리했으며, 빈 scope 는 필드 도입 이전 행을 위해 기존대로 무제한 유지했다(다른 판독부는 없어 admin 화면과의 불일치 없음). 검증: 권한 판정 테이블 단위 테스트와 HTTP 회귀 테스트(거부 마커 403scope_denied, 와일드카드 200)를 추가하고 옛 substring 로직으로 되돌려 둘 다 실제 실패하는 것을 확인,go build ./...·go vet ./...(0건)·go test ./...전체 통과,go run ./cmd/api-surface-auditgap 0, 수정 파일 gofmt 상태는 변경 전과 동일(기존 CRLF 파일).docs/OPERATIONS.md에 허용 권한 목록을 명시하고 v0.9.42 로 릴리즈 정렬(web 변경은 e2e/capture 픽스처의 버전 문자열뿐이라 node_modules 부재로 웹 체크는 미실행). - 보류 아이디어:
gofmt -l미정렬 64개 파일 일괄 포맷(현재 CI 게이트 제외) / Playwright e2e 를 서비스 컨테이너 기반 CI 잡으로 편입 /npm run build가 추적 파일web/dist/.gitkeep을 삭제하는 문제를 vite 설정으로 해결 / 계약·엔타이틀먼트 만료 임박 기준(30일 고정)을 쿼리 파라미터로 노출 /parseWindow의 미사용bucket인자 제거(호출부 70곳 일괄 정리) - 릴리즈: v0.9.42 (2026-09-05)
- 릴리즈: v0.9.42 (2026-09-05)
2026-09-06
- 선택: 런타임이 강제하지 않는 계약
masking_policy를 마스킹 근거로 인정하던 문제 수정 (가치 4 / 위험 2 / 작업량 M) - 결과: 성공
- 요약:
applyMasking이 구현한 정책은redact·hash뿐이고 그 외 값은default분기에서 원본 값을 그대로 반환하는데,POST /admin/dataworks/products/{key}/contract-scopes는masking_policy를 형식 검사 없이 저장했고 민감 상품 Publish Gate 는 비어 있지 않고none이 아니면 무조건 마스킹 구성으로 집계했다. 그래서"개인 단위 원천값 제외"같은 서술형 문구나redact_pii같은 오타를 저장하면masking_configured가 통과해 민감 상품이 게시되지만POST /v1/data-products/{key}/query응답은 마스킹 없이 원본 샘플 값을 그대로 내보냈다(fail-open). 쓰기 경로에서 런타임이 실제로 이해하는none(빈 값 포함)·redact·hash만 허용하고 그 외에는400 invalid_masking_policy로 거부하며 소문자·trim 정규화 후 저장하도록 했고, Publish Gate 판독기도applyMasking이 값을 실제로 바꾸는 정책만 세도록 맞춰 레거시 서술형 행이masking_configured=false(missing_evidence: masking_policy) 로 노출되게 했다. 검증: HTTP 회귀 테스트 2건(쓰기 거부+정규화 저장, Publish Gate 판정)과 정책 분류 단위 테스트, 허용 목록과applyMasking동기화 테스트를 추가하고 옛 동작으로 되돌려 두 회귀 테스트가 실제로 실패하는 것을 확인,go build ./...·go vet ./...(0건)·go test ./...전체 통과,go run ./cmd/api-surface-auditgap 0, web 에서npm ci && npm run lint && npm test && npm run build전부 통과(vitest 14건).docs/OPERATIONS.md에 허용 값을 명시하고 v0.9.43 으로 릴리즈 정렬. - 보류 아이디어:
gofmt -l미정렬 64개 파일 일괄 포맷(현재 CI 게이트 제외, 대부분 CRLF) / Playwright e2e 를 서비스 컨테이너 기반 CI 잡으로 편입 /npm run build가 추적 파일web/dist/.gitkeep을 삭제하는 문제를 vite 설정으로 해결(이번 회차에도 재현·수동 복원) / 계약·엔타이틀먼트 만료 임박 기준(30일 고정)을 쿼리 파라미터로 노출 /parseWindow의 미사용bucket인자 제거(호출부 70곳 일괄 정리) - 릴리즈: v0.9.43 (2026-09-06)
2026-09-07
- 선택: 한 API 키가 같은 상품에 엔타이틀먼트를 여러 개 가질 때 활성 행 대신 최근 행을 평가하던 문제 수정 (가치 4 / 위험 2 / 작업량 M)
- 결과: 성공
- 요약:
dw_api_entitlements는id만 PRIMARY KEY 라 admin POST 마다 새 행이 생겨 한 API 키가 같은 상품에 여러 엔타이틀먼트를 가질 수 있는데(만료된 체험 옆에 발급한 갱신, 감사용으로 남긴revoked행),store.FindAPIEntitlement는ORDER BY updated_at DESC LIMIT 1로 마지막에 쓰인 행 하나만 읽었다. 그래서 활성 계약이 revoked/만료 행보다 먼저 쓰였다면 유효한 권한이 있는데도POST /v1/data-products/{key}/query가 403inactive_entitlement로 막혔고, 활성 계약이 둘이면 어느 쪽allowed_fields·rate_limit·과금으로 처리될지 임의였다(신규 두 행은updated_at이 같은 값으로 저장될 수 있어 순서가 비결정적). 활성·미만료 행을 우선하고 그중 가장 최근 것을 고르도록 바꿨으며, 활성 행이 없으면 종전처럼 최근 행을 돌려줘 게이트가missing_entitlement가 아닌inactive_entitlement로 응답하게 유지했다. 활성 판정은store.EntitlementActive(파싱 불가 만료일은 만료로 취급) 하나로 모으고 런타임 게이트entitlementActive가 이를 위임하게 해 선택 규칙과 게이트 규칙이 어긋나지 않도록 했다. 검증: 스토어 선택 테스트(활성 우선, 활성 부재 시 최근 행 반환)·EntitlementActive표 테스트·HTTP 회귀 테스트(만료 행이 나중에 쓰여도 200 이고 활성 계약 키로 처리)를 추가하고 활성 우선 로직을 꺼서 두 회귀 테스트가 실제로 실패하는 것을 확인,go build ./...·go vet ./...(0건)·go test ./...전체 통과, 수정·신규 파일gofmt -l클린,go run ./cmd/api-surface-auditgap 0.docs/OPERATIONS.md5절에 선택 규칙을 문서화했다. web 변경이 없어 웹 체크는 미실행이고, 직전 회차(v0.9.43)가 아직 main 에 병합되지 않아 버전 충돌을 피하려 이번 회차는 릴리즈 커밋 없이 수정 커밋만 남겼다. -
보류 아이디어: Playwright e2e 를 서비스 컨테이너 기반 CI 잡으로 편입 /
npm run build가 추적 파일web/dist/.gitkeep을 삭제하는 문제를 vite 설정으로 해결 / 계약·엔타이틀먼트 만료 임박 기준(30일 고정)을 쿼리 파라미터로 노출 /contractResponseFields가allowed_fields와 다른 대소문자로 요청한 필드를 요청 표기 그대로 응답(계약 표기로 정규화 필요) /internal/dataworks도메인 함수(EvaluatePublishGateV2·EvaluateRetirementCandidate) 단위 테스트 보강 - 릴리즈: v0.9.43 (2026-09-07, run 2026-09-07-095229-dataworks-release)
2026-09-07
- 선택: 데이터 상품 조회 응답 키를 계약 표기로 정규화 +
allowed_fields없는 Contract Scope 저장 차단 (가치 3 / 위험 1 / 작업량 S) - 결과: 성공
- 요약:
contractResponseFields는 요청 필드를allowed_fields와 대소문자 무시로 대조하면서 응답data의 키는 클라이언트가 보낸 표기를 그대로 썼다. 그런데BuildDynamicOpenAPIDocument는allowed_fields를 그대로 응답 프로퍼티 이름이자required목록으로 선언하므로, 계약이score인데Score로 요청하면 상품 자신의 OpenAPI 문서에 없는 키가 내려가고 required 키는 빠져 스키마 검증하는 소비자가 깨졌다. 매칭된 계약 표기를 돌려주도록 고쳤다. 같은 경로에서POST /admin/dataworks/products/{key}/contract-scopes가allowed_fields없이도 200 으로 저장하는 것을 발견했는데, 그런 계약은 런타임에서 모든 조회를 403(empty_contract_scope/forbidden_fields)으로 막으면서 admin 목록에는 활성 계약으로 보인다. 접근 창·마스킹 정책 검증과 같은 관례로400 invalid_allowed_fields거부와 trim·대소문자 무시 중복 제거 정규화를 추가했다. 검증: 표기 정규화 표 테스트, 런타임 HTTP 회귀 테스트(Score·RISK_BAND요청 →score·risk_band응답), admin 쓰기 회귀 테스트(빈 목록 거부·미저장, 중복 정규화 저장)를 추가하고 각각 옛 동작으로 되돌려 실제로 실패하는 것을 확인,go build ./...·go vet ./...(0건)·go test ./...전체 통과,go run ./cmd/api-surface-auditgap 0, 수정 파일 gofmt 상태는 변경 전과 동일(기존 CRLF 파일).docs/OPERATIONS.md5절에 문서화. web 변경이 없어 웹 체크는 미실행. -
보류 아이디어: Playwright e2e 를 서비스 컨테이너 기반 CI 잡으로 편입 /
npm run build가 추적 파일web/dist/.gitkeep을 삭제하는 문제를 vite 설정으로 해결 / 계약·엔타이틀먼트 만료 임박 기준(30일 고정)을 쿼리 파라미터로 노출 / Contract Scope 쓰기 경로에valid_from > valid_to·미지원status검증 추가 /internal/dataworks도메인 함수(EvaluatePublishGateV2·EvaluateRetirementCandidate) 단위 테스트 보강 - 릴리즈: v0.9.44 (2026-09-07, run 2026-09-07-114539-dataworks-improve)
2026-09-08
- 선택: 엔타이틀먼트 만료 임박 경고 추가 + action center 만료 예정 기준(30일 고정)을
expiring_within쿼리 파라미터로 노출 (가치 4 / 위험 1 / 작업량 M) - 결과: 성공
- 요약:
GET /admin/dataworks/action-center는 계약(Contract Scope)에만 30일 고정 예고를 주고 API Entitlement 에는 예고가 전혀 없어, 고객 API 키는 만료일에 그냥 죽고 화면에는 그 뒤에야inactive_access로 나타났다(운영자가 갱신을 준비할 신호가 0). 활성·미만료 엔타이틀먼트가 창 안에 만료되면expiring_access/entitlement_expiring로 보고하도록 하고, 분기 단위 갱신 주기를 위해 창을?expiring_within=13w|45d|72h로 지정할 수 있게 했다(응답에 적용된 창을expiring_within으로 반환). 해석 불가·비양수 값은 기본값으로 조용히 되돌리면 운영자가 물어본 창과 다른 답을 주게 되므로400 invalid_expiring_within으로 거부한다. 내장 admin UI 는 새 액션을 기존 “접근 만료” 탭에 합산·필터하고 두 접근 액션에 만료일을 표시하며, workbench 검토 화면의 summary→action type 매핑이 5개만 있어 나머지 카운터 버튼이 빈 목록을 보여주던 것도 같이 채웠다. 검증: HTTP 회귀 테스트(기본 30일에서 20일 뒤 만료 엔타이틀먼트 1건 감지·200일짜리는 미감지,13w로 60일 계약까지 감지,7d로 미감지)와 잘못된 파라미터 400 테스트,parseExpiryHorizon표 테스트를 추가하고 옛 동작(경고 없음 / 창 하드코딩)으로 되돌려 각각 실제로 실패하는 것을 확인,go build ./...·go vet ./...(0건)·go test ./...전체 통과,go run ./cmd/api-surface-auditgap 0, 수정·신규 Go 파일gofmt -l클린, web 에서npm ci && npm run lint && npm test && npm run build전부 통과(vitest 14건).docs/OPERATIONS.md5절과 README 에 문서화. 릴리즈 커밋은 남기지 않았다. -
보류 아이디어: Playwright e2e 를 서비스 컨테이너 기반 CI 잡으로 편입 /
npm run build가 추적 파일web/dist/.gitkeep을 삭제하는 문제를 vite 설정으로 해결(이번 회차에도 재현·수동 복원) / Publish Gate 의maskingConfigured가 계약이 하나도 없으면 true 로 폴백(미병합 브랜치 bf27f7d 와 같은 영역이라 병합 후 처리) / 엔타이틀먼트 선택이 계약(Contract Scope) 활성 여부까지 보도록 확장 /internal/dataworks도메인 함수(EvaluatePublishGateV2·EvaluateRetirementCandidate) 단위 테스트 보강 - 릴리즈: v0.9.45 (2026-09-08, run 2026-09-08-171056-dataworks-improve)
2026-09-08
- 선택: 엔타이틀먼트 선택이 계약(Contract Scope) 활성 여부까지 보도록 확장 (가치 3 / 위험 2 / 작업량 M)
- 결과: 성공
- 요약:
dw_api_entitlements는id단위 저장이라 한 API 키가 같은 상품에 여러 행을 가질 수 있고 각 행이 자기contract_key를 가리키는데,POST /v1/data-products/{key}/query는store.FindAPIEntitlement가 고른 한 행만 평가했다. 그래서 우선순위 1순위 행이 이미 종료된 계약을 가리키고 다른 활성 엔타이틀먼트가 살아 있는 계약을 가리키면, 실제로 유효한 접근권이 있는데도 403contract_scope_inactive가 났다. store 에 후보 전체를 활성 우선·최신 우선으로 돌려주는ListAPIEntitlementCandidates를 추가하고(FindAPIEntitlement는 그 첫 원소로 재구현), 런타임이 후보를 훑어 “엔타이틀먼트 활성 + scope 가 조회 허용 + 계약이 이 상품 것이고 active·유효기간 안 + 민감 상품이면 purpose 존재” 를 모두 만족하는 첫 행을 쓰도록 했다. 요청 본문에 좌우되는allowed_fields와 호출량을 소모하는rate_limit은 선택 기준에서 제외해 후보 순회가 분당 한도를 앞당겨 소진하지 않게 했고, 만족하는 후보가 없으면 종전대로 1순위 행으로 검사를 흘려보내inactive_entitlement·contract_scope_missing·contract_scope_inactive·missing_contract_purpose중 실제 원인이 그대로 응답되게 유지했다. 검증: HTTP 회귀 테스트(활성 두 행 중 죽은 계약 쪽이 1순위여도 200 이고 살아 있는 계약 키로 처리 / 계약이 전부 닫히면 여전히 403contract_scope_inactive)와 store 후보 정렬 테스트를 추가하고 선택 로직을 꺼서 회귀 테스트가 실제로 실패하는 것을 확인,go build ./...·go vet ./...(0건)·go test ./...전체 통과,go run ./cmd/api-surface-auditgap 0, gofmt 상태는 변경 전과 동일(dataworks_runtime.go는 기존 CRLF 파일).docs/OPERATIONS.md5절에 선택 규칙을 문서화했다. web 변경이 없어 웹 체크는 미실행이고, 릴리즈 커밋은 남기지 않았다. -
보류 아이디어: Publish Gate 의
maskingConfigured가 계약이 하나도 없으면 true, 계약이 여럿이면 그중 하나만 마스킹해도 true 로 폴백(미병합 브랜치 bf27f7d 는 origin 에 없어 사실상 폐기이므로 이제 충돌 걱정 없음) / Contract Scope 쓰기 경로에valid_from > valid_to·미지원status검증 추가 / action center 가revoked·draft상태 계약도 만료 임박으로 영구 집계 /internal/dataworks도메인 함수(EvaluatePublishGateV2·EvaluateRetirementCandidate) 단위 테스트 보강 / Playwright e2e 를 서비스 컨테이너 기반 CI 잡으로 편입 - 릴리즈: v0.9.46 (2026-09-08, run 2026-09-08-193102-dataworks-improve)
2026-09-09
- 선택: 런타임이 실제로 수행하는 마스킹만 Publish Gate 근거로 인정 (가치 4 / 위험 2 / 작업량 M)
- 결과: 성공
- 요약:
applyMasking이 구현한 정책은redact·hash뿐이고 그 외 값은 원본을 그대로 돌려주는데,POST /admin/dataworks/products/{key}/contract-scopes는masking_policy를 검증 없이 저장했고 민감 상품 Publish Gate 는 비어 있지 않고none이 아니면 마스킹 구성으로 집계해,"개인 단위 원천값 제외"같은 서술형 문구가 게시를 통과시키면서 조회 응답은 원본 값을 그대로 내보냈다(2026-09-06 회차에서 같은 수정을 했지만 그 브랜치 bf27f7d 는 origin 에 병합되지 않아 main 에는 없었다). 같은 판독기는 반대 방향으로도 fail-open 이었는데, 마스킹은 계약별로 적용되는데 계약이 여럿일 때 그중 하나만 마스킹하면 상품 전체가 통과해 나머지 고객은 게시 후에도 원본 값을 받았다. 쓰기 경로에서none(빈 값 포함)·redact·hash만 허용하고(400 invalid_masking_policy, 소문자·trim 정규화) 게이트는 “아직 조회를 처리할 수 있는 계약이 모두” 런타임이 강제하는 정책을 가질 때만 참이 되게 했다.revoked상태나 이미 닫힌 창(파싱 불가valid_to포함)은 다시는 조회를 처리할 수 없으므로 제외해 옛 계약이 영구히 게시를 막지 않게 했고, 아직 시작되지 않은 창은 나중에 서빙하므로 포함한다(contractScopeCanServe로 분리하고contractScopeActive가 이를 재사용). 검증: HTTP 회귀 테스트 3건(서술형 거부·정규화 저장, 레거시 행의 게이트 판정, 계약 둘 중 하나만 마스킹 시 차단 + 종료·미래 계약 처리)과 정책 분류·contractScopeCanServe표 테스트, 허용 목록과applyMasking동기화 테스트를 추가하고 옛 동작으로 되돌려 회귀 테스트 3건이 실제로 실패하는 것을 확인,go build ./...·go vet ./...(0건)·go test ./...전체 통과,go run ./cmd/api-surface-auditgap 0, gofmt 상태는 변경 전과 동일(dataworks_runtime.go는 기존 CRLF 파일).docs/OPERATIONS.md5절에 문서화. web 변경이 없어 웹 체크는 미실행이고, 릴리즈 커밋은 남기지 않았다. -
보류 아이디어: Contract Scope 쓰기 경로에
valid_from > valid_to·미지원status검증 추가 / action center 가revoked·draft상태 계약도 만료 임박으로 영구 집계 /internal/dataworks도메인 함수(EvaluatePublishGateV2·EvaluateRetirementCandidate) 단위 테스트 보강 / 동일 API 키에 활성 엔타이틀먼트가 둘 이상일 때 운영 화면에서 경고 / Playwright e2e 를 서비스 컨테이너 기반 CI 잡으로 편입 - 릴리즈: v0.9.47 (2026-09-09, run 2026-09-09-002104-dataworks-improve)
2026-09-09
- 선택: Contract Scope 쓰기 경로에
valid_from > valid_to·미지원status검증 추가 (가치 3 / 위험 1 / 작업량 S) - 결과: 성공
- 요약:
POST /admin/dataworks/products/{key}/contract-scopes는 두 타임스탬프의 RFC3339 형식만 보고 순서를 보지 않아valid_from이valid_to보다 뒤인 창을 200 으로 저장했는데,contractScopeActive는 현재 시각이 창 안에 있을 때만 참이므로 그런 계약은 존재하는 내내 모든 조회를 403contract_scope_inactive로 막는다.status도 store 가 빈 값만active로 채울 뿐 검증이 없어actve·enabled같은 오타가 그대로 저장돼 계약이 즉시 죽는데, 두 경우 모두 admin 목록에는 정상적인 고객 계약으로 보였다(같은 핸들러가 이미 형식·rate_limit·allowed_fields·masking_policy는 거부한다). 뒤집힌 창은400 invalid_access_window, 미지원 상태는400 invalid_contract_status(허용:active·draft·suspended·revoked)로 거부하고status를 소문자·trim 정규화한 뒤 빈 값은active로 채워 응답이 실제 저장 행과 일치하게 했다. 검증: HTTP 회귀 테스트(뒤집힌 창 거부·미저장, 오타 상태 거부·미저장, 정규화 저장, 생략 시 기본값)와contractScopeStatusKnown표 테스트를 추가하고 두 검사를 각각 꺼서 해당 단언이 실제로 실패하는 것을 확인,go build ./...·go vet ./...(0건)·go test ./...전체 통과,go run ./cmd/api-surface-auditgap 0, gofmt 상태는 변경 전과 동일(dataworks_runtime.go는 기존 CRLF 파일).docs/OPERATIONS.md5절에 문서화. web 변경이 없어 웹 체크는 미실행이고, 릴리즈 커밋은 남기지 않았다. -
보류 아이디어: action center 가
revoked·draft상태 계약도 만료 임박으로 영구 집계 /internal/dataworks도메인 함수(EvaluatePublishGateV2·EvaluateRetirementCandidate) 단위 테스트 보강 / 동일 API 키에 활성 엔타이틀먼트가 둘 이상일 때 운영 화면에서 경고 / API Entitlement 쓰기 경로에도status허용값 검증 추가(계약 쪽과 대칭) / Playwright e2e 를 서비스 컨테이너 기반 CI 잡으로 편입 - 릴리즈: v0.9.48 (2026-09-09, run 2026-09-09-073104-dataworks-improve)
2026-09-09
- 선택: action center 가
revoked·draft·suspended계약도 만료 임박으로 영구 집계하던 문제 수정 (가치 3 / 위험 1 / 작업량 S) - 결과: 성공
- 요약:
GET /admin/dataworks/action-center의contract_expiring루프는valid_to만 보고scope.Status를 보지 않아, 런타임이 서빙하지 않는 계약(draft·suspended·revoked)까지 “renew, narrow, or retire” 항목으로 보고했다. 종료한 계약의valid_to는 시간이 갈수록 과거로 멀어지기만 하므로 이 항목은 절대 사라지지 않고 만료된 창이라 심각도까지high로 고정돼, 운영 화면과expiring_contracts카운터가 의도적으로 닫은 계약으로 영구히 부풀고 정말 갱신이 필요한 활성 계약을 덮었다(바로 아래 엔타이틀먼트 루프는 이미ent.Status != "active"를 걸러낸다). 런타임의 활성 판정과 어긋나지 않도록contractScopeCanServe안에 있던 상태 검사를contractScopeStatusActive로 분리해 두 곳이 같은 규칙을 쓰게 했고, 이미 만료된 활성 계약은 종전대로high로 남겨 운영자가 원하는 신호를 유지했다(창 기준 필터가 아니라 상태 기준 필터만 추가). 검증: HTTP 회귀 테스트(90일 전 만료된revoked·draft·suspended3건 + 10일 뒤 만료active1건 →expiring_contracts=1, 액션은ct_live하나)와contractScopeStatusActive표 테스트를 추가하고 상태 검사를 꺼서 회귀 테스트가 실제로 실패(expiring_contracts = 4)하는 것을 확인,go build ./...·go vet ./...(0건)·go test ./...전체 통과,go run ./cmd/api-surface-auditgap 0, gofmt 상태는 변경 전과 동일(dataworks_runtime.go는 기존 CRLF 파일).docs/OPERATIONS.md5절 “만료 예정 계약·권한 확인” 에 문서화. web 변경이 없어 웹 체크는 미실행이고, 릴리즈 커밋은 남기지 않았다. -
보류 아이디어: API Entitlement 쓰기 경로에도
status허용값 검증 추가(계약 쪽과 대칭) /internal/dataworks도메인 함수(EvaluatePublishGateV2·EvaluateRetirementCandidate) 단위 테스트 보강 / 동일 API 키에 활성 엔타이틀먼트가 둘 이상일 때 운영 화면에서 경고 / action center 가 상품이 사라진 고아 Contract Scope·Entitlement 도 그대로 집계 / Playwright e2e 를 서비스 컨테이너 기반 CI 잡으로 편입 - 릴리즈: v0.9.49 (2026-09-09, run 2026-09-09-152114-dataworks-improve)
2026-09-10
- 선택: API Entitlement 쓰기 경로에도
status허용값 검증 추가(계약 쪽과 대칭) (가치 3 / 위험 1 / 작업량 S) - 결과: 성공
- 요약:
POST /admin/dataworks/products/{key}/entitlements는expires_at형식만 검사하고status는 그대로 저장해,enabled·actve같은 오타가 들어오면store.EntitlementActive가active만 인정하므로 발급 순간부터 고객 API 키가403 inactive_entitlement를 받는데 admin 목록에는 정상 발급된 접근권으로 보였다(계약 쪽invalid_contract_status와 같은 유형의 fail-closed 오설정).contractScopeStatusKnown과 대칭으로entitlementStatusKnown을 두고active(기본)·draft·suspended·revoked만 허용하며 그 외에는400 invalid_entitlement_status로 거부하고, 소문자·trim 정규화 후 빈 값은active로 채워 응답이 실제 저장 행과 일치하게 했다(감사용으로 남기는revoked행은 관례대로 계속 저장 가능). 검증: HTTP 회귀 테스트(오타 상태 거부·미저장, ` ACTIVE ` 정규화 저장,revoked허용, 생략 시 기본값이 응답·저장 행 모두active)와entitlementStatusKnown표 테스트를 추가하고 검사를 꺼서 회귀 테스트가 실제로 실패(200 으로status:"enabled"저장)하는 것을 확인,go build ./...·go vet ./...(0건)·go test ./...전체 통과,go run ./cmd/api-surface-auditgap 0, gofmt 상태는 변경 전과 동일(dataworks_runtime.go는 기존 CRLF 파일).docs/OPERATIONS.md5절에 문서화. web 변경이 없어 웹 체크는 미실행이고, 릴리즈 커밋은 남기지 않았다. -
보류 아이디어: action center 의 활성 판정이
ent.Status != "active"라 대소문자가 다른 레거시 행을 런타임과 반대로 판정(store.EntitlementActive재사용 필요) / 이미 지난expires_at으로 발급되는 죽은 엔타이틀먼트를 쓰기 경로에서 거부 /internal/dataworks도메인 함수(EvaluatePublishGateV2·EvaluateRetirementCandidate) 단위 테스트 보강 / 동일 API 키에 활성 엔타이틀먼트가 둘 이상일 때 운영 화면에서 경고 / action center 가 상품이 사라진 고아 Contract Scope·Entitlement 도 그대로 집계 - 릴리즈: v0.9.50 (2026-09-10, run 2026-09-10-114120-dataworks-improve)
2026-09-10
- 선택: action center 의 엔타이틀먼트 활성 판정을 런타임 규칙(
store.EntitlementActive)으로 통일 (가치 3 / 위험 1 / 작업량 S) - 결과: 성공
- 요약:
GET /admin/dataworks/action-center는ent.Status != "active" || entitlementExpired(...)로 대소문자·공백을 그대로 비교했지만 런타임 조회 게이트는store.EntitlementActive(status 는EqualFold+trim,expires_at은 trim 후 파싱, 해석 불가면 만료)를 쓴다. 그래서 쓰기 경로가 status 를 정규화하기(v0.9.50) 전에 저장된"Active"·" active "행은 고객 API 키가 정상적으로 서빙받는데도 운영 화면에는inactive_access로 떠서 운영자가 멀쩡한 접근권을 회수하도록 유도했고, 그런 행은 만료 예고(entitlement_expiring) 경로에도 도달하지 못해 갱신 신호까지 잃었다. 판정을 같은 패키지의 런타임 래퍼entitlementActive에 위임하고 이제 호출부가 없는entitlementExpired를 삭제했다(진짜로 닫힌revoked행은 종전대로inactive_access로 남는다). 검증: HTTP 회귀 테스트(레거시"Active"·" active "행 +revoked행 →inactive_access=1(revoked 만), 20일 뒤 만료되는 공백 포함 행이expiring_access=1로 잡힘)를 추가하고 옛 대소문자 비교로 되돌려 두 레거시 행이 모두entitlement_inactive로 보고되며 실패하는 것을 확인,go build ./...·go vet ./...(0건)·go test ./...전체 통과,go run ./cmd/api-surface-auditgap 0, 수정·신규 파일gofmt -l클린.docs/OPERATIONS.md“만료 예정 계약·권한 확인” 절에 문서화. web 변경이 없어 웹 체크는 미실행이고, 릴리즈 커밋은 남기지 않았다. -
보류 아이디어:
internal/dataworks도메인 함수(EvaluatePublishGateV2·EvaluateRetirementCandidate) 단위 테스트 보강 / action center 가 상품이 사라진 고아 Contract Scope·Entitlement 도 그대로 집계 / 동일 API 키에 활성 엔타이틀먼트가 둘 이상일 때 운영 화면에서 경고 / 이미 지난expires_at으로 발급되는 죽은 엔타이틀먼트를 쓰기 경로에서 경고·거부 / Playwright e2e 를 서비스 컨테이너 기반 CI 잡으로 편입 - 릴리즈: v0.9.51 (2026-09-10, run 2026-09-10-192115-dataworks-improve)
2026-09-11
- 선택: 사용자·관리자 가이드를 GUIDE-STANDARD 기준 완성본으로 만들고 PDF 산출 (가치 4 / 위험 1 / 작업량 M)
- 결과: 성공
- 요약: 두 가이드 모두
.md만 있고.pdf가 없었고, 표준이 요구하는 절 중 사용자 가이드의 “처음 5분”·”자주 하는 작업”·”용어”와 관리자 가이드의 환경 변수 전수 표·운영(백업·복구·로그 위치·상태 점검·업그레이드/롤백)·장애 대응·보안 절이 빠져 있었다. 저장소의 캡처 도구(web/capture, Playwright 1440x960 headless Chromium + 데모 픽스처)로 65장을 v0.9.51 화면으로 다시 찍고, 관리자 가이드 “계정과 권한” 절에 실을 화면이 없어/admin/roles·/admin/users픽스처(서버의 기본 역할·스코프·설명을 그대로 옮긴 가짜 배포본)와역할 및 권한탭 캡처를 추가했다(desktop·mobile20-admin-roles.jpg). 사용자 가이드의 “막혔을 때”는 일반론 대신 코드에서 확인한 실제 문구를 싣는다 — 로그인 실패(invalid_credentials)와 상품 런타임 API 오류 13종을 HTTP 상태·서버 메시지·조치로 표에 정리했고,forbidden_fields는 다른 코드와 응답 형태가 달라({"error":"requested fields exceed contract scope", …}) 그대로 적었다. 관리자 가이드의 환경 변수 표는internal/config/config.go에서 추출해 만들었고 코드의 137개와 문서의 137개가 정확히 일치함을 스크립트로 대조했다. PDF 는 공용 도구aidev/tools/guide/md2pdf.mjs로 생성했다(사용자 41쪽·그림 24장, 관리자 28쪽·그림 8장). 정본이 갈라지지 않도록 GitHub Pages 용docs/user-guide.html·docs/admin-guide.html이 저장소의.md/.pdf를 정본으로 가리키게 하고 README 에 PDF 링크를 넣었다. 검증: 캡처 스펙 2개(desktop·mobile) 통과(예상 밖 네트워크 요청·콘솔 오류 0), 두 가이드의 이미지 경로 32개 모두 실제 파일 존재 확인,md2pdf중간 HTML 을 headless Chromium 으로 렌더해 표지·표·코드 블록·그림 배치를 눈으로 확인,npm run lint·npm test(14건)·npm run build통과(빌드가 지운web/dist/.gitkeep복원),go build ./...·go vet ./...(0건)·go test ./...전체 통과,go run ./cmd/api-surface-auditgap 0. Go 코드 변경은 없다. -
보류 아이디어:
internal/dataworks도메인 함수(EvaluatePublishGateV2·EvaluateRetirementCandidate) 단위 테스트 보강 / action center 가 상품이 사라진 고아 Contract Scope·Entitlement 도 그대로 집계 / 동일 API 키에 활성 엔타이틀먼트가 둘 이상일 때 운영 화면에서 경고 /npm run build가 추적 파일web/dist/.gitkeep을 삭제하는 문제를 vite 설정으로 해결(이번 회차에도 재현·수동 복원) / Playwright 캡처·e2e 를 CI 잡으로 편입해 가이드 캡처가 UI 변경과 함께 갱신되게 하기 - 릴리즈: v0.9.52 (2026-09-11, run 2026-09-11-035607-dataworks-approve)
2026-09-12
- 선택: 캠페인 tracking-2026-09 — 관리자가 화면에서 붙이는 방문 추적 스크립트 체계(nonce CSP·출처 추출·차단 기록·Momento 같은 오리진 프록시) (가치 5 / 위험 2 / 작업량 M)
- 결과: 성공
- 요약: 이 저장소는 React 워크벤치(
/dataworks/*)와 레거시 콘솔(/admin)을 제공하므로 TRACKING-STANDARD 를 그대로 적용했다. 다만 CSP 가 애초에 없었고(기존script-src 'self'잠금 없음) 레거시 콘솔은 인라인 핸들러 455개·인라인 style 2,297개라 nonce 정책을 달 수 없어, 워크벤치 shell 에만script-src 'self'(+style-src 'unsafe-inline'은 React 인라인 스타일 때문에 허용, 스크립트는 절대 풀지 않음) 정책을 새로 달고 비화면 경로는default-src 'none'으로 좁혔으며 레거시 콘솔은 CSP 없이include_admin일 때만 스니펫을 넣는다(가이드에 명시). 새internal/tracking패키지가 kanpic 과 같은 능력(설정 읽기·요청별 nonce 를 모든<script>에 부착·스니펫에서 http(s) 출처 추출·100개 고리 버퍼 위반 기록·허용 목록 추가)을 제공하고, 설정은 기존 admin settings 레지스트리에tracking.*13개(카테고리tracking, 전부 기본 꺼짐, 8KB·provider·URL·placement·allowed_hosts 검증)로 얹어 런타임 스냅샷(reloadRuntimeConfig)과 멀티 파드 폴링에 그대로 편승한다. Momento 를 첫 provider 로 두고tracking.momento_proxy(기본 켜짐)면/momento/*를httputil.ReverseProxy로 수집기에 넘기며(쿠키·Authorization 제거, 추적 꺼지면 404) 스니펫에data-endpoint="/momento"를 줘 정책에 외부 출처가 등장하지 않는다. SPA 핸들러는withPage훅으로 index.html 응답에만 정책 헤더와 스니펫을 넣고 zero modtime 으로 304 를 막아 캐시된 본문과 새 nonce 가 어긋나지 않게 했다. 엔드포인트:POST /tracking/csp-report(무인증, 추적 꺼져 있으면 버림),GET /admin/tracking/status,GET|DELETE /admin/tracking/violations,POST /admin/tracking/violations/allow. SPA 설정 화면에 “방문 추적” 탭(provider 별 필드 표시, 현재 상태·미완료 사유·정책 출처, 차단된 출처 목록 + 허용/비우기)을 추가했다. 검증:internal/tracking단위 테스트 11건(기본 꺼짐·정규화·프록시/직접 스니펫·불완전 설정 비활성·nonce 부착·placement·출처 추출·허용 목록·기록 중복/축출/와일드카드)과 proxy HTTP 회귀 테스트 6건(꺼짐 시 페이지 불변+엄격 정책+API 경로 좁은 정책+프록시 닫힘 / momento 프록시 nonce 주입·요청별 새 nonce·include_admin·placement·프록시 전달 시 자격 증명 제거·끄면 원복 / custom 스니펫 출처가 script·connect·img 에 들어가고 신고→목록→허용→정책 반영 / 설정 검증 400·꺼진 동안 신고 폐기),go build ./...·go vet ./...(0건)·go test ./...전체 통과,go run ./cmd/api-surface-auditgap 0, 수정·신규 Go 파일gofmt -l클린, webnpm run lint·npm test(14건)·npm run build통과(지워진web/dist/.gitkeep복원). 추가로 빌드된 실제 SPA 를 임베드한 서버를 sqlite 로 띄우고 스텁 Momento 수집기와 headless Chromium(Playwright)으로 18개 항목을 확인했다 — 꺼짐 상태에서 실제 워크벤치가 CSP 콘솔 오류 없이 렌더, momento 프록시 모드에서 tracker.js 가 nonce 로 실행돼/momento/api/event로 비콘이 수집기에 실제 도착(쿠키 미전달), 정책에 수집기 주소 없음, 자동 추출이 못 읽는 출처('http://'+h)는 img-src 위반으로 신고돼 목록에 뜨고 허용 후 정책에 반영, 끄면 정책·프록시 원복.docs/ADMIN_GUIDE.md4.5절에 설정 방법과 CSP·nonce·차단 출처 설명을 넣고(기존 잘못된 “3.3” 앵커도 수정)aidev/tools/guide/md2pdf.mjs로 PDF 를 다시 구웠다. 릴리즈 커밋은 남기지 않았다. -
보류 아이디어: 방문 추적 탭의 가이드 캡처(픽스처 추가 후
capture:docs)와 레거시 콘솔 설정 화면의 provider 별 필드 숨김 /internal/dataworks도메인 함수(EvaluatePublishGateV2·EvaluateRetirementCandidate) 단위 테스트 보강 / action center 가 상품이 사라진 고아 Contract Scope·Entitlement 도 그대로 집계 / 동일 API 키에 활성 엔타이틀먼트가 둘 이상일 때 운영 화면에서 경고 /npm run build가 추적 파일web/dist/.gitkeep을 삭제하는 문제를 vite 설정으로 해결(이번 회차에도 재현·수동 복원) - 릴리즈: v0.9.53 (2026-09-12, run 2026-09-12-153953-dataworks-improve)