seokang-sk 0.7.2 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.MD CHANGED
@@ -201,6 +201,7 @@ sk status
201
201
  | |- sk-plan # 기획/설계 응답 워크플로
202
202
  | |- sk-counseling-feedback # 상담 녹음 로컬 전사·난이도·HTML 피드백
203
203
  | |- sk-db # DB 변경 워크플로
204
+ | |- sk-local-db # 터미널 입력 없는 격리 local DB 셋업·snapshot 워크플로
204
205
  | |- sk-design-system # 디자인 시스템 점검/구축
205
206
  | |- sk-docs # 변경 문서 작성
206
207
  | |- sk-git # git add/commit/push 워크플로
@@ -263,12 +264,15 @@ Windows Git Bash에서는 사용자 경로에 맞춰 `cd /c/Users/<user>/.agents
263
264
  - `sk identity status`: 선택적 GitHub operator 진단 정보 확인
264
265
  - `sk guard implementation --db-impact`: 이전 버전 호환용 안내. DB 코드 작성은 허용하고 실행은 금지
265
266
  - `sk guard db-execution`: local test/CI/일반 build 전에 production DB 자동 실행 경로를 strict 검사. `$sk-deploy`는 내부 deploy-packaging mode로 실행되지 않는 DB test source를 허용하고 실제 test/migration 실행만 차단
267
+ - `sk db local setup|status`: Mac/Windows 공용 Docker local DB 생성과 상태 확인
268
+ - `sk db local copy-production|restore|apply|reset`: OWNER의 sanitized snapshot과 명시 승인된 동일-workspace plain snapshot 생성·복원, managed local DB migration 검증, policy 변경 후 명시적 초기화
266
269
  - `sk guard git --action push --branch <name> --repo <name>`: OWNER main push와 NON_OWNER 작업 branch/PR 경계 확인
267
270
  - `sk guard stage pre-add|pre-commit <path ...>`: intended commit path와 Git index 일치 검사
268
271
  - `sk guard secrets --worktree` / `--staged` / `--outbound --remote origin`: add, commit, push 직전 secret hard-fail
269
272
  - `sk guard deploy --environment <name> --artifact <type> --action <action> --db-impact-status <status>`: 배포 credential 및 DB 무실행 경계 안내
270
273
  - 구현/commit/작업 branch는 identity 확인이나 cache를 요구하지 않는다. OWNER의 canonical `main`/`master` 직접 push는 GitHub ACL을 사용하고, 배포는 사용자 유형이 아니라 local credential과 원격 권한으로 실행한다. NON_OWNER Git 작업은 개인 fork PR까지만 진행한다.
271
274
  - [상세 CLI 가이드](docs/setup/sk-cli-guide.md)를 기준 문서로 사용
275
+ - [Local DB 신규 개발자 온보딩](docs/setup/local-db-onboarding.md)을 프로젝트 DB 셋업 기준으로 사용
272
276
 
273
277
  ---
274
278
 
@@ -424,7 +428,7 @@ PR 처리는 예약 Inbox 자동화 없이 `$sk-git`에서 끝냅니다. OWNER
424
428
 
425
429
  모든 배포 스크립트는 build/upload/restart/publish 전에 DB execution boundary를 확인합니다. OWNER/NON_OWNER/root/seokang 같은 사용자 유형은 판정하지 않으며, local config의 SSH/npm/API/CDN credential로 실제 명령을 실행한 결과만 사용합니다.
426
430
 
427
- 별도 테스트/개발/로컬 DB는 없습니다. DB 코드·SQL·migration 파일은 작성하고 PR로 검토할 수 있지만 Codex, local test, CI, deploy, start/restart script는 migration/DDL/DML/backfill/seed를 실행하지 않습니다. 실제 반영은 OWNER 사용자가 별도 IDE/DB 도구에서 수동 실행합니다.
431
+ 개발 DB는 `sk db local`이 검증한 프로젝트별 Docker container만 사용합니다. OWNER는 SELECT-only production credential로 raw dump를 port/volume 없는 tmpfs sanitizer에 스트리밍하고, project mask/verification을 통과한 snapshot만 다른 개발자에게 전달합니다. Raw data와 production credential은 공유하지 않습니다. CI, deploy, start/restart script는 migration/DDL/DML/backfill/seed를 실행하지 않으며 production 반영은 OWNER가 별도 IDE/DB 도구에서 수동 실행합니다.
428
432
 
429
433
  이 로컬 guard는 실수 방지층입니다. GitHub Free private 저장소의 실제 Git 차단은 `selink-lab` 원본 Read 권한과 개인 private fork/PR로 구성합니다. 배포는 local config에 credential이 설정된 환경에서만 가능하며 fork PR Actions에는 write token, secrets, variables를 전달하지 않습니다.
430
434
 
@@ -47,11 +47,12 @@
47
47
  - 복잡한 분기 이유
48
48
  - 위험한 query 의도
49
49
 
50
- ## 3-1. Production-Only DB Boundary
50
+ ## 3-1. Production / Managed Local DB Boundary
51
51
 
52
- - SeoKang 환경에는 별도 테스트/개발/로컬 DB가 없으므로 모든 DB 연결 target을 production으로 간주한다.
53
- - repository/query/SQL/migration 코드는 작성할 수 있지만 local test, CI, runtime verification에서 실제 DB에 실행하지 않는다.
52
+ - `sk db local` managed container 증거가 없는 모든 DB target은 production 또는 미검증 target으로 간주한다.
53
+ - repository/query/SQL/migration 코드는 작성할 수 있다. CI, startup, deploy에서는 DB에 실행하지 않는다.
54
54
  - DB 영향 검증은 compile/typecheck, mock 단위 테스트, SQL 정적 lint/comment 검토로 제한한다.
55
+ - 개발자가 명시적으로 준비한 managed local DB에서는 sanitized snapshot 또는 위험 고지 후 승인된 OWNER 동일-workspace plain snapshot을 복원하고 repo migration SQL을 `sk db local apply`로 검증할 수 있다. 임의 Testcontainers/host/container는 사용하지 않는다.
55
56
  - local compile/test/build 전에는 `sk guard db-execution`을 통과하고, CI에서도 test/build보다 먼저 같은 검사를 실행한다.
56
57
  - Flyway/Liquibase, ORM schema sync, seed/backfill, startup migration을 실행하지 않는다.
57
58
  - 실제 schema/data 변경은 `$sk-db` 산출물을 OWNER 사용자가 별도 IDE/DB 도구에서 수동 실행한다.
@@ -29,13 +29,14 @@
29
29
  - 직접 허용 범위는 read-only 증거 확보 목적의 `SELECT`, `SHOW`, `DESCRIBE`, `EXPLAIN`으로 한정한다.
30
30
  - 직접 조회 전 필수 조건:
31
31
  - read-only 권한 또는 동등한 안전장치 확인
32
- - SeoKang 환경에는 별도 로컬/개발/테스트 DB가 없으므로 연결 대상을 production으로 간주
32
+ - `sk db local` managed container 증거가 없으면 연결 대상을 production으로 간주
33
33
  - `LIMIT`, 선택적 `WHERE`, timeout 우선
34
34
  - 민감정보 최소 조회, 필요 시 마스킹/샘플링
35
35
  - 금지 범위:
36
36
  - `INSERT`, `UPDATE`, `DELETE`, `ALTER`, `CREATE`, `DROP`, `TRUNCATE`
37
37
  - `FOR UPDATE`, `LOCK`, transaction control, multi-statement, write-capable procedure/function 호출
38
38
  - 조건을 만족하지 못하면 직접 조회를 중단하고 사용자 실행 경로로 전환한다.
39
+ - 예외적으로 production-derived local snapshot 요청은 제8절의 `sk db local copy-production` 전용 경로를 사용한다. 이 경로도 SELECT-only credential만 허용하며 production mutation 권한을 만들지 않는다.
39
40
 
40
41
  ## 3. DB Connection Profile Convention
41
42
 
@@ -81,10 +82,15 @@
81
82
 
82
83
  - DB 관련 코드, repository/query, SQL, migration, DDL/DML, verification, rollback 파일은 OWNER/NON_OWNER/UNKNOWN 구분 없이 작성·검토·commit·개인 fork PR까지 진행할 수 있다.
83
84
  - DB 영향 구현을 시작하기 위한 GitHub identity/cache/actor gate는 사용하지 않는다. `sk guard implementation --db-impact`는 이전 설치 호환용 안내 명령일 뿐 권한 판정이 아니다.
84
- - SeoKang 환경에는 테스트/개발/로컬 DB가 따로 없고, 연결 가능한 모든 DB는 production으로 간주한다.
85
+ - 임의 host/container/Testcontainers는 production 또는 미검증 target으로 간주한다. Local write 예외는 `sk db local`이 생성하고 local Docker context, managed label, workspace/policy hash, image, `_local` DB명, loopback port를 모두 확인한 container 하나뿐이다.
85
86
  - Codex, 로컬 테스트, CI, deploy, start/restart script는 schema/data 변경을 절대 실행하지 않는다.
86
87
  - 금지되는 자동 실행에는 migration CLI, seed/backfill, SQL client write 실행, Flyway/Liquibase startup migration, Hibernate DDL create/update, ORM synchronize가 포함된다.
87
- - DB 영향 코드 검증은 DB 연결 없이 compile/typecheck, mock 단위 테스트, SQL 정적 lint/comment 검토로 제한한다. Testcontainers나 임시 DB를 새로 가정하지 않는다.
88
+ - 기본 DB 영향 코드 검증은 DB 연결 없이 compile/typecheck, mock 단위 테스트, SQL 정적 lint/comment 검토로 제한한다. 일반 Testcontainers나 임의 Docker DB를 새로 가정하지 않는다.
89
+ - 개발자가 명시적으로 `sk db local setup/restore/apply/reset`를 호출한 경우에만 managed local DB를 생성·복원·초기화하거나 repo SQL을 적용할 수 있다. `reset`은 `--replace`와 exact identity 검증을 요구하며, state가 없는 기존 object를 자동 삭제하지 않는다. 이 경로는 CI/test lifecycle/application startup/deploy에 연결하지 않는다.
90
+ - Production-derived test data는 OWNER의 SELECT-only login-path로 dump를 스트리밍해 network/port/persistent volume이 없는 tmpfs sanitizer에서 마스킹·검증한다. Raw dump는 파일로 저장하지 않고, sanitizer는 성공/실패/중단 뒤 제거하며, 정수 `0` verification과 checksum manifest를 통과한 sanitized snapshot만 다른 개발자에게 전달한다. Restore도 current mask/verification을 tmpfs에서 다시 통과한 후 persistent local volume으로 전송한다.
91
+ - 다른 개발자는 production credential을 받지 않는다. Snapshot과 manifest는 Git 밖의 승인된 비공개 경로로 전달한다.
92
+ - 사용자가 raw production data의 로컬 영구 저장 위험을 고지받은 뒤 명시적으로 승인한 OWNER PC에는 sanitized 기본 policy의 `plainSnapshot.enabled=true` opt-in을 허용한다. 생성·복원 모두 GitHub numeric-ID OWNER 확인과 `--allow-raw-production-data`가 필요하며, manifest는 `sanitized:false`, `containsProductionData:true`, `shareable:false`를 기록한다.
93
+ - Plain snapshot은 workspace 전용 `0700` 경로의 `0600` 파일로만 만들고 생성한 동일 workspace에서만 복원한다. Git, Notion, 자동 탐색, 다른 개발자 전달은 금지하며 일반 `$sk-local-db`의 기본 동작과 공유용 snapshot은 계속 sanitized 흐름을 사용한다.
88
94
  - local test와 test를 포함할 수 있는 일반 build 전, 그리고 CI workflow의 첫 검증 단계에서 strict `sk guard db-execution`을 통과해야 한다. CI에 이 단계가 없거나 DB credential marker가 있으면 실행하지 않는다.
89
95
  - `$sk-deploy`의 고정 no-test packaging은 deploy helper가 `check_db_execution_boundary.mjs --mode deploy-packaging`으로 검사한다. 이 모드에서는 DB 연결 test source의 존재만 허용하되 packaging/lifecycle이 test를 실행하거나 migration/DDL/DML/startup schema mutation 경로가 있으면 hard-fail한다.
90
96
  - fork PR workflow에는 production DB connection string, SSH/npm/API/CDN credential, write token, Actions secrets/variables를 전달하지 않는다.
@@ -7,6 +7,8 @@
7
7
 
8
8
  ## 포함 문서
9
9
 
10
+ - `local-db-onboarding.md`
11
+ - Mac/Windows 공용 local DB, production-derived sanitized snapshot, 신규 개발자 온보딩 순서
10
12
  - `sk-cli-guide.md`
11
13
  - `sk setup`, `sk sync-agents`, `sk init`, `sk check`, `sk status` 사용 흐름
12
14
  - `fork-contributor-guide.md`
@@ -97,7 +97,7 @@ $sk-bugfix 문제가 발생한 부분을 고쳐줘
97
97
  $sk-git
98
98
  ```
