@nestarc/data-subject 0.1.0 → 0.2.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.
@@ -0,0 +1,679 @@
1
+ # @nestarc/data-subject - v0.2 Technical Spec
2
+
3
+ 상태: Draft
4
+ 기준 문서: [0.2.0 기능 제안서](./data-subject-0.2.0-feature-proposal.md)
5
+ 대상 버전: `@nestarc/data-subject@0.2.0`
6
+ 작성일: 2026-06-19
7
+
8
+ 본 문서는 `@nestarc/data-subject` 0.2.0에서 구현할 기술 사양을 정의한다. 0.2.0의 목표는 기능 범위를 넓히는 것이 아니라, 0.1.0의 headless export/erase core를 운영 환경에 올릴 수 있는 최소 신뢰층으로 만드는 것이다.
9
+
10
+ 주의: 본 문서는 제품/기술 스펙이며 법률 자문이 아니다. 관할권별 SLA, 신원 확인 수준, 보존 근거, 익명화 충분성, request/evidence retention policy는 모두 법무 검토 필요 영역이다.
11
+
12
+ ## 1. 목표
13
+
14
+ ### 1.1 릴리스 목표
15
+
16
+ - 운영 환경에서 재시작 후에도 request lifecycle과 evidence를 조회할 수 있다.
17
+ - erase 처리 전후의 영향 범위와 보존 근거를 hashable artifact로 남긴다.
18
+ - Prisma schema와 registry/policy 사이의 누락 위험을 CI에서 발견한다.
19
+ - request state transition, audit event, outbox event의 의미를 명확히 한다.
20
+ - README, PRD, spec 간 stale claim을 줄이고 현재 구현 및 0.2.0 범위를 정렬한다.
21
+
22
+ ### 1.2 비목표
23
+
24
+ - Admin portal 또는 end-user portal을 제공하지 않는다.
25
+ - Stripe, Intercom, Segment 등 외부 SaaS 커넥터를 직접 제공하지 않는다.
26
+ - backup, snapshot, search index, ML unlearning 자동화를 제공하지 않는다.
27
+ - GDPR/CCPA/PIPA/APPI 법률 해석 또는 legal basis catalog를 강제하지 않는다.
28
+ - full identity graph 또는 probabilistic matching을 제공하지 않는다.
29
+
30
+ ## 2. 호환성 원칙
31
+
32
+ 0.2.0은 가능한 한 0.1.0 public API를 유지한다.
33
+
34
+ - 기존 `DataSubjectService.export(subjectId, tenantId)`와 `erase(subjectId, tenantId)`는 유지한다.
35
+ - 기존 `RequestStorage`와 `ArtifactStorage` 인터페이스는 breaking change 없이 유지한다.
36
+ - `RequestStats`는 additive field만 허용한다.
37
+ - `DataSubjectRequest`의 기존 필드는 제거하거나 의미를 바꾸지 않는다.
38
+ - 새 기능은 adapter, option, helper, CLI, 추가 타입으로 제공한다.
39
+
40
+ Breaking change가 필요한 경우 문서와 changelog에 명시하고, migration guide를 제공해야 한다.
41
+
42
+ ## 3. 범위
43
+
44
+ ### 3.1 P0 - 0.2.0 필수
45
+
46
+ | 기능 | 산출물 | 성공 기준 |
47
+ |---|---|---|
48
+ | Production request persistence | `PrismaRequestStorage` | storage contract 통과, 재시작 후 request 조회 가능 |
49
+ | Durable artifact path | artifact key convention, recipe, hash 검증 | export와 erase evidence 모두 `artifactHash`와 `artifactUrl` 보유 |
50
+ | Erasure evidence artifact | JSON report schema, pre/post scan stats | erase 완료 request 100%가 evidence artifact를 저장 |
51
+ | Schema/policy lint | `data-subject lint` CLI | 누락 PII 후보, tenantField 누락, policy 누락 시 non-zero exit 가능 |
52
+ | Registry validation | runtime validation helper | 중복, 빈 policy, retain legalBasis, tenantField warning 검출 |
53
+ | Audit event stream | transition별 audit event | created/processing/completed/failed 전환이 audit/outbox로 추적 가능 |
54
+ | Docs cleanup | README/spec/PRD 정합성 | 구현되지 않은 v0.1 claim 제거 또는 roadmap 표시 |
55
+
56
+ ### 3.2 P1 - 포함 권장
57
+
58
+ | 기능 | 산출물 | 성공 기준 |
59
+ |---|---|---|
60
+ | Async lifecycle skeleton | `createRequest`, `processRequest` 또는 internal transition guard | long-running job 통합을 막지 않는 API shape |
61
+ | Typed outbox events | exported event type union | 이벤트 payload에 subjectId 외 PII가 들어가지 않음 |
62
+ | Minimal identity alias resolver | optional `SubjectIdentityResolver` interface | raw email/phone 저장 없이 alias 확장 가능 |
63
+ | Overdue event helper | overdue query 또는 event publisher helper | SLA monitoring 구현 가능 |
64
+
65
+ ### 3.3 P2 - 후속 가능
66
+
67
+ | 기능 | 산출물 | 성공 기준 |
68
+ |---|---|---|
69
+ | Decorator registration | `@DataSubjectEntity`, `@DataSubjectIgnore` | programmatic registry와 동일한 compile path 사용 |
70
+ | Connector receipt abstraction | receipt attach API 또는 stats schema | 외부 processor 처리 결과를 request에 연결 |
71
+ | CSV/NDJSON export | optional export format | ZIP manifest와 format별 hash snapshot |
72
+
73
+ ## 4. 현재 구현 기준
74
+
75
+ 0.1.0 기준으로 이미 존재하는 표면:
76
+
77
+ - `DataSubjectService`
78
+ - `DataSubjectModule.forRoot`
79
+ - `Registry`
80
+ - `compilePolicy`
81
+ - `validateLegalBasis`
82
+ - `fromPrisma`
83
+ - `RequestStorage`
84
+ - `ArtifactStorage`
85
+ - `InMemoryRequestStorage`
86
+ - `InMemoryArtifactStorage`
87
+ - `RequestType = export | erase`
88
+ - `RequestState = created | validating | processing | completed | failed`
89
+ - `Strategy = delete | anonymize | retain`
90
+
91
+ 0.2.0은 위 표면을 확장하되, 기존 sync 실행 경로를 계속 지원한다.
92
+
93
+ ## 5. Data Model
94
+
95
+ ### 5.1 Request table
96
+
97
+ 0.2.0의 기본 `DataSubjectRequest` Prisma model은 0.1.0 예시와 호환되어야 한다.
98
+
99
+ ```prisma
100
+ model DataSubjectRequest {
101
+ id String @id @default(cuid())
102
+ tenantId String
103
+ subjectId String
104
+ type String
105
+ state String
106
+ createdAt DateTime @default(now())
107
+ dueAt DateTime
108
+ completedAt DateTime?
109
+ failedAt DateTime?
110
+ failureReason String?
111
+ artifactHash String?
112
+ artifactUrl String?
113
+ stats Json?
114
+ requestedBy String?
115
+
116
+ @@index([tenantId, subjectId])
117
+ @@index([state, dueAt])
118
+ }
119
+ ```
120
+
121
+ 추가 컬럼은 0.2.0 필수 요건이 아니다. 새 정보는 우선 `stats` JSON과 artifact에 저장한다. DB migration을 강제하지 않기 위함이다.
122
+
123
+ ### 5.2 Request stats
124
+
125
+ 0.2.0은 기존 `RequestStats`에 다음 optional field를 추가할 수 있다.
126
+
127
+ ```ts
128
+ export interface RequestStats {
129
+ entities: Array<{
130
+ entityName: string;
131
+ affected: number;
132
+ strategy: 'delete' | 'anonymize' | 'retain' | 'mixed' | 'export';
133
+ }>;
134
+ retained?: Array<{
135
+ entityName: string;
136
+ field: string;
137
+ legalBasis: string;
138
+ count: number;
139
+ }>;
140
+ verificationResidual?: Array<{
141
+ entityName: string;
142
+ count: number;
143
+ }>;
144
+ preScan?: Array<{
145
+ entityName: string;
146
+ count: number;
147
+ }>;
148
+ postScan?: Array<{
149
+ entityName: string;
150
+ count: number;
151
+ }>;
152
+ evidence?: {
153
+ schemaVersion: 'data-subject.evidence.v1';
154
+ artifactHash: string;
155
+ artifactUrl: string;
156
+ };
157
+ }
158
+ ```
159
+
160
+ `stats`에는 raw row, field value, email, phone, address 등 원문 PII를 저장하면 안 된다.
161
+
162
+ ## 6. Production Persistence
163
+
164
+ ### 6.1 `PrismaRequestStorage`
165
+
166
+ 0.2.0은 `RequestStorage` 구현체로 `PrismaRequestStorage`를 제공한다.
167
+
168
+ ```ts
169
+ export interface PrismaDataSubjectRequestDelegate {
170
+ create(args: { data: unknown }): Promise<unknown>;
171
+ update(args: { where: { id: string }; data: unknown }): Promise<unknown>;
172
+ findUnique(args: { where: { id: string } }): Promise<unknown | null>;
173
+ findMany(args: unknown): Promise<unknown[]>;
174
+ }
175
+
176
+ export interface PrismaRequestStorageOptions {
177
+ delegate: PrismaDataSubjectRequestDelegate;
178
+ now?: () => Date;
179
+ }
180
+
181
+ export class PrismaRequestStorage implements RequestStorage {
182
+ constructor(options: PrismaRequestStorageOptions);
183
+ }
184
+ ```
185
+
186
+ 필수 동작:
187
+
188
+ - `insert`는 request id 충돌 시 `dsr_request_conflict`를 던진다.
189
+ - `update`는 없는 id에 대해 `dsr_request_not_found`를 던진다.
190
+ - `findById`는 없으면 `null`을 반환한다.
191
+ - `listByTenant`는 `tenantId`를 항상 where 조건에 포함한다.
192
+ - `listOverdue(now)`는 `state`가 `completed` 또는 `failed`가 아니고 `dueAt < now`인 request를 반환한다.
193
+ - Date와 JSON stats 직렬화/역직렬화가 `DataSubjectRequest` 타입과 호환되어야 한다.
194
+
195
+ ### 6.2 Artifact storage
196
+
197
+ 기존 `ArtifactStorage` 인터페이스는 유지한다.
198
+
199
+ ```ts
200
+ export interface ArtifactStorage {
201
+ put(key: string, body: Buffer, contentType: string): Promise<string>;
202
+ get(key: string): Promise<{ body: Buffer; contentType: string } | null>;
203
+ }
204
+ ```
205
+
206
+ 0.2.0은 cloud SDK를 필수 dependency로 추가하지 않는다. 대신 다음을 제공한다.
207
+
208
+ - stable artifact key convention
209
+ - hash verification helper
210
+ - S3/R2/GCS style adapter recipe
211
+ - local file adapter는 개발용으로만 선택 제공 가능
212
+
213
+ Artifact key convention:
214
+
215
+ ```txt
216
+ data-subject/{tenantId}/{requestId}/export.zip
217
+ data-subject/{tenantId}/{requestId}/erase-evidence.json
218
+ ```
219
+
220
+ `artifactUrl`은 consumer-managed opaque reference다. public URL이어야 할 필요가 없으며, 가능하면 signed URL이 아니라 storage key 또는 internal URI를 저장한다.
221
+
222
+ ## 7. Erasure Evidence Artifact
223
+
224
+ ### 7.1 Evidence schema
225
+
226
+ Erase 완료 시 `ArtifactStorage.put`으로 다음 JSON artifact를 저장한다.
227
+
228
+ ```ts
229
+ export interface ErasureEvidenceArtifact {
230
+ schemaVersion: 'data-subject.erasure-evidence.v1';
231
+ requestId: string;
232
+ tenantId: string;
233
+ requestType: 'erase';
234
+ generatedAt: string;
235
+ state: 'completed';
236
+ preScan: Array<{
237
+ entityName: string;
238
+ count: number;
239
+ }>;
240
+ actions: Array<{
241
+ entityName: string;
242
+ strategy: 'delete' | 'anonymize' | 'retain' | 'mixed';
243
+ affected: number;
244
+ rowLevel: 'delete-row' | 'delete-fields';
245
+ deleteFields?: string[];
246
+ anonymizedFields?: string[];
247
+ retainedFields?: Array<{
248
+ field: string;
249
+ legalBasis: string;
250
+ until?: string;
251
+ count: number;
252
+ }>;
253
+ }>;
254
+ postScan: Array<{
255
+ entityName: string;
256
+ count: number;
257
+ }>;
258
+ verificationResidual: Array<{
259
+ entityName: string;
260
+ count: number;
261
+ }>;
262
+ artifactHashAlgorithm: 'sha256';
263
+ }
264
+ ```
265
+
266
+ 금지 사항:
267
+
268
+ - raw row를 포함하지 않는다.
269
+ - field value를 포함하지 않는다.
270
+ - email, phone, address, IP address, name 등 원문 PII를 포함하지 않는다.
271
+ - failure stack trace를 포함하지 않는다.
272
+
273
+ `subjectId`는 기본 artifact에 포함하지 않는다. request record와 `requestId`로 연결한다. consumer가 별도 artifact enrichment를 구현하는 경우 subject identifier 보존 근거와 접근제어는 법무 검토 필요다.
274
+
275
+ ### 7.2 Hashing
276
+
277
+ - hash 대상은 artifact body bytes다.
278
+ - JSON은 deterministic serialization을 사용해야 한다.
279
+ - `artifactHash = sha256(bodyBytes).hex()`.
280
+ - `DataSubjectRequest.artifactHash`는 evidence artifact hash와 같아야 한다.
281
+ - `DataSubjectRequest.artifactUrl`은 `ArtifactStorage.put`이 반환한 값을 저장한다.
282
+
283
+ ### 7.3 Execution order
284
+
285
+ Erase sync path는 다음 순서를 따른다.
286
+
287
+ 1. request 생성, state `created`
288
+ 2. outbox `data_subject.request_created`
289
+ 3. audit `data_subject.request_created`
290
+ 4. state `processing`
291
+ 5. audit `data_subject.request_processing`
292
+ 6. pre-scan
293
+ 7. outbox `data_subject.erasure_requested`
294
+ 8. entity별 erase/anonymize/retain 실행
295
+ 9. post-scan 및 residual verification
296
+ 10. evidence artifact 생성 및 upload
297
+ 11. request state `completed`, `artifactHash`, `artifactUrl`, `stats` 업데이트
298
+ 12. audit `data_subject.request_completed`
299
+ 13. outbox `data_subject.request_completed`
300
+
301
+ 실패 시:
302
+
303
+ 1. request state `failed`, `failedAt`, sanitized `failureReason` 업데이트
304
+ 2. audit `data_subject.request_failed`
305
+ 3. outbox `data_subject.request_failed`
306
+
307
+ ## 8. Schema/Policy Lint
308
+
309
+ ### 8.1 CLI
310
+
311
+ 0.2.0은 package bin으로 `data-subject`를 제공한다.
312
+
313
+ ```bash
314
+ npx @nestarc/data-subject lint --schema prisma/schema.prisma --config data-subject.config.ts
315
+ ```
316
+
317
+ 필수 옵션:
318
+
319
+ - `--schema`: Prisma schema path
320
+
321
+ 선택 옵션:
322
+
323
+ - `--config`: lint config path
324
+ - `--fail-on warning|error`: non-zero exit 기준. 기본값은 `error`
325
+ - `--format text|json`: 출력 형식. 기본값은 `text`
326
+
327
+ ### 8.2 Config
328
+
329
+ ```ts
330
+ import type { DataSubjectLintConfig } from '@nestarc/data-subject/lint';
331
+
332
+ export default {
333
+ registry: [
334
+ {
335
+ entityName: 'User',
336
+ subjectField: 'id',
337
+ tenantField: 'tenantId',
338
+ fields: {
339
+ email: 'delete',
340
+ name: 'delete',
341
+ },
342
+ },
343
+ ],
344
+ piiFieldPatterns: ['email', 'phone', 'name', 'address', 'ip', 'birth'],
345
+ suppressions: [
346
+ {
347
+ model: 'SystemMigrationLog',
348
+ reason: 'system-owned operational metadata; no subject PII',
349
+ },
350
+ ],
351
+ } satisfies DataSubjectLintConfig;
352
+ ```
353
+
354
+ Config의 `registry`는 runtime executor가 없는 policy metadata만 가진다. programmatic `DataSubjectModule.forRoot({ entities })`와 동일한 policy compiler를 사용해야 한다.
355
+
356
+ ### 8.3 Findings
357
+
358
+ ```ts
359
+ export interface DataSubjectLintFinding {
360
+ severity: 'warning' | 'error';
361
+ code:
362
+ | 'dsr_lint_unregistered_model'
363
+ | 'dsr_lint_missing_policy_field'
364
+ | 'dsr_lint_missing_tenant_field'
365
+ | 'dsr_lint_empty_suppression_reason'
366
+ | 'dsr_lint_invalid_policy'
367
+ | 'dsr_lint_subject_field_missing';
368
+ model: string;
369
+ field?: string;
370
+ message: string;
371
+ }
372
+ ```
373
+
374
+ 필수 검증:
375
+
376
+ - PII 후보 field가 있는 model이 registry 또는 suppression에 없으면 finding을 낸다.
377
+ - registry에 등록된 field가 schema에 없으면 error를 낸다.
378
+ - registry에 등록된 `subjectField`가 schema에 없으면 error를 낸다.
379
+ - multi-tenant mode에서 `tenantField`가 없으면 warning을 낸다.
380
+ - suppression은 non-empty reason을 요구한다.
381
+ - `retain`에는 `legalBasis`가 필요하다.
382
+ - dynamic anonymize replacement는 허용하지 않는다.
383
+
384
+ PII 후보 탐지는 보조 안전장치이며 완전한 data discovery가 아니다. false negative 가능성은 문서에 명시한다.
385
+
386
+ ## 9. Registry Validation
387
+
388
+ 런타임 validation helper를 제공한다.
389
+
390
+ ```ts
391
+ export interface RegistryValidationReport {
392
+ ok: boolean;
393
+ findings: DataSubjectLintFinding[];
394
+ }
395
+
396
+ export function validateRegistry(
397
+ registry: Registry,
398
+ opts?: {
399
+ requireTenantField?: boolean;
400
+ },
401
+ ): RegistryValidationReport;
402
+ ```
403
+
404
+ `validateRegistry`는 Prisma schema를 요구하지 않는 lightweight validation이다.
405
+
406
+ 필수 검증:
407
+
408
+ - entityName 중복은 기존 `Registry.register`에서 즉시 실패한다.
409
+ - fields가 비어 있으면 warning 또는 error를 낸다.
410
+ - `retain` entry는 compiled policy 기준 legalBasis를 가져야 한다.
411
+ - `subjectField`가 빈 문자열이면 error를 낸다.
412
+ - `requireTenantField`가 true인데 entity metadata에 tenantField가 없으면 warning을 낸다.
413
+
414
+ 현재 `EntityPolicy`에는 `tenantField`가 없다. 0.2.0에서 lint metadata에는 `tenantField`를 허용하되, runtime executor boundary는 기존 `fromPrisma({ tenantField })`를 유지한다.
415
+
416
+ ## 10. Audit and Outbox Events
417
+
418
+ ### 10.1 Event names
419
+
420
+ 0.2.0은 다음 event name을 표준화한다.
421
+
422
+ | Event | Audit | Outbox | Payload |
423
+ |---|---|---|---|
424
+ | `data_subject.request_created` | Yes | Yes | `{ requestId, type, tenantId, subjectId? }` |
425
+ | `data_subject.request_processing` | Yes | Optional | `{ requestId, type, tenantId }` |
426
+ | `data_subject.erasure_requested` | Optional | Yes | `{ requestId, tenantId, subjectId, requestedAt }` |
427
+ | `data_subject.request_completed` | Yes | Yes | `{ requestId, type, tenantId, artifactHash }` |
428
+ | `data_subject.request_failed` | Yes | Yes | `{ requestId, type, tenantId, failureReason }` |
429
+ | `data_subject.request_overdue` | Yes | Yes | `{ requestId, type, tenantId, daysOverdue }` |
430
+
431
+ Outbox payload는 subjectId 외 원문 PII를 포함하지 않는다. audit payload는 가능하면 subjectId도 생략하고 `requestId`, `tenantId`, `type` 중심으로 남긴다.
432
+
433
+ ### 10.2 Typed events
434
+
435
+ ```ts
436
+ export type DataSubjectOutboxEvent =
437
+ | {
438
+ type: 'data_subject.request_created';
439
+ payload: {
440
+ requestId: string;
441
+ requestType: 'export' | 'erase';
442
+ tenantId: string;
443
+ subjectId: string;
444
+ };
445
+ }
446
+ | {
447
+ type: 'data_subject.erasure_requested';
448
+ payload: {
449
+ requestId: string;
450
+ tenantId: string;
451
+ subjectId: string;
452
+ requestedAt: string;
453
+ };
454
+ }
455
+ | {
456
+ type: 'data_subject.request_completed';
457
+ payload: {
458
+ requestId: string;
459
+ requestType: 'export' | 'erase';
460
+ tenantId: string;
461
+ artifactHash: string;
462
+ };
463
+ }
464
+ | {
465
+ type: 'data_subject.request_failed';
466
+ payload: {
467
+ requestId: string;
468
+ requestType: 'export' | 'erase';
469
+ tenantId: string;
470
+ failureReason: string;
471
+ };
472
+ };
473
+ ```
474
+
475
+ `failureReason`은 sanitized string이어야 한다. stack trace와 raw row data를 포함하지 않는다.
476
+
477
+ ## 11. Lifecycle and State Transitions
478
+
479
+ 0.2.0의 기본 sync API는 다음 state machine을 따른다.
480
+
481
+ ```txt
482
+ created -> processing -> completed
483
+ created -> processing -> failed
484
+ ```
485
+
486
+ `validating`은 async lifecycle 또는 explicit verification flow를 위한 reserved state다. 0.2.0에서 `validating`을 실제로 사용할 경우 다음 전이만 허용한다.
487
+
488
+ ```txt
489
+ created -> validating -> processing -> completed
490
+ created -> validating -> failed
491
+ created -> processing -> completed
492
+ created -> processing -> failed
493
+ ```
494
+
495
+ 금지 전이:
496
+
497
+ - `completed -> processing`
498
+ - `completed -> failed`
499
+ - `failed -> completed`
500
+ - `failed -> processing`
501
+
502
+ Retry/resume API를 도입하는 경우 새 request id를 만들거나, 별도 `retryOfRequestId` 같은 additive metadata를 사용해야 한다. 기존 request state를 되돌리는 방식은 기본 제공하지 않는다.
503
+
504
+ ## 12. Optional Identity Alias Resolver
505
+
506
+ 0.2.0은 최소 interface만 제공할 수 있다. 기본 구현은 제공하지 않는다.
507
+
508
+ ```ts
509
+ export interface SubjectIdentityAlias {
510
+ kind: 'subjectId' | 'authSub' | 'customerId' | 'emailHash' | 'externalId';
511
+ value: string;
512
+ provider?: string;
513
+ }
514
+
515
+ export interface SubjectIdentityResolver {
516
+ resolve(input: {
517
+ tenantId: string;
518
+ subjectId: string;
519
+ }): Promise<{
520
+ canonicalSubjectId: string;
521
+ aliases: SubjectIdentityAlias[];
522
+ }>;
523
+ }
524
+ ```
525
+
526
+ 원칙:
527
+
528
+ - raw email, phone, address를 alias value로 저장하지 않는다.
529
+ - email 기반 alias가 필요하면 normalized email의 HMAC 같은 irreversible identifier를 consumer가 제공한다.
530
+ - alias resolution은 tenant boundary 안에서만 동작해야 한다.
531
+ - cross-tenant alias merge는 기본 제공하지 않는다.
532
+ - 신원 확인 수준은 consumer 책임이며 법무 검토 필요다.
533
+
534
+ 0.2.0 P0 기능은 resolver 없이도 동작해야 한다.
535
+
536
+ ## 13. Export Artifact
537
+
538
+ 0.1.0의 JSON ZIP export는 유지한다.
539
+
540
+ 0.2.0에서 export artifact는 다음 manifest를 ZIP 안에 포함할 수 있다.
541
+
542
+ ```json
543
+ {
544
+ "schemaVersion": "data-subject.export-manifest.v1",
545
+ "requestId": "dsr_123",
546
+ "tenantId": "tenant_1",
547
+ "generatedAt": "2026-06-19T00:00:00.000Z",
548
+ "format": "json",
549
+ "entities": [
550
+ { "entityName": "User", "rowCount": 1 }
551
+ ]
552
+ }
553
+ ```
554
+
555
+ CSV/NDJSON은 P2다. 0.2.0 P0에서 필수는 아니다.
556
+
557
+ ## 14. Error Codes
558
+
559
+ 기존 error code는 유지한다.
560
+
561
+ | Code | Status | 의미 |
562
+ |---|---:|---|
563
+ | `dsr_subject_not_found` | 404 | subjectId가 tenant scope에 없음 |
564
+ | `dsr_unregistered_entity` | 500 | 등록되지 않은 entity 사용 |
565
+ | `dsr_invalid_policy` | 500 | policy compile 실패 |
566
+ | `dsr_verification_failed` | 500 | erase 후 residual verification 실패 |
567
+ | `dsr_anonymize_dynamic_replacement` | 500 | anonymize replacement가 static value가 아님 |
568
+ | `dsr_entity_already_registered` | 500 | entityName 중복 등록 |
569
+ | `dsr_request_conflict` | 409 | request id 충돌 |
570
+ | `dsr_request_not_found` | 404 | request 조회 또는 update 대상 없음 |
571
+
572
+ 0.2.0에서 추가 가능한 code:
573
+
574
+ | Code | Status | 의미 |
575
+ |---|---:|---|
576
+ | `dsr_artifact_write_failed` | 500 | artifact upload 실패 |
577
+ | `dsr_invalid_state_transition` | 500 | 허용되지 않은 state transition |
578
+ | `dsr_lint_failed` | 1 exit | lint finding으로 CLI 실패 |
579
+ | `dsr_evidence_report_invalid` | 500 | evidence artifact schema 생성 실패 |
580
+
581
+ 새 code를 추가할 때는 `DataSubjectErrorCode`와 HTTP status mapping을 함께 업데이트한다.
582
+
583
+ ## 15. Security and Privacy Requirements
584
+
585
+ - Export ZIP과 erase evidence artifact는 consumer-managed private storage에 저장한다.
586
+ - `artifactUrl`은 public unauthenticated URL이면 안 된다.
587
+ - audit/outbox payload에는 subjectId 외 원문 PII를 포함하지 않는다.
588
+ - erase evidence artifact에는 raw row와 field value를 포함하지 않는다.
589
+ - `failureReason`은 stack trace, SQL, raw data를 제거한 sanitized string이어야 한다.
590
+ - `tenantId`는 모든 list/query/update path에서 scope로 사용한다.
591
+ - `tenantField`가 없는 Prisma executor는 명시적으로 single-tenant 또는 externally scoped로 문서화해야 한다.
592
+ - `subjectId` 평문 저장, request retention 기간, evidence retention 기간은 법무 검토 필요다.
593
+ - anonymize replacement가 재식별 가능하거나 통계적으로 unique하면 익명화로 주장하면 안 된다. 법무 검토 필요다.
594
+
595
+ ## 16. Tests
596
+
597
+ ### 16.1 Unit tests
598
+
599
+ - `PrismaRequestStorage` Date/JSON mapping
600
+ - `PrismaRequestStorage` missing row and duplicate id error mapping
601
+ - deterministic evidence JSON serialization
602
+ - artifact hash consistency
603
+ - lint finding formatter
604
+ - registry validation helper
605
+ - state transition guard
606
+
607
+ ### 16.2 Integration tests
608
+
609
+ - sync erase path stores evidence artifact and request artifact fields
610
+ - failed erase path emits sanitized failure event
611
+ - export path preserves existing behavior
612
+ - `listOverdue` works with Prisma storage
613
+ - `fromPrisma({ tenantField })` prevents cross-tenant selection in fixtures
614
+ - lint CLI exits non-zero on missing policy field
615
+
616
+ ### 16.3 Contract tests
617
+
618
+ 모든 `RequestStorage` 구현체는 동일한 contract test를 통과해야 한다.
619
+
620
+ - insert/findById
621
+ - update partial field
622
+ - listByTenant with state filter
623
+ - listOverdue excludes completed/failed
624
+ - not found behavior
625
+ - conflict behavior
626
+
627
+ 모든 `ArtifactStorage` 구현체는 동일한 contract test를 통과해야 한다.
628
+
629
+ - put/get round trip
630
+ - contentType preservation
631
+ - missing key returns null
632
+ - binary body preservation
633
+
634
+ ## 17. Documentation Requirements
635
+
636
+ 0.2.0 릴리스 전 다음 문서를 정리한다.
637
+
638
+ - README: 0.2.0 quickstart를 `PrismaRequestStorage` 중심으로 갱신
639
+ - README: in-memory storage는 test/dev only로 명시
640
+ - README: erase evidence artifact 예시 추가
641
+ - README: `data-subject lint` 사용법 추가
642
+ - `docs/spec.md`: v0.1 historical spec 또는 superseded 상태 표시
643
+ - `docs/prd.md`: 실제 0.1 구현과 0.2 scope가 다른 부분 표시
644
+ - `docs/compliance.md`: legal interpretation은 법무 검토 필요 문구 유지
645
+
646
+ ## 18. Acceptance Criteria
647
+
648
+ 0.2.0은 다음 기준을 만족해야 릴리스 가능하다.
649
+
650
+ - `npm test`가 통과한다.
651
+ - `npm run build`가 통과한다.
652
+ - `PrismaRequestStorage`가 request storage contract를 통과한다.
653
+ - erase 완료 request가 `artifactHash`, `artifactUrl`, `stats.evidence`를 가진다.
654
+ - erase evidence artifact hash가 request `artifactHash`와 일치한다.
655
+ - evidence artifact snapshot에 raw row 또는 raw PII fixture value가 포함되지 않는다.
656
+ - lint CLI가 fixture schema의 누락 PII field를 탐지한다.
657
+ - audit/outbox event tests가 created, processing, completed, failed path를 검증한다.
658
+ - README의 quickstart가 in-memory storage를 production path로 오해하게 만들지 않는다.
659
+
660
+ ## 19. Open Questions
661
+
662
+ - `PrismaRequestStorage`를 root export로 둘지, `@nestarc/data-subject/prisma` subpath export로 둘지 결정이 필요하다.
663
+ - artifact encryption helper를 0.2.0 필수로 둘지 recipe로 둘지 결정이 필요하다.
664
+ - `subjectId` HMAC 저장 option을 request table에 추가할지, consumer schema 책임으로 둘지 결정이 필요하다. 법무 검토 필요.
665
+ - lint config에서 TypeScript registry loading을 지원할지, JSON metadata만 지원할지 결정이 필요하다.
666
+ - `validating` state를 0.2.0에서 실제로 사용할지 reserved로 유지할지 결정이 필요하다.
667
+ - connector receipt abstraction을 0.2.0에 포함할 경우 request table 확장 없이 stats/artifact만으로 충분한지 검토가 필요하다.
668
+
669
+ ## 20. Final Recommendation
670
+
671
+ 0.2.0의 구현 순서는 다음을 권장한다.
672
+
673
+ 1. `PrismaRequestStorage`와 storage contract test를 먼저 만든다.
674
+ 2. erase runner/service에 pre-scan, post-scan, evidence artifact 저장을 추가한다.
675
+ 3. audit/outbox transition을 정렬하고 failure payload sanitization을 보강한다.
676
+ 4. Prisma schema lint CLI와 registry validation helper를 추가한다.
677
+ 5. README와 기존 docs를 현재 구현 기준으로 정리한다.
678
+
679
+ 이 순서가 가장 작은 API 변화로 가장 큰 운영 신뢰도를 만든다.