99
99
 
100
- DB 관련 코드, repository/query, SQL, migration, verification, rollback 파일까지 작성해 PR로 전달할 수 있다. 단 모든 DB target은 production이므로 Codex/local test/CI에서 실행하지 않는다.
100
+ DB 관련 코드, repository/query, SQL, migration, verification, rollback 파일까지 작성해 PR로 전달할 수 있다. 임의 DB target은 production 또는 미검증 target이므로 CI/startup/deploy에서 실행하지 않는다. Local 검증은 OWNER가 전달한 sanitized snapshot을 복원한 `sk db local` managed container에서만 수행하며 production credential은 받지 않는다.
101
101
  `$sk-deploy`는 사용자 유형을 판정하지 않는다. 친구 PC의 local config에 SSH/npm/배포 credential이 없으므로 실제 mutation은 원격 시스템에서 거부되며, 필요한 배포는 credential이 설정된 환경에서 실행한다.
102
102
 
103
103
  `$sk-git`은 아래를 확인해야 한다.
@@ -0,0 +1,210 @@
1
+ # Local DB Onboarding (Mac + Windows Git Bash)
2
+
3
+ ## 목적
4
+
5
+ - 개발자는 production DB에 연결하지 않고 프로젝트별 Docker local DB에서 실행·검증한다.
6
+ - 테스트 데이터는 OWNER가 production을 read-only로 복제한 뒤 마스킹·검증한 snapshot으로 공급한다.
7
+ - production credential과 raw production data는 다른 개발자에게 전달하지 않는다.
8
+ - production migration은 계속 OWNER가 별도 DB 도구에서 수동 실행한다.
9
+
10
+ ## 사용자 기본 사용법
11
+
12
+ 사용자 인터페이스는 `$sk-local-db`다. 사용자는 터미널 명령을 복사하지 않고 Codex에 결과만 요청한다.
13
+
14
+ - `$sk-local-db 친구 컴퓨터 로컬 DB 처음 셋업해줘`
15
+ - `$sk-local-db 최신 sanitized snapshot 발급해줘`
16
+ - `$sk-local-db 전달받은 snapshot으로 업데이트해줘`
17
+ - `$sk-local-db V1_81을 로컬 DB에만 적용하고 앱 확인해줘`
18
+ - `$sk-local-db 지금 로컬 DB 상태 확인해줘`
19
+
20
+ 스킬이 OS/runtime/policy/snapshot/manifest/app.env를 확인하고 아래 CLI를 내부적으로 실행한다. 이후의 CLI 예시는 구현·장애 진단 reference이며, 사용자가 직접 터미널에 입력하는 기본 절차가 아니다.
21
+
22
+ Plain `$sk-local-db`의 기본 동작은 다음과 같다.
23
+
24
+ - Managed local DB가 없으면 runtime 확인부터 setup과 가능한 snapshot restore까지 수행한다.
25
+ - 이미 있으면 OWNER PC에서는 SELECT-only production copy를 새로 마스킹·검증해 local DB를 교체한다.
26
+ - Production source가 없는 개발자 PC에서는 전달된 최신 snapshot과 manifest를 찾아 검증·복원한다.
27
+ - Snapshot을 찾을 수 없을 때는 터미널 명령이 아니라 파일 첨부 또는 비공개 전달 위치만 요청한다.
28
+
29
+ Managed identity가 검증된 local DB는 production이 아니다. Codex는 개발 요청 범위에서 local schema/data의 조회와 `INSERT`, `UPDATE`, `DELETE`, DDL, fixture, local migration을 직접 실행할 수 있다. Local write는 production 반영 권한으로 이어지지 않는다.
30
+
31
+ ## 구조
32
+
33
+ ```text
34
+ production DB (read-only login-path)
35
+ -> port/volume 없는 tmpfs sanitizer
36
+ -> project mask SQL
37
+ -> verification SQL 결과 0
38
+ -> sanitized .sql.gz + checksum manifest
39
+ -> 개발자별 sk-managed Docker local DB (127.0.0.1 only)
40
+ ```
41
+
42
+ Raw production dump는 파일로 저장하지 않는다. Sanitizer는 network, 외부 port, persistent volume 없이 실행하며 성공·실패 후 항상 삭제한다. Restore도 snapshot을 tmpfs sanitizer에서 마스킹·검증한 다음에만 개발자 volume으로 전송한다.
43
+
44
+ ### OWNER 전용 plain snapshot 예외
45
+
46
+ 사용자가 로그인 ID, password hash, session/token, 고객 개인정보와 자유입력 원문이 로컬 파일과 DB에 영구 저장되는 위험을 고지받고 명시 승인한 경우에만 `plainSnapshot.enabled=true` policy와 명시 flag로 plain snapshot을 선택할 수 있다. 이 모드는 공유용 sanitized snapshot을 대체하지 않는다.
47
+
48
+ - 생성·복원 모두 GitHub numeric-ID OWNER와 `--allow-raw-production-data`를 요구한다.
49
+ - manifest는 `sanitized:false`, `containsProductionData:true`, `shareable:false`, 생성 workspace hash를 기록한다.
50
+ - snapshot은 workspace 전용 `0700` 디렉터리의 `0600` 파일로만 생성하며 동일 workspace에서만 복원한다.
51
+ - Git, Notion, 자동 snapshot 탐색, 다른 개발자 전달을 금지한다.
52
+ - 논리 dump는 table schema와 row를 보존하지만 DB user/grant, trigger, routine, event는 포함하지 않는다.
53
+
54
+ ## 프로젝트 관리자가 최초 1회 준비할 것
55
+
56
+ Sanitized snapshot을 지원하는 각 DB 프로젝트는 아래 3개 파일을 versioned source로 관리한다. Plain opt-in을 함께 지원해도 이 기본 3파일은 유지한다.
57
+
58
+ 1. `db/local/local-db-policy.json`
59
+ 2. `db/local/mask-production-copy.sql`
60
+ 3. `db/local/verify-production-copy.sql`
61
+
62
+ 정책 예시(`slk-api`, 2026-08-29 production read-only 확인 기준):
63
+
64
+ ```json
65
+ {
66
+ "schemaVersion": 1,
67
+ "project": "slk-api",
68
+ "containerImage": "mariadb:10.11.11",
69
+ "localDatabase": "selink_local",
70
+ "localPort": 3308,
71
+ "appUser": "slk_local",
72
+ "jdbcScheme": "mariadb",
73
+ "clientBinary": "mariadb",
74
+ "dumpBinary": "mariadb-dump",
75
+ "snapshotMode": "sanitized",
76
+ "plainSnapshot": {
77
+ "enabled": true
78
+ },
79
+ "sanitizerTmpfsSize": "2g",
80
+ "source": {
81
+ "database": "selink",
82
+ "loginPath": "codex-db-slk-server",
83
+ "engineVersionPrefix": "10.11.11-MariaDB",
84
+ "clientBinary": "mysql",
85
+ "dumpBinary": "mysqldump"
86
+ },
87
+ "masking": {
88
+ "sql": "db/local/mask-production-copy.sql",
89
+ "verificationSql": "db/local/verify-production-copy.sql"
90
+ }
91
+ }
92
+ ```
93
+
94
+ OWNER 전용 plain 모드는 sanitized 기본 policy에 `plainSnapshot.enabled=true`를 추가한다. `--allow-raw-production-data`가 있는 생성·복원만 plain으로 선택하며, flag가 없으면 기존 sanitized 경로를 사용한다. Plain mode는 위험 고지 후 명시 승인된 현재 workspace에서만 사용한다.
95
+
96
+ 정책에는 host, port, username, password를 넣지 않는다. Production source 연결은 OWNER PC의 read-only MySQL login-path만 사용한다. `localDatabase`는 production 오인 방지를 위해 `_local`로 끝나야 한다.
97
+
98
+ ### 마스킹 SQL 계약
99
+
100
+ - 실제 프로젝트 schema를 기준으로 결정적 가명값을 만든다. 같은 고객을 여러 테이블에서 참조하면 동일한 가명 key를 사용해 join/unique/FK 동작을 보존한다.
101
+ - Snapshot restore 시 현재 mask SQL을 tmpfs에서 한 번 더 적용하므로, 가명화된 값에 다시 실행해도 결과가 바뀌지 않는 idempotent SQL로 작성한다.
102
+ - password/token/session/요청 IP, 이름·전화번호·주소, 상담 자유입력, 문의 본문, 메모, 첨부 파일명·storage key처럼 개인·민감정보가 될 수 있는 필드를 제거하거나 가명화한다.
103
+ - `CREATE USER`, `GRANT`, `INTO OUTFILE`, `LOAD DATA`, `DROP DATABASE`처럼 local data masking 범위를 벗어난 명령은 넣지 않는다.
104
+ - `slk-api`의 현재 metadata 기준 우선 검토 대상에는 `auth_user`, `office_customer`, `office_order_history`, `office_expo_contract_record`, `office_customer_inquiry*`, `office_session`, `frame_session`, `office_vendor_contact`, attachment/history 계열이 포함된다. 이 목록만으로 충분하다고 간주하지 말고 schema 변경 때 다시 감사한다.
105
+
106
+ ### 검증 SQL 계약
107
+
108
+ - read-only `SELECT`만 사용한다.
109
+ - 민감 원문 잔존 건수와 정책 위반 건수를 합산해 **정수 `0` 하나만** 출력한다.
110
+ - 결과가 비어 있거나 여러 값이 나오거나 `0`이 아니면 snapshot 생성은 실패한다.
111
+
112
+ ## OWNER: sanitized snapshot 만들기(Codex 내부 reference)
113
+
114
+ 사전 조건:
115
+
116
+ - 프로젝트 main이 최신 상태다.
117
+ - Docker Desktop이 실행 중이다.
118
+ - `mysql`/`mysqldump`와 production read-only login-path가 준비돼 있다.
119
+ - Docker context가 local `unix://` 또는 Windows `npipe://`다. SSH/TCP remote context는 차단된다.
120
+ - 전체 dump는 production I/O가 있으므로 저부하 시간대에 실행한다. `--single-transaction` 일관성은 transactional table에 한정된다.
121
+
122
+ ```bash
123
+ sk db local copy-production \
124
+ --output /private/team-share/slk-api-local-20260829.sql.gz
125
+ ```
126
+
127
+ 생성 결과는 `.sql.gz`와 같은 이름의 `.manifest.json` 두 파일이다. 둘을 함께 승인된 비공개 전달 경로로 공유한다. Git, PR, 공개 링크에는 올리지 않는다. 친구에게 production DB login-path나 credential은 전달하지 않는다.
128
+
129
+ ## 신규 개발자 온보딩 순서(Codex 내부 reference)
130
+
131
+ ### 1. 공용 도구 설치
132
+
133
+ Mac은 Docker Desktop과 Node.js 20 이상, Windows는 Docker Desktop과 Git Bash 및 Node.js 20 이상을 준비한다. Windows 명령은 PowerShell이 아니라 Git Bash를 공통 기준으로 한다.
134
+
135
+ ```bash
136
+ npm install -g seokang-sk
137
+ sk setup
138
+ sk check
139
+ ```
140
+
141
+ ### 2. 제품 저장소를 최신 main으로 준비
142
+
143
+ ```bash
144
+ cd /path/to/slk-api
145
+ sk init
146
+ ```
147
+
148
+ 친구는 `$sk-git` 흐름으로 `upstream/main`을 `--ff-only` 동기화한다. Local DB policy나 최신 migration이 없으면 DB 작업을 시작하지 말고 먼저 main 동기화를 해결한다.
149
+
150
+ ### 3. 개발자별 local DB 생성
151
+
152
+ ```bash
153
+ sk db local setup
154
+ sk db local status
155
+ ```
156
+
157
+ 실행 상태와 앱 연결값 파일 위치가 출력된다. 실제 password는 채팅이나 Git에 복사하지 않고 출력된 user-local `app.env`에서만 읽는다.
158
+
159
+ ### 4. OWNER가 전달한 snapshot 복원
160
+
161
+ ```bash
162
+ sk db local restore \
163
+ --snapshot /private/team-share/slk-api-local-20260829.sql.gz \
164
+ --replace
165
+ ```
166
+
167
+ `--replace`는 현재 프로젝트의 label·workspace·policy hash·image·loopback binding이 모두 일치하는 managed local DB만 교체한다. Snapshot checksum, `sanitized:true`, 현재 policy hash가 하나라도 다르면 중단한다.
168
+
169
+ ### 5. 애플리케이션 local profile 연결
170
+
171
+ `sk db local setup/status`가 알려준 `app.env`의 `DB_URL`, `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USERNAME`, `DB_PASSWORD`를 IDE의 local run configuration에 연결한다. 기존 managed DB를 재사용하는 `setup`도 이 계약으로 `app.env`를 다시 쓴다. Versioned `application*.properties`에 실제 credential을 쓰거나 production URL fallback을 추가하지 않는다.
172
+
173
+ ### 6. 새 migration을 local에 적용
174
+
175
+ Production에는 적용하지 않은 다음 migration을 개발 중이면 프로젝트 내부 SQL 파일만 명시적으로 local DB에 적용한다.
176
+
177
+ ```bash
178
+ sk db local apply --file db/schema/V1_81__example.sql
179
+ ```
180
+
181
+ 이 명령은 `sk` label, workspace hash, policy hash, image, `_local` DB명, loopback port를 검증한 container에만 실행된다. CI, application startup, deploy에서는 호출하지 않는다. Production 반영은 `$sk-db`가 만든 migration/verification/rollback 세트를 OWNER가 별도 DB 도구에서 수동 실행한다.
182
+
183
+ ### 7. 앱 실행과 확인
184
+
185
+ ```bash
186
+ sk db local status
187
+ ```
188
+
189
+ - 앱이 `127.0.0.1:<policy port>/<database>_local`에 연결되는지 확인한다.
190
+ - 최신 migration이 필요한 기능은 local apply 후 재검증한다.
191
+ - 작업 종료 시 DB를 보존하려면 그대로 두고, container만 멈추려면 `sk db local stop`을 사용한다.
192
+
193
+ ## 다음 프로젝트 추가 순서
194
+
195
+ 새 DB 프로젝트를 추가할 때는 CLI 코드를 복사하지 않는다.
196
+
197
+ 1. Production engine/version과 source schema를 read-only로 확인한다.
198
+ 2. 프로젝트에 `db/local/local-db-policy.json`을 추가한다.
199
+ 3. Schema 전체를 감사해 project-specific mask SQL과 verification SQL을 작성한다.
200
+ 4. OWNER가 `copy-production`을 1회 실행하고 manifest/checksum을 확인한다.
201
+ 5. 별도 개발자 PC에서 `setup → restore → app local profile → status`를 끝까지 검증한다.
202
+ 6. Schema/PII 컬럼이 바뀌면 mask/verification policy를 같은 PR에서 갱신한다. Policy hash가 바뀌므로 이전 snapshot은 자동 거부되고 각 개발자는 `reset --replace` 후 새 snapshot을 restore해야 한다.
203
+
204
+ ## 실패/복구
205
+
206
+ - `policy hash mismatch`: 최신 main을 pull하고 OWNER가 새 snapshot을 만든 뒤, 개발자는 `sk db local reset --replace`로 기존 managed local volume을 명시적으로 초기화하고 새 snapshot을 restore한다.
207
+ - `SELECT-only credential 증명 실패`: production copy를 중단하고 read-only login-path 권한을 수정한다.
208
+ - `verification != 0`: 마스킹 SQL/검증 SQL을 고치기 전에는 snapshot을 공유하지 않는다.
209
+ - `remote Docker context`: local Docker Desktop context로 전환한다.
210
+ - local migration 실패: production에는 영향이 없다. 검증된 snapshot을 `restore --replace`로 다시 복원한다.
@@ -274,7 +274,19 @@ schema/migration/DML/repository/ORM/query/transaction/persistence 코드와 SQL
274
274
  sk guard implementation --db-impact
275
275
  ```
276
276
 
277
- 별도 테스트/개발/로컬 DB는 없으며 모든 DB target은 production으로 간주한다. Codex, local test, CI, deploy, start/restart script는 migration/DDL/DML/backfill/seed를 실행하지 않는다. 검증은 compile/typecheck, mock 단위 테스트, SQL 정적 검사로 제한하고 실제 반영은 OWNER 사용자가 IDE/DB 도구에서 수동 실행한다.
277
+ `sk db local` managed container 증거가 없는 DB target은 production 또는 미검증 target으로 간주한다. CI, deploy, start/restart script는 migration/DDL/DML/backfill/seed를 실행하지 않는다. Production 반영은 OWNER 사용자가 IDE/DB 도구에서 수동 실행한다.
278
+
279
+ 개발자별 DB는 Mac/Windows Git Bash 공통 Docker 흐름으로 준비한다.
280
+
281
+ ```bash
282
+ sk db local setup
283
+ sk db local restore --snapshot /private/path/project-local.sql.gz --replace
284
+ sk db local status
285
+ sk db local apply --file db/schema/V1_81__example.sql
286
+ sk db local reset --replace
287
+ ```
288
+
289
+ `reset --replace`는 policy/mask/verification 변경 후 기존 managed local volume을 명시적으로 삭제하고 새 계약으로 재생성한다. OWNER의 production data copy, project policy, masking/verification, 신규 개발자 전체 순서는 [Local DB Onboarding](local-db-onboarding.md)을 따른다. 친구에게 production DB credential을 전달하지 않고 sanitized snapshot과 checksum manifest만 비공개 경로로 공유한다.
278
290
 
279
291
  local test/build 전과 CI의 첫 검증 단계에는 아래 명령을 둔다. 이 검사는 DB 연결 test, Testcontainers, migration CLI, startup schema sync, CI DB credential을 탐지하면 종료 코드 `2`로 중단한다.
280
292
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "seokang-sk",
3
- "version": "0.7.2",
3
+ "version": "0.9.0",
4
4
  "description": "SeoKang Codex 공용 설정과 스킬을 설치하는 CLI",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/selink-lab/seokang-codex-setting",
@@ -22,7 +22,7 @@
22
22
  "sk": "scripts/sk.mjs"
23
23
  },
24
24
  "scripts": {
25
- "check:harness": "npm run check:harness:defaults -- . && npm run check:harness:ddl-comments -- . && npm run check:harness:docs -- . && npm run check:harness:style && npm run check:harness:models && npm run check:harness:ui-router && npm run check:harness:automation && npm run check:harness:db-execution && npm run check:harness:operator-guard && npm run check:harness:deploy-guard && npm run check:harness:fork-workflow && npm run check:harness:work-start && npm run check:harness:git-stage && npm run check:harness:git-secrets && npm run check:harness:npm-release && npm run check:harness:cli-smoke",
25
+ "check:harness": "npm run check:harness:defaults -- . && npm run check:harness:ddl-comments -- . && npm run check:harness:docs -- . && npm run check:harness:style && npm run check:harness:models && npm run check:harness:ui-router && npm run check:harness:automation && npm run check:harness:db-execution && npm run check:harness:local-db && npm run check:harness:local-db-skill && npm run check:harness:operator-guard && npm run check:harness:deploy-guard && npm run check:harness:fork-workflow && npm run check:harness:work-start && npm run check:harness:git-stage && npm run check:harness:git-secrets && npm run check:harness:npm-release && npm run check:harness:cli-smoke",
26
26
  "check:harness:defaults": "node scripts/check_no_unauthorized_defaults.mjs",
27
27
  "check:harness:ddl-comments": "node scripts/check_ddl_comments.mjs",
28
28
  "check:harness:docs": "node scripts/check_docs_integrity.mjs",
@@ -31,6 +31,8 @@
31
31
  "check:harness:ui-router": "node scripts/check_ui_runtime_router.mjs",
32
32
  "check:harness:automation": "node scripts/check_automation_safety.mjs",
33
33
  "check:harness:db-execution": "node scripts/check_db_execution_boundary_smoke.mjs",
34
+ "check:harness:local-db": "node scripts/check_local_db_workflow.mjs",
35
+ "check:harness:local-db-skill": "node scripts/check_local_db_skill.mjs",
34
36
  "check:harness:operator-guard": "node scripts/check_operator_guard.mjs",
35
37
  "check:harness:deploy-guard": "node scripts/check_deploy_actor_guard.mjs",
36
38
  "check:harness:fork-workflow": "node scripts/check_fork_contribution.mjs",
@@ -94,6 +94,8 @@ const STATIC_CHECKS = [
94
94
  'MAIN_SHA_CHANGED',
95
95
  'POST_MERGE_STATE_UNVERIFIED',
96
96
  'POST_MERGE_MAIN_UNCHANGED',
97
+ 'inspectPullRequestStable',
98
+ 'SK_PR_MERGE_RETRY_DELAYS_MS',
97
99
  'mergeStateStatus',
98
100
  'requiresUpToDateBranch',
99
101
  "ghJson(['api', 'user'",
@@ -110,7 +112,10 @@ const STATIC_CHECKS = [
110
112
  'STRICT_BASE_NOT_ENFORCED',
111
113
  'TRUSTED_CHECK_NOT_REQUIRED',
112
114
  'ADMIN_BYPASS_NOT_BLOCKED',
113
- 'MERGE_STATE_NOT_CLEAN',
115
+ 'classifyMergeReadiness',
116
+ 'MERGEABILITY_CALCULATION_PENDING',
117
+ 'PR_CONFLICT_RESOLUTION_REQUIRED',
118
+ 'PR_BASE_UPDATE_REQUIRED',
114
119
  'PR_NOT_OPEN',
115
120
  'HEAD_REPOSITORY_NOT_ALLOWED',
116
121
  'MAIN_SHA_MISSING',
@@ -9,6 +9,8 @@ const EXEMPT_HARNESS_FILES = new Set([
9
9
  path.join(SCRIPT_DIR, 'check_db_execution_boundary.mjs'),
10
10
  path.join(SCRIPT_DIR, 'check_db_execution_boundary_smoke.mjs'),
11
11
  path.join(SCRIPT_DIR, 'check_deploy_actor_guard.mjs'),
12
+ // Exact canonical runtime exception: this file can mutate only a label-verified local Docker DB.
13
+ path.join(SCRIPT_DIR, 'local_db.mjs'),
12
14
  ]);
13
15
 
14
16
  const SKIP_DIRS = new Set([
@@ -40,8 +40,8 @@ const REQUIRED_POLICY_FILES = [
40
40
  fragments: ['Runtime Delegation', '명시적 위임 지시', '사용자의 별도 요청을 기다리지 않는다', '반드시 실제 위임한다', '둘 이상의 레이어', '병렬 write 충돌', '작업 이력의 provenance'],
41
41
  },
42
42
  {
43
- label: 'production-only DB execution policy',
44
- fragments: ['DB Authoring / Production Execution Boundary', 'sk guard implementation --db-impact', '연결 가능한 모든 DB는 production', 'Codex, 로컬 테스트, CI, deploy, start/restart script', 'scripts/check_db_execution_boundary.mjs', 'Testcontainers', 'OWNER 사용자가'],
43
+ label: 'production and managed-local DB execution policy',
44
+ fragments: ['DB Authoring / Production Execution Boundary', 'sk guard implementation --db-impact', '`sk db local`이 생성', 'tmpfs sanitizer', 'sanitized snapshot', 'Codex, 로컬 테스트, CI, deploy, start/restart script', 'scripts/check_db_execution_boundary.mjs', 'Testcontainers', 'OWNER 사용자가'],
45
45
  },
46
46
  {
47
47
  label: 'git deploy capability policy',
@@ -75,7 +75,7 @@ const REQUIRED_POLICY_FILES = [
75
75
  },
76
76
  {
77
77
  label: 'feature DB execution boundary',
78
- fragments: ['DB Authoring / Execution Boundary', 'GitHub actor 상태와 무관', '모든 DB target은 production', 'compile/typecheck', 'OWNER 사용자가'],
78
+ fragments: ['DB Authoring / Execution Boundary', 'GitHub actor 상태와 무관', '`sk db local` managed container', 'compile/typecheck', 'OWNER 사용자가'],
79
79
  },
80
80
  {
81
81
  label: 'feature npm publish decision check',
@@ -92,7 +92,7 @@ const REQUIRED_POLICY_FILES = [
92
92
  checks: [
93
93
  {
94
94
  label: 'bugfix DB execution boundary',
95
- fragments: ['DB Authoring / Execution Boundary', 'GitHub actor 상태와 무관', '모든 DB target은 production', 'mock 단위 테스트', '수동 실행'],
95
+ fragments: ['DB Authoring / Execution Boundary', 'GitHub actor 상태와 무관', '`sk db local` managed container', 'sanitized snapshot', '수동 실행'],
96
96
  },
97
97
  ],
98
98
  },
@@ -103,6 +103,10 @@ const REQUIRED_POLICY_FILES = [
103
103
  label: 'db migration execution boundary',
104
104
  fragments: ['Mode: Migration', 'GitHub identity/cache/OWNER 판정 없이', 'local test, CI, deploy/startup script', 'SQL 정적 lint/comment', '수동 실행용'],
105
105
  },
106
+ {
107
+ label: 'db local snapshot boundary',
108
+ fragments: ['Mode: Local DB', 'tmpfs sanitizer', '정수 `0`', 'sk db local apply', 'production credential'],
109
+ },
106
110
  ],
107
111
  },
108
112
  {
@@ -114,6 +118,15 @@ const REQUIRED_POLICY_FILES = [
114
118
  },
115
119
  ],
116
120
  },
121
+ {
122
+ relativePath: path.join('docs', 'setup', 'local-db-onboarding.md'),
123
+ checks: [
124
+ {
125
+ label: 'local DB onboarding sequence',
126
+ fragments: ['Mac + Windows Git Bash', 'tmpfs sanitizer', 'local-db-policy.json', 'sk db local setup', 'sk db local restore', 'sk db local apply', 'production credential', '다음 프로젝트 추가 순서'],
127
+ },
128
+ ],
129
+ },
117
130
  {
118
131
  relativePath: path.join('skills', 'sk-git', 'SKILL.md'),
119
132
  checks: [
@@ -139,7 +152,7 @@ const REQUIRED_POLICY_FILES = [
139
152
  },
140
153
  {
141
154
  label: 'git owner merge and contributor sync',
142
- fragments: ['Executable Git Workflow Preflight Receipt', 'sk git preflight', 'receipt', 'NON_OWNER Upstream Sync', 'git_fork_sync.mjs', 'OWNER Current-Repository PR Merge', 'git_pr_merge.mjs', 'plain `$sk-git`', '--match-head-commit'],
155
+ fragments: ['Executable Git Workflow Preflight Receipt', 'sk git preflight', 'receipt', 'NON_OWNER Upstream Sync', 'git_fork_sync.mjs', 'OWNER Current-Repository PR Merge', 'git_pr_merge.mjs', 'plain `$sk-git`', '--match-head-commit', 'MERGEABILITY_CALCULATION_PENDING', 'resolution-required', 'local-sync-pending'],
143
156
  },
144
157
  {
145
158
  label: 'git secret hard-fail',
@@ -152,7 +165,7 @@ const REQUIRED_POLICY_FILES = [
152
165
  checks: [
153
166
  {
154
167
  label: 'deploy DB execution boundary',
155
- fragments: ['DB Execution Boundary', '테스트/개발/로컬 DB가 따로 없고', 'scripts/check_db_execution_boundary.mjs', 'start/restart script', '별도 IDE/DB 도구에서 수동 실행'],
168
+ fragments: ['DB Execution Boundary', '`sk db local` managed container', 'scripts/check_db_execution_boundary.mjs', 'start/restart script', '별도 IDE/DB 도구에서 수동 실행'],
156
169
  },
157
170
  {
158
171
  label: 'deploy npm publish decision gate',
@@ -121,7 +121,9 @@ requireFragments('friend guide', guide, [
121
121
  '$sk-git',
122
122
  'gh pr create',
123
123
  'DB 관련 코드',
124
- '모든 DB target은 production',
124
+ 'sanitized snapshot',
125
+ '`sk db local` managed container',
126
+ 'production credential은 받지 않는다',
125
127
  '사용자 유형을 판정하지 않는다',
126
128
  '접근 없음',
127
129
  'Read 전용',
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import assert from 'node:assert/strict';
4
- import { classifyRisk, detectStateChanges, evaluateChecks, evaluatePullRequest } from './git_pr_merge_core.mjs';
4
+ import { classifyMergeReadiness, classifyRisk, detectStateChanges, evaluateChecks, evaluatePullRequest } from './git_pr_merge_core.mjs';
5
5
 
6
6
  const policy = { allowedAuthors: ['91kwangmin'], requiredCheckNames: ['pr-safe'], trustedRepository: 'selink-lab/slk-api' };
7
7
  const repositoryState = {
@@ -92,7 +92,26 @@ assert.equal(evaluatePullRequest(pr({ state: 'MERGED', mergedAt: '2026-08-29T00:
92
92
  assert.equal(evaluatePullRequest(pr({ headRepositoryOwner: { login: 'outsider' } }), policy, repositoryState).eligible, false);
93
93
  assert.equal(evaluatePullRequest(pr({ isCrossRepository: false }), policy, repositoryState).eligible, false);
94
94
  assert.equal(evaluatePullRequest(pr({ statusCheckRollup: [] }), policy, repositoryState).eligible, false);
95
- assert.equal(evaluatePullRequest(pr({ mergeable: 'CONFLICTING' }), policy, repositoryState).eligible, false);
95
+ assert.deepEqual(classifyMergeReadiness(pr()), {
96
+ status: 'ready', reason: null, mergeable: 'MERGEABLE', mergeStateStatus: 'CLEAN',
97
+ });
98
+ assert.equal(classifyMergeReadiness(pr({ mergeable: 'UNKNOWN', mergeStateStatus: 'UNKNOWN' })).status, 'pending');
99
+ assert.equal(classifyMergeReadiness(pr({ mergeable: 'CONFLICTING', mergeStateStatus: 'DIRTY' })).status, 'resolution-required');
100
+ assert.equal(classifyMergeReadiness(pr({ mergeable: 'MERGEABLE', mergeStateStatus: 'BEHIND' })).status, 'update-required');
101
+ assert.equal(classifyMergeReadiness(pr({ mergeable: 'UNKNOWN', mergeStateStatus: 'BLOCKED' })).reason, 'MERGE_POLICY_BLOCKED');
102
+ assert.equal(classifyMergeReadiness(pr({ mergeable: 'UNKNOWN', mergeStateStatus: 'UNSTABLE' })).reason, 'MERGE_CHECKS_UNSTABLE');
103
+ assert.equal(classifyMergeReadiness(pr({ mergeable: 'CONFLICTING', mergeStateStatus: 'UNKNOWN' })).reason, 'PR_CONFLICT_RESOLUTION_REQUIRED');
104
+ assert.equal(classifyMergeReadiness(pr({ mergeable: 'UNKNOWN', mergeStateStatus: 'DIRTY' })).reason, 'PR_CONFLICT_RESOLUTION_REQUIRED');
105
+ assert.equal(classifyMergeReadiness(pr({ mergeable: 'UNKNOWN', mergeStateStatus: 'BEHIND' })).reason, 'PR_BASE_UPDATE_REQUIRED');
106
+ assert.equal(classifyMergeReadiness(pr({ mergeable: 'MERGEABLE', mergeStateStatus: 'HAS_HOOKS' })).status, 'ready');
107
+ assert.equal(classifyMergeReadiness(pr({ mergeStateStatus: 'BLOCKED' })).reason, 'MERGE_POLICY_BLOCKED');
108
+ assert.equal(classifyMergeReadiness(pr({ mergeStateStatus: 'UNSTABLE' })).reason, 'MERGE_CHECKS_UNSTABLE');
109
+ const pendingMergeability = evaluatePullRequest(pr({ mergeable: 'UNKNOWN', mergeStateStatus: 'UNKNOWN' }), policy, repositoryState);
110
+ assert.ok(pendingMergeability.reasons.includes('MERGEABILITY_CALCULATION_PENDING'));
111
+ assert.ok(!pendingMergeability.reasons.includes('NOT_MERGEABLE'));
112
+ const conflicting = evaluatePullRequest(pr({ mergeable: 'CONFLICTING', mergeStateStatus: 'DIRTY' }), policy, repositoryState);
113
+ assert.equal(conflicting.eligible, false);
114
+ assert.ok(conflicting.reasons.includes('PR_CONFLICT_RESOLUTION_REQUIRED'));
96
115
  assert.equal(evaluatePullRequest(pr({ isDraft: true }), policy, repositoryState).eligible, false);
97
116
  assert.equal(evaluatePullRequest(pr({ reviewDecision: 'CHANGES_REQUESTED' }), policy, repositoryState).eligible, false);
98
117
  assert.equal(evaluatePullRequest(pr({ mergeStateStatus: 'BEHIND' }), policy, repositoryState).eligible, false);