From ad07bcd2fb68c4a3f9f6bc79333bbb81fb5cc430 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 18:24:09 +0900 Subject: [PATCH 01/45] test: specify deterministic policy JSON export --- src/policy-export.test.ts | 72 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 src/policy-export.test.ts diff --git a/src/policy-export.test.ts b/src/policy-export.test.ts new file mode 100644 index 0000000..1066c34 --- /dev/null +++ b/src/policy-export.test.ts @@ -0,0 +1,72 @@ +import { describe, expect, it } from 'vitest' +import { createPolicyExport, initialFacts, initialItems } from './policy' + +describe('policy JSON export', () => { + it('projects normalized operator facts into a deterministic versioned draft', () => { + const items = initialItems.map((item) => item.id === 'email' + ? { ...item, enabled: true, mode: '필수' as const, purpose: ' Account access ', detail: ' Signup form ' } + : item) + const facts = { + ...initialFacts, + serviceName: ' Buyer Portal ', + serviceUrl: ' https://buyer.example.test/privacy ', + retentionStatus: 'none' as const, + thirdPartyStatus: 'no' as const, + internationalStatus: 'no' as const, + privacyOfficerName: ' Privacy Team ', + privacyOfficerEmail: ' privacy@example.test ', + } + + expect(createPolicyExport(items, false, facts)).toEqual({ + schema_version: 1, + document_state: 'review_ready', + policy_facts: { + service_profile: { + service_name: 'Buyer Portal', + service_url: 'https://buyer.example.test/privacy', + }, + no_collection_attested: false, + collection_items: [{ + collection_item_key: 'email', + collection_item_label: '이메일 주소', + collection_mode: '필수', + collection_path: 'Signup form', + processing_purpose: 'Account access', + }], + retention: { + retention_status: 'none', + retention_period: null, + }, + third_party_transfer: { + transfer_status: 'no', + recipient_name: null, + transfer_purpose: null, + }, + international_transfer: { + transfer_status: 'no', + destination_country: null, + recipient_name: null, + }, + privacy_contact: { + contact_name: 'Privacy Team', + contact_email: 'privacy@example.test', + }, + }, + review_finding_codes: [], + }) + }) + + it('preserves unresolved state and finding codes without inventing facts', () => { + const exported = createPolicyExport(initialItems, false, initialFacts) + + expect(exported.document_state).toBe('incomplete') + expect(exported.policy_facts.collection_items).toEqual([]) + expect(exported.policy_facts.retention).toEqual({ + retention_status: null, + retention_period: null, + }) + expect(exported.review_finding_codes).toContain('collection_selection') + expect(exported.review_finding_codes).toContain('service_name') + expect(exported.review_finding_codes).toContain('retention_status') + }) +}) From d82485a62e7f55682530ee40bafea5670b18f211 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 18:24:37 +0900 Subject: [PATCH 02/45] test: require browser JSON download --- src/App.test.tsx | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/App.test.tsx b/src/App.test.tsx index 32e95c0..95f8115 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -1,9 +1,12 @@ // @vitest-environment jsdom import { cleanup, fireEvent, render } from '@testing-library/react' -import { afterEach, describe, expect, it } from 'vitest' +import { afterEach, describe, expect, it, vi } from 'vitest' import App from './App' -afterEach(cleanup) +afterEach(() => { + cleanup() + vi.restoreAllMocks() +}) function openCollectionStep(container: HTMLElement) { fireEvent.click(container.querySelectorAll('.rail li button')[1]) @@ -67,12 +70,24 @@ describe('policy editing workflow', () => { expect(reviewDraft).toContain('서비스 URL 형식') }) - it('아직 제공하지 않는 내보내기와 생성 기능을 클릭 가능한 동작처럼 노출하지 않는다', () => { + it('작성 사실을 JSON 파일로 로컬 내보내고 제공하지 않는 생성 기능은 노출하지 않는다', () => { + const createObjectUrl = vi.fn(() => 'blob:policyweave-draft') + const revokeObjectUrl = vi.fn() + Object.defineProperty(URL, 'createObjectURL', { configurable: true, value: createObjectUrl }) + Object.defineProperty(URL, 'revokeObjectURL', { configurable: true, value: revokeObjectUrl }) + const clickDownload = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => undefined) + const { container } = render() const buttons = Array.from(container.querySelectorAll('button')) const exportButton = buttons.find((button) => button.textContent?.includes('JSON 내보내기')) - expect(exportButton?.disabled).toBe(true) - expect(exportButton?.textContent).toContain('준비 중') + expect(exportButton?.disabled).toBe(false) + expect(exportButton?.textContent).not.toContain('준비 중') + + fireEvent.click(exportButton!) + expect(createObjectUrl).toHaveBeenCalledOnce() + expect(createObjectUrl.mock.calls[0][0]).toBeInstanceOf(Blob) + expect(clickDownload).toHaveBeenCalledOnce() + expect(revokeObjectUrl).toHaveBeenCalledWith('blob:policyweave-draft') expect(buttons.find((button) => button.textContent?.includes('개인정보처리방침'))).toBeUndefined() expect(container.querySelector('.document-name')?.tagName).toBe('SPAN') expect(Array.from(container.querySelectorAll('.preview button')).some((button) => button.textContent?.includes('검토본 생성'))).toBe(false) From e834aa742d384c86f6fcd17a73473083e11ac21c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 18:26:22 +0900 Subject: [PATCH 03/45] test: prevent credential URL export --- src/policy-export.test.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/policy-export.test.ts b/src/policy-export.test.ts index 1066c34..3d9b5bf 100644 --- a/src/policy-export.test.ts +++ b/src/policy-export.test.ts @@ -68,5 +68,14 @@ describe('policy JSON export', () => { expect(exported.review_finding_codes).toContain('collection_selection') expect(exported.review_finding_codes).toContain('service_name') expect(exported.review_finding_codes).toContain('retention_status') + }) it('does not export credentials embedded in an invalid service URL', () => { + const exported = createPolicyExport(initialItems, false, { + ...initialFacts, + serviceUrl: 'https://operator:secret@example.test', + }) + + expect(exported.policy_facts.service_profile.service_url).toBeNull() + expect(exported.review_finding_codes).toContain('service_url_format') + expect(JSON.stringify(exported)).not.toContain('operator:secret') }) }) From 553c1a62514286b0a2e57621072a192556594314 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 18:26:38 +0900 Subject: [PATCH 04/45] test: keep export cases independently runnable --- src/policy-export.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/policy-export.test.ts b/src/policy-export.test.ts index 3d9b5bf..37cc650 100644 --- a/src/policy-export.test.ts +++ b/src/policy-export.test.ts @@ -68,7 +68,9 @@ describe('policy JSON export', () => { expect(exported.review_finding_codes).toContain('collection_selection') expect(exported.review_finding_codes).toContain('service_name') expect(exported.review_finding_codes).toContain('retention_status') - }) it('does not export credentials embedded in an invalid service URL', () => { + }) + + it('does not export credentials embedded in an invalid service URL', () => { const exported = createPolicyExport(initialItems, false, { ...initialFacts, serviceUrl: 'https://operator:secret@example.test', From 856958a74319615299f72145ebedb04246121afc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 18:28:45 +0900 Subject: [PATCH 05/45] feat: project deterministic policy draft export --- src/policy.ts | 93 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/src/policy.ts b/src/policy.ts index c792e31..1ec1f2d 100644 --- a/src/policy.ts +++ b/src/policy.ts @@ -148,3 +148,96 @@ export function getCompletedSteps(items: PolicyItem[], noCollectionAttested: boo return completed } + + +/** Versioned local export of operator-authored policy facts and deterministic readiness evidence. */ +export type PolicyDraftExport = { + schema_version: 1 + document_state: 'incomplete' | 'review_ready' + policy_facts: { + service_profile: { + service_name: string | null + service_url: string | null + } + no_collection_attested: boolean + collection_items: Array<{ + collection_item_key: string + collection_item_label: string + collection_mode: CollectionMode + collection_path: string | null + processing_purpose: string | null + }> + retention: { + retention_status: RetentionStatus | null + retention_period: string | null + } + third_party_transfer: { + transfer_status: DisclosureStatus | null + recipient_name: string | null + transfer_purpose: string | null + } + international_transfer: { + transfer_status: DisclosureStatus | null + destination_country: string | null + recipient_name: string | null + } + privacy_contact: { + contact_name: string | null + contact_email: string | null + } + } + review_finding_codes: string[] +} + +/** Creates a deterministic draft export without network access, inferred facts, or credential-bearing service URLs. */ +export function createPolicyExport(items: PolicyItem[], noCollectionAttested: boolean, facts: DraftFacts): PolicyDraftExport { + const trimOrNull = (value: string) => value.trim() || null + const collectionReview = getReview(items, noCollectionAttested) + const reviewFindingCodes = [ + ...(collectionReview.selectionMissing ? ['collection_selection'] : []), + ...(collectionReview.collectionContradiction ? ['collection_contradiction'] : []), + ...collectionReview.modeBlocking.map((item) => `collection_mode:${item.id}`), + ...collectionReview.pathBlocking.map((item) => `collection_path:${item.id}`), + ...collectionReview.blocking.map((item) => `processing_purpose:${item.id}`), + ...getDraftReview(facts, noCollectionAttested).map((finding) => finding.code), + ] + const serviceUrl = facts.serviceUrl.trim() + + return { + schema_version: 1, + document_state: reviewFindingCodes.length === 0 ? 'review_ready' : 'incomplete', + policy_facts: { + service_profile: { + service_name: trimOrNull(facts.serviceName), + service_url: isWebServiceUrl(serviceUrl) ? serviceUrl : null, + }, + no_collection_attested: noCollectionAttested, + collection_items: collectionReview.enabled.map((item) => ({ + collection_item_key: item.id, + collection_item_label: item.label, + collection_mode: item.mode, + collection_path: trimOrNull(item.detail ?? ''), + processing_purpose: trimOrNull(item.purpose), + })), + retention: { + retention_status: facts.retentionStatus || null, + retention_period: facts.retentionStatus === 'applies' ? trimOrNull(facts.retentionPeriod) : null, + }, + third_party_transfer: { + transfer_status: facts.thirdPartyStatus || null, + recipient_name: facts.thirdPartyStatus === 'yes' ? trimOrNull(facts.thirdPartyRecipient) : null, + transfer_purpose: facts.thirdPartyStatus === 'yes' ? trimOrNull(facts.thirdPartyPurpose) : null, + }, + international_transfer: { + transfer_status: facts.internationalStatus || null, + destination_country: facts.internationalStatus === 'yes' ? trimOrNull(facts.internationalCountry) : null, + recipient_name: facts.internationalStatus === 'yes' ? trimOrNull(facts.internationalRecipient) : null, + }, + privacy_contact: { + contact_name: trimOrNull(facts.privacyOfficerName), + contact_email: trimOrNull(facts.privacyOfficerEmail), + }, + }, + review_finding_codes: reviewFindingCodes, + } +} From d883d16bdf116484153895fdc98c8c7fb7bb57a3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 18:29:07 +0900 Subject: [PATCH 06/45] feat: download policy draft JSON locally --- src/App.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 9899035..aeb9642 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,6 @@ import { useMemo, useState } from 'react' import { AlertTriangle, Check, ChevronDown, ExternalLink, FileText, Link, Save } from 'lucide-react' -import { DraftFacts, getCompletedSteps, getDraftReview, getReview, initialFacts, initialItems, isWebServiceUrl, PolicyItem, steps } from './policy' +import { createPolicyExport, DraftFacts, getCompletedSteps, getDraftReview, getReview, initialFacts, initialItems, isWebServiceUrl, PolicyItem, steps } from './policy' type FactField = { key: keyof DraftFacts @@ -217,8 +217,19 @@ export default function App() { const blockingCount = collectionReview.blockingCount + draftFindings.length const [message, setMessage] = useState('') function publish() { setMessage(blockingCount ? '필수 확인 항목을 먼저 입력하세요.' : '필수 확인이 완료되었습니다. 현재 검토본을 책임자와 검토하고 필요한 사실을 보완하세요.') } + function exportDraft() { + const fileUrl = URL.createObjectURL(new Blob([`${JSON.stringify(createPolicyExport(items, noCollectionAttested, facts), null, 2)}\n`], { type: 'application/json' })) + const downloadLink = document.createElement('a') + downloadLink.href = fileUrl + downloadLink.download = 'policyweave-draft.json' + try { + downloadLink.click() + } finally { + URL.revokeObjectURL(fileUrl) + } + } return
-
PolicyWeave{facts.serviceName || '내 서비스'} 개인정보처리방침작성 중버전 0.1.0 (임시저장) 브라우저 작업 중
+
PolicyWeave{facts.serviceName || '내 서비스'} 개인정보처리방침작성 중버전 0.1.0 (임시저장) 브라우저 작업 중
검토 요약확인을 마친 뒤 공개 준비 상태를 확인하세요.
필수 확인 {blockingCount}건
권장 검토 {collectionReview.recommended.length}건
{message}
From 384c337a03f0a248a3f43385b26aa26956cde72b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 18:30:48 +0900 Subject: [PATCH 07/45] test: type the exported Blob mock argument --- src/App.test.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/App.test.tsx b/src/App.test.tsx index 95f8115..bca5865 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -71,7 +71,10 @@ describe('policy editing workflow', () => { }) it('작성 사실을 JSON 파일로 로컬 내보내고 제공하지 않는 생성 기능은 노출하지 않는다', () => { - const createObjectUrl = vi.fn(() => 'blob:policyweave-draft') + const createObjectUrl = vi.fn((fileBlob: Blob) => { + void fileBlob + return 'blob:policyweave-draft' + }) const revokeObjectUrl = vi.fn() Object.defineProperty(URL, 'createObjectURL', { configurable: true, value: createObjectUrl }) Object.defineProperty(URL, 'revokeObjectURL', { configurable: true, value: revokeObjectUrl }) From 2a4249ccc40e880caeac02166a1ed4bbb3c629d7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 18:33:15 +0900 Subject: [PATCH 08/45] docs: define local draft export boundary --- ARCHITECTURE.md | 2 +- CHANGELOG.md | 3 ++- docs/PRD.md | 3 ++- docs/SECURITY.md | 4 ++++ docs/TRD.md | 4 ++++ docs/product-technical-gap-baseline.md | 3 +++ 6 files changed, 16 insertions(+), 3 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index a28f5d9..981aa1f 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -31,7 +31,7 @@ Core invariants: 11. External legal-source updates produce explicit re-evaluation, not silent rewriting. ## Current implementation -The active MVP is a React/Vite browser workspace. State is in memory and there is no production persistence or publication backend. The seven PRD steps are routed to distinct editing surfaces. The collection taxonomy is metadata only. `src/policy.ts` owns deterministic collection-selection/no-collection/mode/purpose/path and non-collection authoring-completeness findings; `src/App.tsx` owns browser orchestration, explicit no-collection and transfer-status capture, warning-to-source navigation, stale dependent-fact invalidation, and deterministic preview rendering. `src/AuthoringFocusController.tsx` is a browser interaction adapter: after explicit rail, previous/next, or review-warning navigation changes the active editing surface, it moves programmatic focus to that surface's heading without changing domain state, intercepting ordinary field interaction, or overriding the separate preview-focus shortcut. +The active MVP is a React/Vite browser workspace. State is in memory and there is no production persistence or publication backend. The browser can download a deterministic versioned JSON draft containing normalized operator-authored facts and readiness finding codes; this local portability projection is not publication, persistence, or legal approval. The seven PRD steps are routed to distinct editing surfaces. The collection taxonomy is metadata only. `src/policy.ts` owns deterministic collection-selection/no-collection/mode/purpose/path and non-collection authoring-completeness findings; `src/App.tsx` owns browser orchestration, explicit no-collection and transfer-status capture, warning-to-source navigation, stale dependent-fact invalidation, and deterministic preview rendering. `src/AuthoringFocusController.tsx` is a browser interaction adapter: after explicit rail, previous/next, or review-warning navigation changes the active editing surface, it moves programmatic focus to that surface's heading without changing domain state, intercepting ordinary field interaction, or overriding the separate preview-focus shortcut. Authoring completeness is deliberately separate from legal sufficiency. Current readiness rules prove that product-defined fact responsibilities were explicitly addressed; they do not assert that a policy complies with law. Source/effective-date-bound legal validation belongs to the Legal Source Registry -> Review & Publication boundary. diff --git a/CHANGELOG.md b/CHANGELOG.md index d22276c..a83f603 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ All notable product changes are recorded here. PolicyWeave is pre-release; entri ## Unreleased ### Added +- Deterministic local JSON draft export with a versioned `snake_case` contract, normalized operator-authored facts, explicit incomplete/review-ready state, readiness finding codes, and fail-closed omission of credential-bearing service URLs. The browser download performs no network transfer and does not claim publication. - PostgreSQL restart and custom-format dump/restore evidence that preserves NULL-safe complete service/collection-item values, a collecting-without-retention cross-state fixture, and independent no-collection and applies-retention facts, then re-executes no-collection plus both retention-status/rule contradictions against the restored database. - PostgreSQL two-session concurrency evidence that observes real lock waits, rejects a collection-item writer racing with a no-collection update, and proves competing same-item UPSERTs converge to one row carrying the second writer's label, mode, and path with NULL-safe complete-value assertions and without timing-based transaction sleeps. - PostgreSQL 18 runtime contract coverage for migration apply/down/apply cycles, item-key UPSERT idempotency, and deferred rejection of no-collection, missing-retention-rule, and revision-owner contradictions. The database remains CI-only and is not a hosted product backend. @@ -45,7 +46,7 @@ All notable product changes are recorded here. PolicyWeave is pre-release; entri - Step-rail, previous/next, and review-warning navigation now transfers programmatic focus to the newly active step heading; ordinary form controls and the dedicated preview shortcut are excluded from that transfer. - Review-warning navigation now lets the browser scroll the focused owner heading into view; the previous `preventScroll` option could leave that heading hundreds of pixels above the desktop or mobile viewport. - The publication-area CTA describes a readiness check and directs the operator to responsible review rather than exposing internal implementation boundaries. -- Unshipped JSON export is visibly disabled as `준비 중`, the redundant no-op `검토본 생성` control was removed, and the document title is non-interactive status text. +- JSON export now downloads the current structured draft locally; the redundant no-op `검토본 생성` control remains removed, and the document title remains non-interactive status text. - Authored generic and custom-checkbox keyboard focus outlines now use the high-contrast `--green` token; a CSS regression test computes and enforces at least 3:1 contrast against white instead of relying on a low-contrast focus color. - Responsive review behavior and mobile publication feedback were repaired during PR review. - Responsive CSS contract tests use literal media-query regular expressions, removing the Semgrep dynamic-RegExp finding without suppressing or weakening the scanner gate. diff --git a/docs/PRD.md b/docs/PRD.md index 3c6b577..0767c4c 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -19,7 +19,7 @@ PolicyWeave는 법률 문장을 임의로 창작하는 도구가 아니다. 운 - 제3자 제공과 국외 이전의 명시적 `있음`/`없음` 확인; `있음`일 때만 종속 상세 사실 요구 - 공개 전 검토 요약과 버전 정보 - 정적 공개 URL 발행 계약(후속 백엔드에서 구현) -- JSON 내보내기 가능한 정책 데이터 모델 +- 버전이 명시된 JSON으로 현재 정책 사실과 검토 상태를 로컬 내보내기 ## 비목표 @@ -45,5 +45,6 @@ PolicyWeave는 법률 문장을 임의로 창작하는 도구가 아니다. 운 - 제3자 제공 또는 국외 이전을 `있음`으로 확인한 경우 해당 수령자/목적 또는 국가/수령자 사실까지 확인되어야 한다. - `없음` 확인은 명시적 운영자 사실이며, 이전에 입력한 종속 제공·이전 상세 사실은 상태 변경 시 폐기된다. - 모든 경고는 해당 입력 단계로 이동할 수 있다. +- JSON 내보내기는 확인되지 않은 값을 임의 사실로 채우지 않고, 자격정보가 포함된 잘못된 서비스 URL을 파일에 기록하지 않으며, 네트워크 전송 없이 현재 작성 사실과 검토 상태를 재현한다. - 모바일에서도 작성과 미리보기를 전환할 수 있다. - 키보드만으로 모든 입력과 주요 동작을 수행할 수 있다. diff --git a/docs/SECURITY.md b/docs/SECURITY.md index a480736..a256f35 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -30,3 +30,7 @@ Protected assets include policy facts, contact details, processing descriptions, ## Verification Security posture is head-specific. A successful predecessor scan, unresolved finding dismissal, or queued security workflow is not passing evidence. Merge/release decisions must reacquire the exact current head's organization-required security/SAST/review checks. + + +## Local JSON export +The export path serializes only the current in-memory PolicyWeave draft and deterministic readiness codes into a browser Blob. It makes no network request, uses a fixed filename rather than customer-controlled path text, and revokes its object URL after initiating the download. A service URL containing username or password components is omitted from the file and remains represented by the `service_url_format` finding. The file is still customer-controlled sensitive data; operators remain responsible for its storage and transfer. This control is not encryption, persistence, publication, backup, or authorization evidence. diff --git a/docs/TRD.md b/docs/TRD.md index 425d035..c06fa8f 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -6,6 +6,7 @@ This TRD covers the pre-release PolicyWeave browser workspace and the contracts ## Current runtime - React + TypeScript + Vite browser application. - Structured authoring state is in browser memory; no production database or backend exists. +- Local JSON export projects the current draft through `createPolicyExport` into deterministic `schema_version = 1` data with normalized facts, explicit `incomplete`/`review_ready` state, and readiness finding codes. Invalid credential-bearing service URLs export as `null`; download uses a browser Blob/object URL and performs no network request. - `src/policy.ts` owns deterministic review logic for collection selection/no-collection attestation/mode/purpose/path and the non-collection authoring-completeness findings for service identity, explicit retention status/period, transfer statuses/details, and privacy contact. - `src/App.tsx` provides the seven-step authoring flow, review navigation, explicit collection/retention/transfer-status capture, stale dependent-fact invalidation, and deterministic preview projection. - `src/AuthoringFocusController.tsx` keeps explicit step navigation and review-warning jumps aligned with the newly active step by moving programmatic focus to its heading after the React update and allowing the browser to reveal that target; ordinary form controls and the dedicated preview shortcut are outside this behavior. @@ -37,6 +38,9 @@ The separation between collection and retention follows the PIPC Standard Person - Hosted web endpoints, when introduced, use non-blocking/asynchronous handling and require realistic k6 tests before a p95 <=20 ms page/API claim is recorded. - Production does not depend on synthetic demo data. +## Local draft portability +The JSON file is a draft portability artifact, not a publication receipt, immutable revision, legal approval, or persistence backup. It may be exported while incomplete so operators can inspect and transfer their authored work without converting blanks into `none`. Contract changes require a new schema version and compatibility evidence; the current fixed filename avoids using customer-controlled text as a filesystem name. + ## Hosted persistence/publication entry criteria Before network persistence lands, define a versioned policy-data schema, migration policy, 3NF relational model, per-item UPSERT/idempotency rules, immutable publication receipt, supersession/rollback semantics, tenant/purpose authorization, audit evidence, encryption/key management, retention/deletion behavior, and backup/restore testing. Use two-or-more-word semantic persistence object names in `snake_case` by default. The revision model must preserve explicit no-collection and explicit retention status independently; `none` must not be materialized from collection absence, and an inapplicable/non-retained state must not carry a live `retention_rule` value. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 6ad587b..f04dd50 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -50,11 +50,14 @@ Queue RCA then observed distinct stale heads `cbfbdae4a4bb72433d7bdcc51afdbd8a29 The restart/restore slice remains bounded CI evidence and does not enable a hosted adapter. After the initial atomic-seed repair, review found that paired collection/applies and no-collection/none fixtures could not prove collection and retention are independent, the restored database re-exercised only the no-collection trigger, and a nullable restored `service_name` could evade `<>` through SQL three-valued logic. Test-only head `5e54834873e125b3e3ce4f599e4037e017330638` added the missing cross-state and NULL-safe assertions; exact-head CI `34204279846` was RED only in the restore step with `restart did not preserve independent collection and retention facts`. The next commit seeds a valid collecting revision with `retention_status = none`, keeps authored service and collection-item assertions NULL-safe, and executes status-side missing-rule plus rule-side unexpected-rule transactions against the restored database. Pre-documentation head `202e69d95c94e4432365d6599016a371c0f2cbc3` CI `34204464388` then passed the complete suite. A later exact-head review found that the nullable authored service URL was not selected or asserted even though the evidence claim covered complete service values. Mutation-probe head `aaef3b5489493669cdb53c08a72b6a109fc0b687` deliberately nulled that URL after restart; CI `34205653966` passed every preceding step and failed only the new NULL-safe restore assertion. Commit `57732c6dbec872ad29e97a7f22096dbba9613e9a` removes the probe while retaining literal name/URL checks. These immutable runs establish the TDD transitions but are not substitutes for the final current-head verdict. This is CI durability evidence, not operational backup, tenant authorization, or a released datastore. +The local-draft portability slice converts the previously disabled JSON affordance into a deterministic versioned export. Test-only head `553c1a62514286b0a2e57621072a192556594314` produced exact RED CI `34210028140`: the four new contracts failed while 62 predecessor tests passed. The minimal projection normalizes authored values, keeps unresolved states explicit, exports readiness finding codes, omits credential-bearing service URLs, and downloads through a fixed-name browser Blob without network transfer. This is a mutable draft artifact, not `publication_revision`, durable persistence, backup, authorization, or legal approval. + ## Current baseline | Area | Evidence | Status | Commercialization gap | Owner/action | Next verification | | --- | --- | --- | --- | --- | --- | | Guided authoring | PRD, ADR-0002, seven routed editors, first-responsibility startup, `getReview`, explicit retention `getDraftReview`, no-collection/transfer states | Repaired foundation | Fresh state no longer skips or falsely completes step 1; collection, retention, transfer and other responsibilities fail closed independently; collection-path evidence remains unstructured free text and legal sufficiency is deliberately separate | Policy Fact Authoring: preserve deterministic completeness and independent authority; add structured path-evidence types only when a real integration/use case proves the need | Exact-head unit/UI edge tests, then browser E2E | +| Draft portability | Versioned `createPolicyExport`, fixed-name browser Blob download, explicit readiness state/finding codes, credential-URL omission | Implemented foundation; exact-head verification required | No import/migration contract or immutable publication semantics; exported files remain operator-controlled sensitive data | Policy Fact Authoring: preserve deterministic schema-versioned projection without network transfer or inferred facts | Exact-head unit/UI/build/browser evidence and future version compatibility tests | | Customer-fact authority | Zero-inferred startup facts; truthful initial rail state; explicit no-collection; independent explicit retention status; explicit transfer states; stale dependent-fact invalidation | Repaired | No known buyer-facing authority dead end remains in the in-memory seven-step fact model; hosted persistence must preserve these independent states without deriving one from another | Policy Fact Authoring: encode collection and retention as separate revision-owned facts; no automatic no-collection→no-retention rule | Persistence/schema invariant tests and exact-head UI tests | | Review workspace | Live preview, total blocker count, warning-to-owner navigation, buyer-facing readiness guidance, deterministic step-heading focus transfer, Playwright viewport/screenshot harness | Implemented foundation; offscreen focus repaired and exact-head browser verified | Automated desktop/tablet/mobile focus-scroll evidence is GREEN; broader interaction coverage remains bounded | UX: retain exact-head artifacts, then extend interaction coverage | Exact-head screenshots, keyboard/focus and accessibility checks | | Accessibility | Semantic controls, visible focus behavior, focus-token >=3:1 regression, muted-text >=4.5:1 authored-surface regression, jsdom focus transition, axe/browser, focused-heading viewport checks, responsive retention transitions, and effective 200% browser-zoom reflow | Partial; bounded exact-head browser GREEN | Native browser UI zoom automation, screen-reader, and manual WCAG evidence remain absent | UX/Test Engineering: add a manual interaction record and remaining cases without claiming conformance from automation alone | Exact-head WCAG/browser matrix plus screen-reader and manual evidence | From e3861409e3d5f786a1a6756178db71912d1edec0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 18:53:53 +0900 Subject: [PATCH 09/45] test: expose export portability edge cases --- src/App.test.tsx | 4 ++++ src/policy-export.test.ts | 11 +++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/App.test.tsx b/src/App.test.tsx index bca5865..12321cd 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -6,6 +6,7 @@ import App from './App' afterEach(() => { cleanup() vi.restoreAllMocks() + vi.useRealTimers() }) function openCollectionStep(container: HTMLElement) { @@ -71,6 +72,7 @@ describe('policy editing workflow', () => { }) it('작성 사실을 JSON 파일로 로컬 내보내고 제공하지 않는 생성 기능은 노출하지 않는다', () => { + vi.useFakeTimers() const createObjectUrl = vi.fn((fileBlob: Blob) => { void fileBlob return 'blob:policyweave-draft' @@ -90,6 +92,8 @@ describe('policy editing workflow', () => { expect(createObjectUrl).toHaveBeenCalledOnce() expect(createObjectUrl.mock.calls[0][0]).toBeInstanceOf(Blob) expect(clickDownload).toHaveBeenCalledOnce() + expect(revokeObjectUrl).not.toHaveBeenCalled() + vi.runAllTimers() expect(revokeObjectUrl).toHaveBeenCalledWith('blob:policyweave-draft') expect(buttons.find((button) => button.textContent?.includes('개인정보처리방침'))).toBeUndefined() expect(container.querySelector('.document-name')?.tagName).toBe('SPAN') diff --git a/src/policy-export.test.ts b/src/policy-export.test.ts index 37cc650..f96b6d9 100644 --- a/src/policy-export.test.ts +++ b/src/policy-export.test.ts @@ -70,6 +70,17 @@ describe('policy JSON export', () => { expect(exported.review_finding_codes).toContain('retention_status') }) + it('normalizes an unresolved collection mode without leaking the UI empty-string sentinel', () => { + const items = initialItems.map((item) => item.id === 'email' + ? { ...item, enabled: true, purpose: 'Account access', detail: 'Signup form' } + : item) + + const exported = createPolicyExport(items, false, initialFacts) + + expect(exported.policy_facts.collection_items[0].collection_mode).toBeNull() + expect(exported.review_finding_codes).toContain('collection_mode:email') + }) + it('does not export credentials embedded in an invalid service URL', () => { const exported = createPolicyExport(initialItems, false, { ...initialFacts, From eaf18cb4115c1419263b132b5693325e2be42a8d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 18:55:34 +0900 Subject: [PATCH 10/45] fix: preserve portable export semantics --- src/App.tsx | 2 +- src/policy.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index aeb9642..f5a91e1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -225,7 +225,7 @@ export default function App() { try { downloadLink.click() } finally { - URL.revokeObjectURL(fileUrl) + setTimeout(() => URL.revokeObjectURL(fileUrl), 0) } } return
diff --git a/src/policy.ts b/src/policy.ts index 1ec1f2d..ef1e84e 100644 --- a/src/policy.ts +++ b/src/policy.ts @@ -163,7 +163,7 @@ export type PolicyDraftExport = { collection_items: Array<{ collection_item_key: string collection_item_label: string - collection_mode: CollectionMode + collection_mode: Exclude | null collection_path: string | null processing_purpose: string | null }> @@ -215,7 +215,7 @@ export function createPolicyExport(items: PolicyItem[], noCollectionAttested: bo collection_items: collectionReview.enabled.map((item) => ({ collection_item_key: item.id, collection_item_label: item.label, - collection_mode: item.mode, + collection_mode: item.mode || null, collection_path: trimOrNull(item.detail ?? ''), processing_purpose: trimOrNull(item.purpose), })), From b72ce67c6992d4206559bef4c4f087aa81add58c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 18:57:15 +0900 Subject: [PATCH 11/45] docs: record export review repairs --- CHANGELOG.md | 2 +- docs/SECURITY.md | 2 +- docs/TRD.md | 2 +- docs/product-technical-gap-baseline.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a83f603..916150d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable product changes are recorded here. PolicyWeave is pre-release; entri ## Unreleased ### Added -- Deterministic local JSON draft export with a versioned `snake_case` contract, normalized operator-authored facts, explicit incomplete/review-ready state, readiness finding codes, and fail-closed omission of credential-bearing service URLs. The browser download performs no network transfer and does not claim publication. +- Deterministic local JSON draft export with a versioned `snake_case` contract, normalized operator-authored facts, explicit incomplete/review-ready state, readiness finding codes, and fail-closed omission of credential-bearing service URLs. Unresolved collection mode is serialized as `null`, not the UI empty-string sentinel, and object-URL cleanup is deferred until after download navigation starts. The browser download performs no network transfer and does not claim publication. - PostgreSQL restart and custom-format dump/restore evidence that preserves NULL-safe complete service/collection-item values, a collecting-without-retention cross-state fixture, and independent no-collection and applies-retention facts, then re-executes no-collection plus both retention-status/rule contradictions against the restored database. - PostgreSQL two-session concurrency evidence that observes real lock waits, rejects a collection-item writer racing with a no-collection update, and proves competing same-item UPSERTs converge to one row carrying the second writer's label, mode, and path with NULL-safe complete-value assertions and without timing-based transaction sleeps. - PostgreSQL 18 runtime contract coverage for migration apply/down/apply cycles, item-key UPSERT idempotency, and deferred rejection of no-collection, missing-retention-rule, and revision-owner contradictions. The database remains CI-only and is not a hosted product backend. diff --git a/docs/SECURITY.md b/docs/SECURITY.md index a256f35..efb5cc1 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -33,4 +33,4 @@ Security posture is head-specific. A successful predecessor scan, unresolved fin ## Local JSON export -The export path serializes only the current in-memory PolicyWeave draft and deterministic readiness codes into a browser Blob. It makes no network request, uses a fixed filename rather than customer-controlled path text, and revokes its object URL after initiating the download. A service URL containing username or password components is omitted from the file and remains represented by the `service_url_format` finding. The file is still customer-controlled sensitive data; operators remain responsible for its storage and transfer. This control is not encryption, persistence, publication, backup, or authorization evidence. +The export path serializes only the current in-memory PolicyWeave draft and deterministic readiness codes into a browser Blob. It makes no network request, uses a fixed filename rather than customer-controlled path text, and defers object-URL revocation until the next task after initiating the download so browsers with deferred navigation can consume the Blob. A service URL containing username or password components is omitted from the file and remains represented by the `service_url_format` finding. The file is still customer-controlled sensitive data; operators remain responsible for its storage and transfer. This control is not encryption, persistence, publication, backup, or authorization evidence. diff --git a/docs/TRD.md b/docs/TRD.md index c06fa8f..c197125 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -6,7 +6,7 @@ This TRD covers the pre-release PolicyWeave browser workspace and the contracts ## Current runtime - React + TypeScript + Vite browser application. - Structured authoring state is in browser memory; no production database or backend exists. -- Local JSON export projects the current draft through `createPolicyExport` into deterministic `schema_version = 1` data with normalized facts, explicit `incomplete`/`review_ready` state, and readiness finding codes. Invalid credential-bearing service URLs export as `null`; download uses a browser Blob/object URL and performs no network request. +- Local JSON export projects the current draft through `createPolicyExport` into deterministic `schema_version = 1` data with normalized facts, explicit `incomplete`/`review_ready` state, and readiness finding codes. Invalid credential-bearing service URLs and unresolved collection modes export as `null`; download uses a browser Blob/object URL, defers URL revocation until the next task so deferred browser navigation can consume it, and performs no network request. - `src/policy.ts` owns deterministic review logic for collection selection/no-collection attestation/mode/purpose/path and the non-collection authoring-completeness findings for service identity, explicit retention status/period, transfer statuses/details, and privacy contact. - `src/App.tsx` provides the seven-step authoring flow, review navigation, explicit collection/retention/transfer-status capture, stale dependent-fact invalidation, and deterministic preview projection. - `src/AuthoringFocusController.tsx` keeps explicit step navigation and review-warning jumps aligned with the newly active step by moving programmatic focus to its heading after the React update and allowing the browser to reveal that target; ordinary form controls and the dedicated preview shortcut are outside this behavior. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index f04dd50..1dd54ed 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -50,7 +50,7 @@ Queue RCA then observed distinct stale heads `cbfbdae4a4bb72433d7bdcc51afdbd8a29 The restart/restore slice remains bounded CI evidence and does not enable a hosted adapter. After the initial atomic-seed repair, review found that paired collection/applies and no-collection/none fixtures could not prove collection and retention are independent, the restored database re-exercised only the no-collection trigger, and a nullable restored `service_name` could evade `<>` through SQL three-valued logic. Test-only head `5e54834873e125b3e3ce4f599e4037e017330638` added the missing cross-state and NULL-safe assertions; exact-head CI `34204279846` was RED only in the restore step with `restart did not preserve independent collection and retention facts`. The next commit seeds a valid collecting revision with `retention_status = none`, keeps authored service and collection-item assertions NULL-safe, and executes status-side missing-rule plus rule-side unexpected-rule transactions against the restored database. Pre-documentation head `202e69d95c94e4432365d6599016a371c0f2cbc3` CI `34204464388` then passed the complete suite. A later exact-head review found that the nullable authored service URL was not selected or asserted even though the evidence claim covered complete service values. Mutation-probe head `aaef3b5489493669cdb53c08a72b6a109fc0b687` deliberately nulled that URL after restart; CI `34205653966` passed every preceding step and failed only the new NULL-safe restore assertion. Commit `57732c6dbec872ad29e97a7f22096dbba9613e9a` removes the probe while retaining literal name/URL checks. These immutable runs establish the TDD transitions but are not substitutes for the final current-head verdict. This is CI durability evidence, not operational backup, tenant authorization, or a released datastore. -The local-draft portability slice converts the previously disabled JSON affordance into a deterministic versioned export. Test-only head `553c1a62514286b0a2e57621072a192556594314` produced exact RED CI `34210028140`: the four new contracts failed while 62 predecessor tests passed. The minimal projection normalizes authored values, keeps unresolved states explicit, exports readiness finding codes, omits credential-bearing service URLs, and downloads through a fixed-name browser Blob without network transfer. This is a mutable draft artifact, not `publication_revision`, durable persistence, backup, authorization, or legal approval. +The local-draft portability slice converts the previously disabled JSON affordance into a deterministic versioned export. Test-only head `553c1a62514286b0a2e57621072a192556594314` produced exact RED CI `34210028140`: the four new contracts failed while 62 predecessor tests passed. The minimal projection normalizes authored values, keeps unresolved states explicit, exports readiness finding codes, omits credential-bearing service URLs, and downloads through a fixed-name browser Blob without network transfer. This is a mutable draft artifact, not `publication_revision`, durable persistence, backup, authorization, or legal approval. Exact-head review then found two portability defects: immediate object-URL revocation could race deferred WebKit navigation, and an unresolved collection mode leaked the UI sentinel `""` into schema v1. Test-only head `e3861409e3d5f786a1a6756178db71912d1edec0` produced RED CI `34212535756` with precisely 65 passed/2 failed. The minimal repair defers revocation to the next task and narrows the exported mode to the selected enum or `null`; implementation head `eaf18cb4115c1419263b132b5693325e2be42a8d` passed lint, 67/67 tests, build, and all PostgreSQL evidence before final documentation sealing. ## Current baseline From 0f1a608428681aebfc9187f3d867c59f9905171b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 19:05:53 +0900 Subject: [PATCH 12/45] test: expose export data and mock leaks --- src/App.test.tsx | 5 +++++ src/policy-export.test.ts | 11 +++++++++++ 2 files changed, 16 insertions(+) diff --git a/src/App.test.tsx b/src/App.test.tsx index 12321cd..8b844d2 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -3,10 +3,15 @@ import { cleanup, fireEvent, render } from '@testing-library/react' import { afterEach, describe, expect, it, vi } from 'vitest' import App from './App' +const originalCreateObjectUrl = URL.createObjectURL +const originalRevokeObjectUrl = URL.revokeObjectURL + afterEach(() => { cleanup() vi.restoreAllMocks() vi.useRealTimers() + expect(URL.createObjectURL).toBe(originalCreateObjectUrl) + expect(URL.revokeObjectURL).toBe(originalRevokeObjectUrl) }) function openCollectionStep(container: HTMLElement) { diff --git a/src/policy-export.test.ts b/src/policy-export.test.ts index f96b6d9..de2b623 100644 --- a/src/policy-export.test.ts +++ b/src/policy-export.test.ts @@ -81,6 +81,17 @@ describe('policy JSON export', () => { expect(exported.review_finding_codes).toContain('collection_mode:email') }) + it('removes query and fragment data from an otherwise valid exported service URL', () => { + const exported = createPolicyExport(initialItems, false, { + ...initialFacts, + serviceUrl: 'https://example.test/privacy?access_token=query-secret#fragment-secret', + }) + + expect(exported.policy_facts.service_profile.service_url).toBe('https://example.test/privacy') + expect(JSON.stringify(exported)).not.toContain('query-secret') + expect(JSON.stringify(exported)).not.toContain('fragment-secret') + }) + it('does not export credentials embedded in an invalid service URL', () => { const exported = createPolicyExport(initialItems, false, { ...initialFacts, From 5c803b1d2369d4149566b1bf30b83248a46c74c1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 19:06:45 +0900 Subject: [PATCH 13/45] fix(export): strip query credentials from service URL --- src/policy.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/policy.ts b/src/policy.ts index ef1e84e..ab3fdac 100644 --- a/src/policy.ts +++ b/src/policy.ts @@ -62,16 +62,24 @@ export const initialFacts: DraftFacts = { export const steps = ['서비스 정보', '수집 항목', '처리 목적', '보유 기간', '제3자 제공', '국외 이전', '개인정보 보호 담당자'] -/** Returns whether a service URL is an absolute HTTP(S) web location suitable for a buyer-facing policy target. */ -export function isWebServiceUrl(value: string) { +/** Returns a canonical credential-free HTTP(S) service URL, or null when the address is not admissible. */ +function normalizeWebServiceUrl(value: string): string | null { try { const url = new URL(value) - return (url.protocol === 'https:' || url.protocol === 'http:') && Boolean(url.hostname) && !url.username && !url.password + if ((url.protocol !== 'https:' && url.protocol !== 'http:') || !url.hostname || url.username || url.password) return null + url.search = '' + url.hash = '' + return url.toString() } catch { - return false + return null } } +/** Returns whether a service URL is an absolute HTTP(S) web location suitable for a buyer-facing policy target. */ +export function isWebServiceUrl(value: string) { + return normalizeWebServiceUrl(value) !== null +} + /** Applies the minimal address-shape contract needed for a usable contact channel without claiming mailbox existence. */ function isContactEmail(value: string) { return /^[^\s@]+@[^\s@]+$/.test(value) @@ -209,7 +217,7 @@ export function createPolicyExport(items: PolicyItem[], noCollectionAttested: bo policy_facts: { service_profile: { service_name: trimOrNull(facts.serviceName), - service_url: isWebServiceUrl(serviceUrl) ? serviceUrl : null, + service_url: normalizeWebServiceUrl(serviceUrl), }, no_collection_attested: noCollectionAttested, collection_items: collectionReview.enabled.map((item) => ({ From 90a3d6810ab213af7fb332df0e88f3475109488e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 19:09:58 +0900 Subject: [PATCH 14/45] test(export): restore browser URL spies --- src/App.test.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/App.test.tsx b/src/App.test.tsx index 8b844d2..94c758c 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -78,13 +78,11 @@ describe('policy editing workflow', () => { it('작성 사실을 JSON 파일로 로컬 내보내고 제공하지 않는 생성 기능은 노출하지 않는다', () => { vi.useFakeTimers() - const createObjectUrl = vi.fn((fileBlob: Blob) => { + const createObjectUrl = vi.spyOn(URL, 'createObjectURL').mockImplementation((fileBlob) => { void fileBlob return 'blob:policyweave-draft' }) - const revokeObjectUrl = vi.fn() - Object.defineProperty(URL, 'createObjectURL', { configurable: true, value: createObjectUrl }) - Object.defineProperty(URL, 'revokeObjectURL', { configurable: true, value: revokeObjectUrl }) + const revokeObjectUrl = vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => undefined) const clickDownload = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => undefined) const { container } = render() From 2833391323b8e70420365f809b0512edddbbd5aa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 19:11:13 +0900 Subject: [PATCH 15/45] docs: record export disclosure controls --- CHANGELOG.md | 2 +- docs/SECURITY.md | 2 +- docs/TRD.md | 2 +- docs/product-technical-gap-baseline.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 916150d..b7bc7a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable product changes are recorded here. PolicyWeave is pre-release; entri ## Unreleased ### Added -- Deterministic local JSON draft export with a versioned `snake_case` contract, normalized operator-authored facts, explicit incomplete/review-ready state, readiness finding codes, and fail-closed omission of credential-bearing service URLs. Unresolved collection mode is serialized as `null`, not the UI empty-string sentinel, and object-URL cleanup is deferred until after download navigation starts. The browser download performs no network transfer and does not claim publication. +- Deterministic local JSON draft export with a versioned `snake_case` contract, normalized operator-authored facts, explicit incomplete/review-ready state, readiness finding codes, and fail-closed omission of credential-bearing service URLs plus query/fragment data. Unresolved collection mode is serialized as `null`, not the UI empty-string sentinel, and object-URL cleanup is deferred until after download navigation starts. The browser download performs no network transfer and does not claim publication. - PostgreSQL restart and custom-format dump/restore evidence that preserves NULL-safe complete service/collection-item values, a collecting-without-retention cross-state fixture, and independent no-collection and applies-retention facts, then re-executes no-collection plus both retention-status/rule contradictions against the restored database. - PostgreSQL two-session concurrency evidence that observes real lock waits, rejects a collection-item writer racing with a no-collection update, and proves competing same-item UPSERTs converge to one row carrying the second writer's label, mode, and path with NULL-safe complete-value assertions and without timing-based transaction sleeps. - PostgreSQL 18 runtime contract coverage for migration apply/down/apply cycles, item-key UPSERT idempotency, and deferred rejection of no-collection, missing-retention-rule, and revision-owner contradictions. The database remains CI-only and is not a hosted product backend. diff --git a/docs/SECURITY.md b/docs/SECURITY.md index efb5cc1..e83b975 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -33,4 +33,4 @@ Security posture is head-specific. A successful predecessor scan, unresolved fin ## Local JSON export -The export path serializes only the current in-memory PolicyWeave draft and deterministic readiness codes into a browser Blob. It makes no network request, uses a fixed filename rather than customer-controlled path text, and defers object-URL revocation until the next task after initiating the download so browsers with deferred navigation can consume the Blob. A service URL containing username or password components is omitted from the file and remains represented by the `service_url_format` finding. The file is still customer-controlled sensitive data; operators remain responsible for its storage and transfer. This control is not encryption, persistence, publication, backup, or authorization evidence. +The export path serializes only the current in-memory PolicyWeave draft and deterministic readiness codes into a browser Blob. It makes no network request, uses a fixed filename rather than customer-controlled path text, and defers object-URL revocation until the next task after initiating the download so browsers with deferred navigation can consume the Blob. A service URL containing username or password components is omitted from the file and remains represented by the `service_url_format` finding. Query and fragment components are removed from accepted URLs so token-like values are not copied into the export. The file is still customer-controlled sensitive data; operators remain responsible for its storage and transfer. This control is not encryption, persistence, publication, backup, or authorization evidence. diff --git a/docs/TRD.md b/docs/TRD.md index c197125..5299b69 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -6,7 +6,7 @@ This TRD covers the pre-release PolicyWeave browser workspace and the contracts ## Current runtime - React + TypeScript + Vite browser application. - Structured authoring state is in browser memory; no production database or backend exists. -- Local JSON export projects the current draft through `createPolicyExport` into deterministic `schema_version = 1` data with normalized facts, explicit `incomplete`/`review_ready` state, and readiness finding codes. Invalid credential-bearing service URLs and unresolved collection modes export as `null`; download uses a browser Blob/object URL, defers URL revocation until the next task so deferred browser navigation can consume it, and performs no network request. +- Local JSON export projects the current draft through `createPolicyExport` into deterministic `schema_version = 1` data with normalized facts, explicit `incomplete`/`review_ready` state, and readiness finding codes. Invalid credential-bearing service URLs export as `null`, accepted service URLs are reduced to origin/path without query or fragment data, and unresolved collection modes export as `null`; download uses a browser Blob/object URL, defers URL revocation until the next task so deferred browser navigation can consume it, and performs no network request. - `src/policy.ts` owns deterministic review logic for collection selection/no-collection attestation/mode/purpose/path and the non-collection authoring-completeness findings for service identity, explicit retention status/period, transfer statuses/details, and privacy contact. - `src/App.tsx` provides the seven-step authoring flow, review navigation, explicit collection/retention/transfer-status capture, stale dependent-fact invalidation, and deterministic preview projection. - `src/AuthoringFocusController.tsx` keeps explicit step navigation and review-warning jumps aligned with the newly active step by moving programmatic focus to its heading after the React update and allowing the browser to reveal that target; ordinary form controls and the dedicated preview shortcut are outside this behavior. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 1dd54ed..2390d2b 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -50,7 +50,7 @@ Queue RCA then observed distinct stale heads `cbfbdae4a4bb72433d7bdcc51afdbd8a29 The restart/restore slice remains bounded CI evidence and does not enable a hosted adapter. After the initial atomic-seed repair, review found that paired collection/applies and no-collection/none fixtures could not prove collection and retention are independent, the restored database re-exercised only the no-collection trigger, and a nullable restored `service_name` could evade `<>` through SQL three-valued logic. Test-only head `5e54834873e125b3e3ce4f599e4037e017330638` added the missing cross-state and NULL-safe assertions; exact-head CI `34204279846` was RED only in the restore step with `restart did not preserve independent collection and retention facts`. The next commit seeds a valid collecting revision with `retention_status = none`, keeps authored service and collection-item assertions NULL-safe, and executes status-side missing-rule plus rule-side unexpected-rule transactions against the restored database. Pre-documentation head `202e69d95c94e4432365d6599016a371c0f2cbc3` CI `34204464388` then passed the complete suite. A later exact-head review found that the nullable authored service URL was not selected or asserted even though the evidence claim covered complete service values. Mutation-probe head `aaef3b5489493669cdb53c08a72b6a109fc0b687` deliberately nulled that URL after restart; CI `34205653966` passed every preceding step and failed only the new NULL-safe restore assertion. Commit `57732c6dbec872ad29e97a7f22096dbba9613e9a` removes the probe while retaining literal name/URL checks. These immutable runs establish the TDD transitions but are not substitutes for the final current-head verdict. This is CI durability evidence, not operational backup, tenant authorization, or a released datastore. -The local-draft portability slice converts the previously disabled JSON affordance into a deterministic versioned export. Test-only head `553c1a62514286b0a2e57621072a192556594314` produced exact RED CI `34210028140`: the four new contracts failed while 62 predecessor tests passed. The minimal projection normalizes authored values, keeps unresolved states explicit, exports readiness finding codes, omits credential-bearing service URLs, and downloads through a fixed-name browser Blob without network transfer. This is a mutable draft artifact, not `publication_revision`, durable persistence, backup, authorization, or legal approval. Exact-head review then found two portability defects: immediate object-URL revocation could race deferred WebKit navigation, and an unresolved collection mode leaked the UI sentinel `""` into schema v1. Test-only head `e3861409e3d5f786a1a6756178db71912d1edec0` produced RED CI `34212535756` with precisely 65 passed/2 failed. The minimal repair defers revocation to the next task and narrows the exported mode to the selected enum or `null`; implementation head `eaf18cb4115c1419263b132b5693325e2be42a8d` passed lint, 67/67 tests, build, and all PostgreSQL evidence before final documentation sealing. +The local-draft portability slice converts the previously disabled JSON affordance into a deterministic versioned export. Test-only head `553c1a62514286b0a2e57621072a192556594314` produced exact RED CI `34210028140`: the four new contracts failed while 62 predecessor tests passed. The minimal projection normalizes authored values, keeps unresolved states explicit, exports readiness finding codes, omits credential-bearing service URLs, and downloads through a fixed-name browser Blob without network transfer. This is a mutable draft artifact, not `publication_revision`, durable persistence, backup, authorization, or legal approval. Exact-head review then found two portability defects: immediate object-URL revocation could race deferred WebKit navigation, and an unresolved collection mode leaked the UI sentinel `""` into schema v1. Test-only head `e3861409e3d5f786a1a6756178db71912d1edec0` produced RED CI `34212535756` with precisely 65 passed/2 failed. The minimal repair defers revocation to the next task and narrows the exported mode to the selected enum or `null`; implementation head `eaf18cb4115c1419263b132b5693325e2be42a8d` passed lint, 67/67 tests, build, and all PostgreSQL evidence before final documentation sealing. A later CodeRabbit review found that direct `Object.defineProperty` URL mocks survived `restoreAllMocks()` and that accepted service URLs could copy query/fragment secrets into JSON. Test-first head `0f1a608428681aebfc9187f3d867c59f9905171b` added both regression contracts, but CI `34213620870` was cancelled before execution when the concurrent writer advanced the same branch, so it is not claimed as RED evidence. Concurrent child `5c803b1d2369d4149566b1bf30b83248a46c74c1` preserves that test head and canonicalizes accepted URLs to credential-free origin/path; `90a3d6810ab213af7fb332df0e88f3475109488e` replaces the URL overrides with restorable Vitest spies. ## Current baseline From 0e9a349d010c9b5918765dae0407de428a960152 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 19:15:58 +0900 Subject: [PATCH 16/45] test(export): reject UI status sentinels in schema types --- src/policy-export.test.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/policy-export.test.ts b/src/policy-export.test.ts index de2b623..68ce89f 100644 --- a/src/policy-export.test.ts +++ b/src/policy-export.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'vitest' +import { describe, expect, expectTypeOf, it } from 'vitest' import { createPolicyExport, initialFacts, initialItems } from './policy' describe('policy JSON export', () => { @@ -70,6 +70,14 @@ describe('policy JSON export', () => { expect(exported.review_finding_codes).toContain('retention_status') }) + it('keeps UI empty-string sentinels out of the portable schema types', () => { + const exported = createPolicyExport(initialItems, false, initialFacts) + + expectTypeOf(exported.policy_facts.retention.retention_status).toEqualTypeOf<'applies' | 'none' | null>() + expectTypeOf(exported.policy_facts.third_party_transfer.transfer_status).toEqualTypeOf<'yes' | 'no' | null>() + expectTypeOf(exported.policy_facts.international_transfer.transfer_status).toEqualTypeOf<'yes' | 'no' | null>() + }) + it('normalizes an unresolved collection mode without leaking the UI empty-string sentinel', () => { const items = initialItems.map((item) => item.id === 'email' ? { ...item, enabled: true, purpose: 'Account access', detail: 'Signup form' } From f3f0f9ef4d26467e5138f649646dd78fa758217b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 19:16:41 +0900 Subject: [PATCH 17/45] fix(export): narrow portable status schema types --- src/policy.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/policy.ts b/src/policy.ts index ab3fdac..7d91424 100644 --- a/src/policy.ts +++ b/src/policy.ts @@ -176,16 +176,16 @@ export type PolicyDraftExport = { processing_purpose: string | null }> retention: { - retention_status: RetentionStatus | null + retention_status: Exclude | null retention_period: string | null } third_party_transfer: { - transfer_status: DisclosureStatus | null + transfer_status: Exclude | null recipient_name: string | null transfer_purpose: string | null } international_transfer: { - transfer_status: DisclosureStatus | null + transfer_status: Exclude | null destination_country: string | null recipient_name: string | null } From 7be6d366540ae87ee2504dba0020bf9d31ab811c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 19:27:06 +0900 Subject: [PATCH 18/45] test: verify policy draft browser download --- tests/e2e/authoring.spec.ts | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/e2e/authoring.spec.ts b/tests/e2e/authoring.spec.ts index 84aff66..f5d8e90 100644 --- a/tests/e2e/authoring.spec.ts +++ b/tests/e2e/authoring.spec.ts @@ -109,3 +109,31 @@ test('reflows the core authoring flow at an effective 200% browser zoom', async await page.keyboard.press('Enter') await expect(page.getByRole('heading', { level: 1, name: '2. 수집 항목' })).toBeFocused() }) + +test('downloads a versioned policy draft with real browser payload semantics', async ({ page }) => { + await page.goto('/') + await page.getByLabel('서비스 이름').fill('Buyer Portal') + + const downloadPromise = page.waitForEvent('download') + await page.getByRole('button', { name: /JSON 내보내기/ }).click() + const download = await downloadPromise + + expect(download.suggestedFilename()).toBe('policyweave-draft.json') + const downloadStream = await download.createReadStream() + downloadStream.setEncoding('utf8') + let downloadContent = '' + for await (const contentChunk of downloadStream) downloadContent += contentChunk + + const exportedDraft = JSON.parse(downloadContent) + expect(exportedDraft).toMatchObject({ + schema_version: 1, + document_state: 'incomplete', + policy_facts: { + service_profile: { + service_name: 'Buyer Portal', + service_url: null, + }, + }, + }) + expect(exportedDraft.review_finding_codes).toEqual(expect.arrayContaining(['service_url', 'collection_selection'])) +}) From 35b9fbd7bdd66642fe2e8996fc6d28e063e3620f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 19:36:09 +0900 Subject: [PATCH 19/45] docs: record browser download evidence --- CHANGELOG.md | 2 +- docs/TRD.md | 2 +- docs/product-technical-gap-baseline.md | 6 ++++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7bc7a6..94f6660 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ All notable product changes are recorded here. PolicyWeave is pre-release; entri - Explicit unresolved/yes/no states for third-party provision and international transfer, with dependent detail capture only for confirmed `yes` cases. - Regression coverage for all seven step routes, zero-inferred startup facts, first-responsibility startup state, explicit no-collection state and stale-item invalidation, independent retention authority and stale-period invalidation, collection-mode/path confirmation, seven-step readiness, explicit no-transfer attestations, transfer-dependent fact invalidation, whitespace normalization, service URL projection, warning navigation, collection-path/purpose separation, stale collection evidence invalidation, buyer-facing publication guidance, non-deceptive handling of unshipped affordances, authored focus-indicator contrast, and authoring-step focus transfer. - Product/technical gap ledger, architecture, technical requirements, security baseline, and legal-source/accessibility traceability. -- Playwright/axe browser evidence harness covering desktop, tablet, and mobile rendering; horizontal overflow; keyboard activation and focus transfer; explicit no-collection progression; retention-status transitions and stale-period invalidation; effective 200% browser-zoom reflow from the desktop profile; serious/critical automated accessibility findings; and exact-head screenshot artifacts. +- Playwright/axe browser evidence harness covering desktop, tablet, and mobile rendering; horizontal overflow; keyboard activation and focus transfer; explicit no-collection progression; retention-status transitions and stale-period invalidation; effective 200% browser-zoom reflow from the desktop profile; serious/critical automated accessibility findings; real-browser JSON download event, fixed filename, and readable payload semantics; and exact-head screenshot artifacts. ### Changed - PostgreSQL negative-path evidence now matches each expected domain error message, so an unrelated SQL or connection failure cannot masquerade as a passing invariant check. diff --git a/docs/TRD.md b/docs/TRD.md index 5299b69..a930f2e 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -10,7 +10,7 @@ This TRD covers the pre-release PolicyWeave browser workspace and the contracts - `src/policy.ts` owns deterministic review logic for collection selection/no-collection attestation/mode/purpose/path and the non-collection authoring-completeness findings for service identity, explicit retention status/period, transfer statuses/details, and privacy contact. - `src/App.tsx` provides the seven-step authoring flow, review navigation, explicit collection/retention/transfer-status capture, stale dependent-fact invalidation, and deterministic preview projection. - `src/AuthoringFocusController.tsx` keeps explicit step navigation and review-warning jumps aligned with the newly active step by moving programmatic focus to its heading after the React update and allowing the browser to reveal that target; ordinary form controls and the dedicated preview shortcut are outside this behavior. -- The current CI contract is lint, Vitest, TypeScript/Vite build, and Playwright Chromium browser evidence plus live organization-required security/review workflows. Browser cases cover desktop/tablet/mobile rendering, keyboard-triggered focus transfer, the explicit no-collection path, retention-status transitions with stale-period invalidation, effective 200% browser-zoom reflow from the desktop layout viewport, horizontal overflow, serious/critical axe findings, and per-project screenshots retained as an exact-head artifact. +- The current CI contract is lint, Vitest, TypeScript/Vite build, and Playwright Chromium browser evidence plus live organization-required security/review workflows. Browser cases cover desktop/tablet/mobile rendering, keyboard-triggered focus transfer, the explicit no-collection path, retention-status transitions with stale-period invalidation, effective 200% browser-zoom reflow from the desktop layout viewport, horizontal overflow, serious/critical axe findings, a real download event with fixed filename and parsed schema-v1 payload, and per-project screenshots retained as an exact-head artifact. - Muted small text uses one authored color token whose contrast is regression-tested against every current surface background at a minimum 4.5:1 ratio; browser axe remains the integration authority for rendered combinations. ## Functional contracts diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 2390d2b..41aefee 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -52,12 +52,14 @@ The restart/restore slice remains bounded CI evidence and does not enable a host The local-draft portability slice converts the previously disabled JSON affordance into a deterministic versioned export. Test-only head `553c1a62514286b0a2e57621072a192556594314` produced exact RED CI `34210028140`: the four new contracts failed while 62 predecessor tests passed. The minimal projection normalizes authored values, keeps unresolved states explicit, exports readiness finding codes, omits credential-bearing service URLs, and downloads through a fixed-name browser Blob without network transfer. This is a mutable draft artifact, not `publication_revision`, durable persistence, backup, authorization, or legal approval. Exact-head review then found two portability defects: immediate object-URL revocation could race deferred WebKit navigation, and an unresolved collection mode leaked the UI sentinel `""` into schema v1. Test-only head `e3861409e3d5f786a1a6756178db71912d1edec0` produced RED CI `34212535756` with precisely 65 passed/2 failed. The minimal repair defers revocation to the next task and narrows the exported mode to the selected enum or `null`; implementation head `eaf18cb4115c1419263b132b5693325e2be42a8d` passed lint, 67/67 tests, build, and all PostgreSQL evidence before final documentation sealing. A later CodeRabbit review found that direct `Object.defineProperty` URL mocks survived `restoreAllMocks()` and that accepted service URLs could copy query/fragment secrets into JSON. Test-first head `0f1a608428681aebfc9187f3d867c59f9905171b` added both regression contracts, but CI `34213620870` was cancelled before execution when the concurrent writer advanced the same branch, so it is not claimed as RED evidence. Concurrent child `5c803b1d2369d4149566b1bf30b83248a46c74c1` preserves that test head and canonicalizes accepted URLs to credential-free origin/path; `90a3d6810ab213af7fb332df0e88f3475109488e` replaces the URL overrides with restorable Vitest spies. +Design-assurance then identified that the local download itself had only jsdom/mocked-anchor evidence. Test-only head `7be6d366540ae87ee2504dba0020bf9d31ab811c` adds a Playwright contract that consumes the real Chromium download event in desktop, tablet, and mobile projects, checks the fixed filename, reads and parses the downloaded file, and verifies schema version, incomplete state, authored service name, explicit null, and finding codes. Exact-head CI `34215521539` passed 69/69 Vitest contracts, the TypeScript/Vite build, PostgreSQL 18.6 migration/concurrency/restart/custom restore, and 16 Playwright/axe cases with two scoped reflow skips; artifact `10051741653` is bound by digest `sha256:9210de5928ff1ab16a0862c8f988b872517af9f28368288e074df187aa04a615`. No production change was needed; this closes the browser-download evidence gap without claiming import, publication, or hosted persistence. + ## Current baseline | Area | Evidence | Status | Commercialization gap | Owner/action | Next verification | | --- | --- | --- | --- | --- | --- | | Guided authoring | PRD, ADR-0002, seven routed editors, first-responsibility startup, `getReview`, explicit retention `getDraftReview`, no-collection/transfer states | Repaired foundation | Fresh state no longer skips or falsely completes step 1; collection, retention, transfer and other responsibilities fail closed independently; collection-path evidence remains unstructured free text and legal sufficiency is deliberately separate | Policy Fact Authoring: preserve deterministic completeness and independent authority; add structured path-evidence types only when a real integration/use case proves the need | Exact-head unit/UI edge tests, then browser E2E | -| Draft portability | Versioned `createPolicyExport`, fixed-name browser Blob download, explicit readiness state/finding codes, credential-URL omission | Implemented foundation; exact-head verification required | No import/migration contract or immutable publication semantics; exported files remain operator-controlled sensitive data | Policy Fact Authoring: preserve deterministic schema-versioned projection without network transfer or inferred facts | Exact-head unit/UI/build/browser evidence and future version compatibility tests | +| Draft portability | Versioned `createPolicyExport`, fixed-name browser Blob download, explicit readiness state/finding codes, credential-URL omission | Implemented foundation; bounded exact-head browser download GREEN | No import/migration contract or immutable publication semantics; exported files remain operator-controlled sensitive data | Policy Fact Authoring: preserve deterministic schema-versioned projection without network transfer or inferred facts | Future version compatibility and fail-closed import tests before schema evolution | | Customer-fact authority | Zero-inferred startup facts; truthful initial rail state; explicit no-collection; independent explicit retention status; explicit transfer states; stale dependent-fact invalidation | Repaired | No known buyer-facing authority dead end remains in the in-memory seven-step fact model; hosted persistence must preserve these independent states without deriving one from another | Policy Fact Authoring: encode collection and retention as separate revision-owned facts; no automatic no-collection→no-retention rule | Persistence/schema invariant tests and exact-head UI tests | | Review workspace | Live preview, total blocker count, warning-to-owner navigation, buyer-facing readiness guidance, deterministic step-heading focus transfer, Playwright viewport/screenshot harness | Implemented foundation; offscreen focus repaired and exact-head browser verified | Automated desktop/tablet/mobile focus-scroll evidence is GREEN; broader interaction coverage remains bounded | UX: retain exact-head artifacts, then extend interaction coverage | Exact-head screenshots, keyboard/focus and accessibility checks | | Accessibility | Semantic controls, visible focus behavior, focus-token >=3:1 regression, muted-text >=4.5:1 authored-surface regression, jsdom focus transition, axe/browser, focused-heading viewport checks, responsive retention transitions, and effective 200% browser-zoom reflow | Partial; bounded exact-head browser GREEN | Native browser UI zoom automation, screen-reader, and manual WCAG evidence remain absent | UX/Test Engineering: add a manual interaction record and remaining cases without claiming conformance from automation alone | Exact-head WCAG/browser matrix plus screen-reader and manual evidence | @@ -65,7 +67,7 @@ The local-draft portability slice converts the previously disabled JSON affordan | Policy model | ADRs, ARCHITECTURE, TRD, ADR-0003, Proposed ERD, up/down migration, schema, runtime, two-session concurrency, restart, and dump/restore contract tests | Proposed 3NF foundation; PostgreSQL 18 exact-head CI execution required; browser runtime remains memory-only | Apply/down/apply, exact negative errors, observed lock waits, conflicting-fact rejection, same-item UPSERT convergence with NULL-safe complete label/mode/path assertions, process restart, and custom-format restore with a collection/no-retention cross-state, NULL-safe complete service name/URL and item assertions, and restored no-collection plus both retention contradiction checks are implemented; authorization, audit, encryption, deletion, and production-scale contention remain unproved | Platform: retain exact-head PostgreSQL evidence, then add the hosted authorization/audit boundary while keeping the adapter disabled | Tenant authorization, immutable audit, and encryption tests | | Publication | Readiness CTA truthfully does not pretend to publish; immutable `publication_revision` is designed | Planned | No authenticated approval, immutable publication, supersession, rollback, or public URL lifecycle | Review & Publication: implement after persistence/security entry criteria | Authorization, replay/digest, supersession tests | | Security/privacy | `docs/SECURITY.md`; local-first runtime; SHA-pinned checkout | Baseline documented | Hosted tenant model, encryption/key handling, audit/incident/retention evidence absent | Platform/Security: threat-model hosted boundary before backend | Exact-head security tests and org scans | -| Tests | 63 unit/UI/schema/workflow regressions plus PostgreSQL runtime/concurrency/restore scripts, thirteen Playwright/axe cases, two project-scoped skips, and a screenshot artifact contract | Improved; exact-head unit/build/browser/PostgreSQL GREEN required | Repository-wide 100% execution/docstring coverage is not yet evidenced | Test Engineering: measure coverage and extend realistic edge cases | Exact-head coverage + browser/database evidence | +| Tests | 69 unit/UI/schema/workflow regressions plus PostgreSQL runtime/concurrency/restore scripts, sixteen Playwright/axe cases, two project-scoped skips, a real-browser download contract, and a screenshot artifact contract | Improved; exact-head unit/build/browser/PostgreSQL GREEN required | Repository-wide 100% execution/docstring coverage is not yet evidenced | Test Engineering: measure coverage and extend realistic edge cases | Exact-head coverage + browser/database evidence | | Performance | Static Vite client | Unevidenced | No realistic buyer-flow browser performance baseline; no network backend exists for meaningful k6 endpoint evidence | Operability: record browser baseline now; add k6 only when hosted network surfaces exist | Real measurements before latency claims | | CI/security merge gate | Repo CI plus active organization ruleset-required workflows; immutable Node 24-based checkout, setup/cache, and artifact-upload action releases | Live external gate; warning-free evidence is re-fetched for the exact merge candidate | Every branch movement invalidates predecessor evidence and stale approval; current hosted jobs may remain queued before runner assignment and dependency/reviewer control-plane failures can fail closed independently | Re-fetch exact-head workflows/reviews; use the central owner path for runner/dependency-review incidents rather than leaf-side churn or bypass | Terminal exact-head checks with no action-runtime deprecation warnings + independent approval + resolved threads | From b731c0150f21cc14d0d365b40dca6b2dceb8715b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 19:44:16 +0900 Subject: [PATCH 20/45] docs: preserve non-recursive evidence wording --- docs/product-technical-gap-baseline.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 41aefee..2c5f368 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -52,7 +52,7 @@ The restart/restore slice remains bounded CI evidence and does not enable a host The local-draft portability slice converts the previously disabled JSON affordance into a deterministic versioned export. Test-only head `553c1a62514286b0a2e57621072a192556594314` produced exact RED CI `34210028140`: the four new contracts failed while 62 predecessor tests passed. The minimal projection normalizes authored values, keeps unresolved states explicit, exports readiness finding codes, omits credential-bearing service URLs, and downloads through a fixed-name browser Blob without network transfer. This is a mutable draft artifact, not `publication_revision`, durable persistence, backup, authorization, or legal approval. Exact-head review then found two portability defects: immediate object-URL revocation could race deferred WebKit navigation, and an unresolved collection mode leaked the UI sentinel `""` into schema v1. Test-only head `e3861409e3d5f786a1a6756178db71912d1edec0` produced RED CI `34212535756` with precisely 65 passed/2 failed. The minimal repair defers revocation to the next task and narrows the exported mode to the selected enum or `null`; implementation head `eaf18cb4115c1419263b132b5693325e2be42a8d` passed lint, 67/67 tests, build, and all PostgreSQL evidence before final documentation sealing. A later CodeRabbit review found that direct `Object.defineProperty` URL mocks survived `restoreAllMocks()` and that accepted service URLs could copy query/fragment secrets into JSON. Test-first head `0f1a608428681aebfc9187f3d867c59f9905171b` added both regression contracts, but CI `34213620870` was cancelled before execution when the concurrent writer advanced the same branch, so it is not claimed as RED evidence. Concurrent child `5c803b1d2369d4149566b1bf30b83248a46c74c1` preserves that test head and canonicalizes accepted URLs to credential-free origin/path; `90a3d6810ab213af7fb332df0e88f3475109488e` replaces the URL overrides with restorable Vitest spies. -Design-assurance then identified that the local download itself had only jsdom/mocked-anchor evidence. Test-only head `7be6d366540ae87ee2504dba0020bf9d31ab811c` adds a Playwright contract that consumes the real Chromium download event in desktop, tablet, and mobile projects, checks the fixed filename, reads and parses the downloaded file, and verifies schema version, incomplete state, authored service name, explicit null, and finding codes. Exact-head CI `34215521539` passed 69/69 Vitest contracts, the TypeScript/Vite build, PostgreSQL 18.6 migration/concurrency/restart/custom restore, and 16 Playwright/axe cases with two scoped reflow skips; artifact `10051741653` is bound by digest `sha256:9210de5928ff1ab16a0862c8f988b872517af9f28368288e074df187aa04a615`. No production change was needed; this closes the browser-download evidence gap without claiming import, publication, or hosted persistence. +Design-assurance then identified that the local download itself had only jsdom/mocked-anchor evidence. Test-only head `7be6d366540ae87ee2504dba0020bf9d31ab811c` adds a Playwright contract that consumes the real Chromium download event in desktop, tablet, and mobile projects, checks the fixed filename, reads and parses the downloaded file, and verifies schema version, incomplete state, authored service name, explicit null, and finding codes. Immutable test-head run `34215521539` passed 69/69 Vitest contracts, the TypeScript/Vite build, PostgreSQL 18.6 migration/concurrency/restart/custom restore, and 16 Playwright/axe cases with two scoped reflow skips; artifact `10051741653` is bound by digest `sha256:9210de5928ff1ab16a0862c8f988b872517af9f28368288e074df187aa04a615`. No production change was needed; this closes the browser-download evidence gap without claiming import, publication, or hosted persistence. ## Current baseline From 798f2c486d19968f7f858219670279cec636fd6b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 20:27:07 +0900 Subject: [PATCH 21/45] test: cover JSON export interactions --- tests/e2e/authoring.spec.ts | 101 ++++++++++++++++++++++++++++++++++-- 1 file changed, 96 insertions(+), 5 deletions(-) diff --git a/tests/e2e/authoring.spec.ts b/tests/e2e/authoring.spec.ts index f5d8e90..72bb1e9 100644 --- a/tests/e2e/authoring.spec.ts +++ b/tests/e2e/authoring.spec.ts @@ -1,5 +1,31 @@ import AxeBuilder from '@axe-core/playwright' -import { expect, test } from '@playwright/test' +import { expect, test, type Download, type Page } from '@playwright/test' + +async function readDownload(download: Download) { + const downloadStream = await download.createReadStream() + downloadStream.setEncoding('utf8') + let downloadContent = '' + for await (const contentChunk of downloadStream) downloadContent += contentChunk + return downloadContent +} + +async function installDownloadAudit(page: Page) { + await page.addInitScript(() => { + const audit = { created: [] as Array<{ url: string; type: string }>, revoked: [] as string[] } + const createObjectUrl = URL.createObjectURL.bind(URL) + const revokeObjectUrl = URL.revokeObjectURL.bind(URL) + Object.defineProperty(window, '__policyweaveDownloadAudit', { value: audit }) + URL.createObjectURL = (object) => { + const url = createObjectUrl(object) + audit.created.push({ url, type: object instanceof Blob ? object.type : '' }) + return url + } + URL.revokeObjectURL = (url) => { + audit.revoked.push(url) + revokeObjectUrl(url) + } + }) +} test('renders a truthful responsive initial workspace without serious accessibility violations', async ({ page }, testInfo) => { await page.goto('/') @@ -119,10 +145,7 @@ test('downloads a versioned policy draft with real browser payload semantics', a const download = await downloadPromise expect(download.suggestedFilename()).toBe('policyweave-draft.json') - const downloadStream = await download.createReadStream() - downloadStream.setEncoding('utf8') - let downloadContent = '' - for await (const contentChunk of downloadStream) downloadContent += contentChunk + const downloadContent = await readDownload(download) const exportedDraft = JSON.parse(downloadContent) expect(exportedDraft).toMatchObject({ @@ -137,3 +160,71 @@ test('downloads a versioned policy draft with real browser payload semantics', a }) expect(exportedDraft.review_finding_codes).toEqual(expect.arrayContaining(['service_url', 'collection_selection'])) }) + +test('keeps keyboard exports byte-stable and revokes every JSON object URL', async ({ page }) => { + await installDownloadAudit(page) + await page.goto('/') + await page.getByLabel('서비스 이름').fill('Buyer Portal') + + const exportButton = page.getByRole('button', { name: /JSON 내보내기/ }) + const exportedBytes: string[] = [] + for (let exportAttempt = 0; exportAttempt < 2; exportAttempt += 1) { + const downloadPromise = page.waitForEvent('download') + await exportButton.focus() + await page.keyboard.press('Enter') + const download = await downloadPromise + expect(download.suggestedFilename()).toBe('policyweave-draft.json') + exportedBytes.push(await readDownload(download)) + } + + expect(exportedBytes[1]).toBe(exportedBytes[0]) + await expect.poll(() => page.evaluate(() => { + const audit = (window as typeof window & { __policyweaveDownloadAudit: { created: Array<{ url: string; type: string }>; revoked: string[] } }).__policyweaveDownloadAudit + return { created: audit.created, revoked: audit.revoked } + })).toEqual({ + created: [ + { url: expect.stringMatching(/^blob:/), type: 'application/json' }, + { url: expect.stringMatching(/^blob:/), type: 'application/json' }, + ], + revoked: [expect.stringMatching(/^blob:/), expect.stringMatching(/^blob:/)], + }) +}) + +test('exports a review-ready no-collection draft without unresolved findings', async ({ page }, testInfo) => { + test.skip(testInfo.project.name !== 'desktop-chromium', 'One complete export proves state semantics; layout coverage is exercised separately.') + + await page.goto('/') + await page.getByLabel('서비스 이름').fill('Example Service') + await page.getByLabel('서비스 URL').fill('https://example.test/privacy') + await page.locator('.rail').getByRole('button', { name: /수집 항목/ }).click() + await page.getByRole('checkbox', { name: '개인정보를 수집하지 않음으로 확인' }).check() + await page.locator('.rail').getByRole('button', { name: /보유 기간/ }).click() + await page.getByLabel('개인정보 보유 여부').selectOption('none') + await page.locator('.rail').getByRole('button', { name: /제3자 제공/ }).click() + await page.getByLabel('제3자 제공 여부').selectOption('no') + await page.locator('.rail').getByRole('button', { name: /국외 이전/ }).click() + await page.getByLabel('국외 이전 여부').selectOption('no') + await page.locator('.rail').getByRole('button', { name: /개인정보 보호 담당자/ }).click() + await page.getByLabel('담당자 또는 담당 부서').fill('Privacy Team') + await page.getByLabel('연락 이메일').fill('privacy@example.test') + await expect(page.getByText('7/7 완료')).toBeVisible() + + const downloadPromise = page.waitForEvent('download') + await page.getByRole('button', { name: /JSON 내보내기/ }).click() + const exportedDraft = JSON.parse(await readDownload(await downloadPromise)) + + expect(exportedDraft.document_state).toBe('review_ready') + expect(exportedDraft.review_finding_codes).toEqual([]) +}) + +test('accepts touch activation for the mobile JSON download', async ({ page }, testInfo) => { + test.skip(testInfo.project.name !== 'mobile-chromium', 'Touch activation is scoped to the touch-enabled mobile project.') + + await page.goto('/') + const downloadPromise = page.waitForEvent('download') + await page.getByRole('button', { name: /JSON 내보내기/ }).tap() + const download = await downloadPromise + + expect(download.suggestedFilename()).toBe('policyweave-draft.json') + expect(JSON.parse(await readDownload(download)).schema_version).toBe(1) +}) From b9507d93cd97b9954febbad4a772ad504ca45c38 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 20:33:48 +0900 Subject: [PATCH 22/45] docs: record JSON export interaction evidence --- CHANGELOG.md | 3 ++- docs/TRD.md | 2 +- docs/product-technical-gap-baseline.md | 6 ++++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94f6660..ab1a005 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ All notable product changes are recorded here. PolicyWeave is pre-release; entri - Explicit unresolved/yes/no states for third-party provision and international transfer, with dependent detail capture only for confirmed `yes` cases. - Regression coverage for all seven step routes, zero-inferred startup facts, first-responsibility startup state, explicit no-collection state and stale-item invalidation, independent retention authority and stale-period invalidation, collection-mode/path confirmation, seven-step readiness, explicit no-transfer attestations, transfer-dependent fact invalidation, whitespace normalization, service URL projection, warning navigation, collection-path/purpose separation, stale collection evidence invalidation, buyer-facing publication guidance, non-deceptive handling of unshipped affordances, authored focus-indicator contrast, and authoring-step focus transfer. - Product/technical gap ledger, architecture, technical requirements, security baseline, and legal-source/accessibility traceability. -- Playwright/axe browser evidence harness covering desktop, tablet, and mobile rendering; horizontal overflow; keyboard activation and focus transfer; explicit no-collection progression; retention-status transitions and stale-period invalidation; effective 200% browser-zoom reflow from the desktop profile; serious/critical automated accessibility findings; real-browser JSON download event, fixed filename, and readable payload semantics; and exact-head screenshot artifacts. +- Playwright/axe browser evidence harness covering desktop, tablet, and mobile rendering; horizontal overflow; keyboard activation and focus transfer; explicit no-collection progression; retention-status transitions and stale-period invalidation; effective 200% browser-zoom reflow from the desktop profile; serious/critical automated accessibility findings; real-browser JSON download events with mouse, keyboard, and touch activation; fixed filename; JSON MIME; byte-stable repeated exports; review-ready payload semantics; object-URL cleanup; and exact-head screenshot artifacts. ### Changed - PostgreSQL negative-path evidence now matches each expected domain error message, so an unrelated SQL or connection failure cannot masquerade as a passing invariant check. @@ -59,3 +59,4 @@ All notable product changes are recorded here. PolicyWeave is pre-release; entri - A product persistence adapter, durable hosted storage, tenant authorization, immutable audit history, encryption, operational backup/restore, and production-scale contention evidence. Bounded CI database execution, including process restart and dump/restore, does not constitute a hosted runtime. - Authenticated immutable publication revisions and public URL lifecycle. - Hosted tenant/security/operability evidence and endpoint load testing. +- Versioned DB-backed ko/en/ja/zh/vi/es/de/fr translation resources and localized export acceptance evidence. diff --git a/docs/TRD.md b/docs/TRD.md index a930f2e..203804b 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -10,7 +10,7 @@ This TRD covers the pre-release PolicyWeave browser workspace and the contracts - `src/policy.ts` owns deterministic review logic for collection selection/no-collection attestation/mode/purpose/path and the non-collection authoring-completeness findings for service identity, explicit retention status/period, transfer statuses/details, and privacy contact. - `src/App.tsx` provides the seven-step authoring flow, review navigation, explicit collection/retention/transfer-status capture, stale dependent-fact invalidation, and deterministic preview projection. - `src/AuthoringFocusController.tsx` keeps explicit step navigation and review-warning jumps aligned with the newly active step by moving programmatic focus to its heading after the React update and allowing the browser to reveal that target; ordinary form controls and the dedicated preview shortcut are outside this behavior. -- The current CI contract is lint, Vitest, TypeScript/Vite build, and Playwright Chromium browser evidence plus live organization-required security/review workflows. Browser cases cover desktop/tablet/mobile rendering, keyboard-triggered focus transfer, the explicit no-collection path, retention-status transitions with stale-period invalidation, effective 200% browser-zoom reflow from the desktop layout viewport, horizontal overflow, serious/critical axe findings, a real download event with fixed filename and parsed schema-v1 payload, and per-project screenshots retained as an exact-head artifact. +- The current CI contract is lint, Vitest, TypeScript/Vite build, and Playwright Chromium browser evidence plus live organization-required security/review workflows. Browser cases cover desktop/tablet/mobile rendering, keyboard-triggered focus transfer, the explicit no-collection path, retention-status transitions with stale-period invalidation, effective 200% browser-zoom reflow from the desktop layout viewport, horizontal overflow, serious/critical axe findings, real download events with mouse, keyboard, and touch activation, fixed filename, JSON MIME, byte-stable repeated exports, review-ready payload semantics, object-URL cleanup, and per-project screenshots retained as an exact-head artifact. - Muted small text uses one authored color token whose contrast is regression-tested against every current surface background at a minimum 4.5:1 ratio; browser axe remains the integration authority for rendered combinations. ## Functional contracts diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 2c5f368..b066328 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -54,12 +54,14 @@ The local-draft portability slice converts the previously disabled JSON affordan Design-assurance then identified that the local download itself had only jsdom/mocked-anchor evidence. Test-only head `7be6d366540ae87ee2504dba0020bf9d31ab811c` adds a Playwright contract that consumes the real Chromium download event in desktop, tablet, and mobile projects, checks the fixed filename, reads and parses the downloaded file, and verifies schema version, incomplete state, authored service name, explicit null, and finding codes. Immutable test-head run `34215521539` passed 69/69 Vitest contracts, the TypeScript/Vite build, PostgreSQL 18.6 migration/concurrency/restart/custom restore, and 16 Playwright/axe cases with two scoped reflow skips; artifact `10051741653` is bound by digest `sha256:9210de5928ff1ab16a0862c8f988b872517af9f28368288e074df187aa04a615`. No production change was needed; this closes the browser-download evidence gap without claiming import, publication, or hosted persistence. +The next exact-head design review correctly limited that evidence to mouse activation and one incomplete Korean-language state. Test-only head `798f2c486d19968f7f858219670279cec636fd6b` retains the real download path and adds keyboard activation across desktop/tablet/mobile, touch activation in the touch-enabled mobile project, JSON MIME inspection, byte equality across repeated exports, complete `review_ready` state with no findings, and one-to-one object-URL revocation. CI `34220766193` passed 69/69 Vitest contracts, TypeScript/Vite build, PostgreSQL 18.6 migration/concurrency/restart/custom restore, and 21 Playwright/axe cases with six intentional project-scope skips. Artifact `10053675937` is bound by digest `sha256:0f600da884ed4a42e2839e05e1dec5c6deda2be6747fb6550a0161315c1e709a`. No production change was needed. Error/cancel behavior and versioned ko/en/ja/zh/vi/es/de/fr resources remain open rather than being inferred from this bounded evidence. + ## Current baseline | Area | Evidence | Status | Commercialization gap | Owner/action | Next verification | | --- | --- | --- | --- | --- | --- | | Guided authoring | PRD, ADR-0002, seven routed editors, first-responsibility startup, `getReview`, explicit retention `getDraftReview`, no-collection/transfer states | Repaired foundation | Fresh state no longer skips or falsely completes step 1; collection, retention, transfer and other responsibilities fail closed independently; collection-path evidence remains unstructured free text and legal sufficiency is deliberately separate | Policy Fact Authoring: preserve deterministic completeness and independent authority; add structured path-evidence types only when a real integration/use case proves the need | Exact-head unit/UI edge tests, then browser E2E | -| Draft portability | Versioned `createPolicyExport`, fixed-name browser Blob download, explicit readiness state/finding codes, credential-URL omission | Implemented foundation; bounded exact-head browser download GREEN | No import/migration contract or immutable publication semantics; exported files remain operator-controlled sensitive data | Policy Fact Authoring: preserve deterministic schema-versioned projection without network transfer or inferred facts | Future version compatibility and fail-closed import tests before schema evolution | +| Draft portability | Versioned `createPolicyExport`, fixed-name browser Blob download, explicit readiness state/finding codes, credential-URL omission, keyboard/touch activation, JSON MIME, repeat-byte and URL-cleanup checks | Implemented foundation; bounded exact-head browser download GREEN | No import/migration contract, error/cancel evidence, localized resource contract, or immutable publication semantics; exported files remain operator-controlled sensitive data | Policy Fact Authoring: preserve deterministic schema-versioned projection without network transfer or inferred facts | Future version compatibility, error/cancel, locale, and fail-closed import tests before schema evolution | | Customer-fact authority | Zero-inferred startup facts; truthful initial rail state; explicit no-collection; independent explicit retention status; explicit transfer states; stale dependent-fact invalidation | Repaired | No known buyer-facing authority dead end remains in the in-memory seven-step fact model; hosted persistence must preserve these independent states without deriving one from another | Policy Fact Authoring: encode collection and retention as separate revision-owned facts; no automatic no-collection→no-retention rule | Persistence/schema invariant tests and exact-head UI tests | | Review workspace | Live preview, total blocker count, warning-to-owner navigation, buyer-facing readiness guidance, deterministic step-heading focus transfer, Playwright viewport/screenshot harness | Implemented foundation; offscreen focus repaired and exact-head browser verified | Automated desktop/tablet/mobile focus-scroll evidence is GREEN; broader interaction coverage remains bounded | UX: retain exact-head artifacts, then extend interaction coverage | Exact-head screenshots, keyboard/focus and accessibility checks | | Accessibility | Semantic controls, visible focus behavior, focus-token >=3:1 regression, muted-text >=4.5:1 authored-surface regression, jsdom focus transition, axe/browser, focused-heading viewport checks, responsive retention transitions, and effective 200% browser-zoom reflow | Partial; bounded exact-head browser GREEN | Native browser UI zoom automation, screen-reader, and manual WCAG evidence remain absent | UX/Test Engineering: add a manual interaction record and remaining cases without claiming conformance from automation alone | Exact-head WCAG/browser matrix plus screen-reader and manual evidence | @@ -67,7 +69,7 @@ Design-assurance then identified that the local download itself had only jsdom/m | Policy model | ADRs, ARCHITECTURE, TRD, ADR-0003, Proposed ERD, up/down migration, schema, runtime, two-session concurrency, restart, and dump/restore contract tests | Proposed 3NF foundation; PostgreSQL 18 exact-head CI execution required; browser runtime remains memory-only | Apply/down/apply, exact negative errors, observed lock waits, conflicting-fact rejection, same-item UPSERT convergence with NULL-safe complete label/mode/path assertions, process restart, and custom-format restore with a collection/no-retention cross-state, NULL-safe complete service name/URL and item assertions, and restored no-collection plus both retention contradiction checks are implemented; authorization, audit, encryption, deletion, and production-scale contention remain unproved | Platform: retain exact-head PostgreSQL evidence, then add the hosted authorization/audit boundary while keeping the adapter disabled | Tenant authorization, immutable audit, and encryption tests | | Publication | Readiness CTA truthfully does not pretend to publish; immutable `publication_revision` is designed | Planned | No authenticated approval, immutable publication, supersession, rollback, or public URL lifecycle | Review & Publication: implement after persistence/security entry criteria | Authorization, replay/digest, supersession tests | | Security/privacy | `docs/SECURITY.md`; local-first runtime; SHA-pinned checkout | Baseline documented | Hosted tenant model, encryption/key handling, audit/incident/retention evidence absent | Platform/Security: threat-model hosted boundary before backend | Exact-head security tests and org scans | -| Tests | 69 unit/UI/schema/workflow regressions plus PostgreSQL runtime/concurrency/restore scripts, sixteen Playwright/axe cases, two project-scoped skips, a real-browser download contract, and a screenshot artifact contract | Improved; exact-head unit/build/browser/PostgreSQL GREEN required | Repository-wide 100% execution/docstring coverage is not yet evidenced | Test Engineering: measure coverage and extend realistic edge cases | Exact-head coverage + browser/database evidence | +| Tests | 69 unit/UI/schema/workflow regressions plus PostgreSQL runtime/concurrency/restore scripts, 21 Playwright/axe passes, six intentional project-scope skips, real-browser interaction/download contracts, and a screenshot artifact contract | Improved; bounded exact-head unit/build/browser/PostgreSQL GREEN | Repository-wide 100% execution/docstring coverage is not yet evidenced | Test Engineering: measure coverage and extend realistic edge cases | Exact-head coverage + browser/database evidence | | Performance | Static Vite client | Unevidenced | No realistic buyer-flow browser performance baseline; no network backend exists for meaningful k6 endpoint evidence | Operability: record browser baseline now; add k6 only when hosted network surfaces exist | Real measurements before latency claims | | CI/security merge gate | Repo CI plus active organization ruleset-required workflows; immutable Node 24-based checkout, setup/cache, and artifact-upload action releases | Live external gate; warning-free evidence is re-fetched for the exact merge candidate | Every branch movement invalidates predecessor evidence and stale approval; current hosted jobs may remain queued before runner assignment and dependency/reviewer control-plane failures can fail closed independently | Re-fetch exact-head workflows/reviews; use the central owner path for runner/dependency-review incidents rather than leaf-side churn or bypass | Terminal exact-head checks with no action-runtime deprecation warnings + independent approval + resolved threads | From 4d57f60713302b2bd8104f631e9416f4867a8b10 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 20:40:48 +0900 Subject: [PATCH 23/45] test: bind revoked URLs to created downloads --- tests/e2e/authoring.spec.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/e2e/authoring.spec.ts b/tests/e2e/authoring.spec.ts index 72bb1e9..4660abc 100644 --- a/tests/e2e/authoring.spec.ts +++ b/tests/e2e/authoring.spec.ts @@ -180,14 +180,14 @@ test('keeps keyboard exports byte-stable and revokes every JSON object URL', asy expect(exportedBytes[1]).toBe(exportedBytes[0]) await expect.poll(() => page.evaluate(() => { const audit = (window as typeof window & { __policyweaveDownloadAudit: { created: Array<{ url: string; type: string }>; revoked: string[] } }).__policyweaveDownloadAudit - return { created: audit.created, revoked: audit.revoked } - })).toEqual({ - created: [ - { url: expect.stringMatching(/^blob:/), type: 'application/json' }, - { url: expect.stringMatching(/^blob:/), type: 'application/json' }, - ], - revoked: [expect.stringMatching(/^blob:/), expect.stringMatching(/^blob:/)], - }) + return { created: audit.created.length, revoked: audit.revoked.length } + })).toEqual({ created: 2, revoked: 2 }) + const downloadAudit = await page.evaluate(() => (window as typeof window & { + __policyweaveDownloadAudit: { created: Array<{ url: string; type: string }>; revoked: string[] } + }).__policyweaveDownloadAudit) + expect(downloadAudit.created.map(({ type }) => type)).toEqual(['application/json', 'application/json']) + expect(new Set(downloadAudit.created.map(({ url }) => url)).size).toBe(2) + expect(downloadAudit.revoked).toEqual(downloadAudit.created.map(({ url }) => url)) }) test('exports a review-ready no-collection draft without unresolved findings', async ({ page }, testInfo) => { From bc4c7f1e16cf5cb2ae8606efe81d8201bbd578de Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 20:54:13 +0900 Subject: [PATCH 24/45] test: specify download activation failure recovery --- tests/e2e/authoring.spec.ts | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/e2e/authoring.spec.ts b/tests/e2e/authoring.spec.ts index 4660abc..8768f3e 100644 --- a/tests/e2e/authoring.spec.ts +++ b/tests/e2e/authoring.spec.ts @@ -190,6 +190,37 @@ test('keeps keyboard exports byte-stable and revokes every JSON object URL', asy expect(downloadAudit.revoked).toEqual(downloadAudit.created.map(({ url }) => url)) }) +test('reports a download activation failure and revokes its JSON object URL', async ({ page }, testInfo) => { + test.skip(testInfo.project.name !== 'desktop-chromium', 'One browser profile proves the activation-error lifecycle.') + + await installDownloadAudit(page) + await page.addInitScript(() => { + const click = HTMLAnchorElement.prototype.click + HTMLAnchorElement.prototype.click = function () { + if (this.download === 'policyweave-draft.json') throw new Error('simulated download activation failure') + click.call(this) + } + }) + await page.goto('/') + + const pageErrors: Error[] = [] + page.on('pageerror', (error) => pageErrors.push(error)) + await page.getByRole('button', { name: /JSON 내보내기/ }).click() + await expect(page.locator('output')).toHaveText('JSON 파일을 내보내지 못했습니다. 다시 시도하세요.') + + await expect.poll(() => page.evaluate(() => { + const audit = (window as typeof window & { + __policyweaveDownloadAudit: { created: Array<{ url: string; type: string }>; revoked: string[] } + }).__policyweaveDownloadAudit + return { created: audit.created.map(({ url }) => url), revoked: audit.revoked } + })).toEqual({ created: [expect.any(String)], revoked: [expect.any(String)] }) + const downloadAudit = await page.evaluate(() => (window as typeof window & { + __policyweaveDownloadAudit: { created: Array<{ url: string; type: string }>; revoked: string[] } + }).__policyweaveDownloadAudit) + expect(downloadAudit.revoked).toEqual(downloadAudit.created.map(({ url }) => url)) + expect(pageErrors).toEqual([]) +}) + test('exports a review-ready no-collection draft without unresolved findings', async ({ page }, testInfo) => { test.skip(testInfo.project.name !== 'desktop-chromium', 'One complete export proves state semantics; layout coverage is exercised separately.') From d8117e2a9a52a7c42941252d77e2327b18d903a2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 20:57:10 +0900 Subject: [PATCH 25/45] fix: report local export activation failures --- src/App.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/App.tsx b/src/App.tsx index f5a91e1..5bdfcfc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -224,6 +224,9 @@ export default function App() { downloadLink.download = 'policyweave-draft.json' try { downloadLink.click() + setMessage('') + } catch { + setMessage('JSON 파일을 내보내지 못했습니다. 다시 시도하세요.') } finally { setTimeout(() => URL.revokeObjectURL(fileUrl), 0) } From f7c3ee276e53a39177e49e6d73cb9958f46a386f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 21:02:37 +0900 Subject: [PATCH 26/45] docs: record export activation failure recovery --- CHANGELOG.md | 4 ++-- docs/SECURITY.md | 2 +- docs/TRD.md | 6 +++--- docs/product-technical-gap-baseline.md | 8 +++++--- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab1a005..a83292d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ All notable product changes are recorded here. PolicyWeave is pre-release; entri - Explicit unresolved/yes/no states for third-party provision and international transfer, with dependent detail capture only for confirmed `yes` cases. - Regression coverage for all seven step routes, zero-inferred startup facts, first-responsibility startup state, explicit no-collection state and stale-item invalidation, independent retention authority and stale-period invalidation, collection-mode/path confirmation, seven-step readiness, explicit no-transfer attestations, transfer-dependent fact invalidation, whitespace normalization, service URL projection, warning navigation, collection-path/purpose separation, stale collection evidence invalidation, buyer-facing publication guidance, non-deceptive handling of unshipped affordances, authored focus-indicator contrast, and authoring-step focus transfer. - Product/technical gap ledger, architecture, technical requirements, security baseline, and legal-source/accessibility traceability. -- Playwright/axe browser evidence harness covering desktop, tablet, and mobile rendering; horizontal overflow; keyboard activation and focus transfer; explicit no-collection progression; retention-status transitions and stale-period invalidation; effective 200% browser-zoom reflow from the desktop profile; serious/critical automated accessibility findings; real-browser JSON download events with mouse, keyboard, and touch activation; fixed filename; JSON MIME; byte-stable repeated exports; review-ready payload semantics; object-URL cleanup; and exact-head screenshot artifacts. +- Playwright/axe browser evidence harness covering desktop, tablet, and mobile rendering; horizontal overflow; keyboard activation and focus transfer; explicit no-collection progression; retention-status transitions and stale-period invalidation; effective 200% browser-zoom reflow from the desktop profile; serious/critical automated accessibility findings; real-browser JSON download events with mouse, keyboard, and touch activation; fixed filename; JSON MIME; byte-stable repeated exports; review-ready payload semantics; success and activation-error object-URL cleanup; and exact-head screenshot artifacts. ### Changed - PostgreSQL negative-path evidence now matches each expected domain error message, so an unrelated SQL or connection failure cannot masquerade as a passing invariant check. @@ -46,7 +46,7 @@ All notable product changes are recorded here. PolicyWeave is pre-release; entri - Step-rail, previous/next, and review-warning navigation now transfers programmatic focus to the newly active step heading; ordinary form controls and the dedicated preview shortcut are excluded from that transfer. - Review-warning navigation now lets the browser scroll the focused owner heading into view; the previous `preventScroll` option could leave that heading hundreds of pixels above the desktop or mobile viewport. - The publication-area CTA describes a readiness check and directs the operator to responsible review rather than exposing internal implementation boundaries. -- JSON export now downloads the current structured draft locally; the redundant no-op `검토본 생성` control remains removed, and the document title remains non-interactive status text. +- JSON export now downloads the current structured draft locally, contains download-activation exceptions, reports a retry action through the existing live status output, and still revokes the temporary object URL; the redundant no-op `검토본 생성` control remains removed, and the document title remains non-interactive status text. - Authored generic and custom-checkbox keyboard focus outlines now use the high-contrast `--green` token; a CSS regression test computes and enforces at least 3:1 contrast against white instead of relying on a low-contrast focus color. - Responsive review behavior and mobile publication feedback were repaired during PR review. - Responsive CSS contract tests use literal media-query regular expressions, removing the Semgrep dynamic-RegExp finding without suppressing or weakening the scanner gate. diff --git a/docs/SECURITY.md b/docs/SECURITY.md index e83b975..66d4a39 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -33,4 +33,4 @@ Security posture is head-specific. A successful predecessor scan, unresolved fin ## Local JSON export -The export path serializes only the current in-memory PolicyWeave draft and deterministic readiness codes into a browser Blob. It makes no network request, uses a fixed filename rather than customer-controlled path text, and defers object-URL revocation until the next task after initiating the download so browsers with deferred navigation can consume the Blob. A service URL containing username or password components is omitted from the file and remains represented by the `service_url_format` finding. Query and fragment components are removed from accepted URLs so token-like values are not copied into the export. The file is still customer-controlled sensitive data; operators remain responsible for its storage and transfer. This control is not encryption, persistence, publication, backup, or authorization evidence. +The export path serializes only the current in-memory PolicyWeave draft and deterministic readiness codes into a browser Blob. It makes no network request, uses a fixed filename rather than customer-controlled path text, and defers object-URL revocation until the next task after initiating the download so browsers with deferred navigation can consume the Blob. If local download activation throws, the exception is contained, the existing live status output directs the operator to retry, and the same temporary object URL is still revoked. A service URL containing username or password components is omitted from the file and remains represented by the `service_url_format` finding. Query and fragment components are removed from accepted URLs so token-like values are not copied into the export. The file is still customer-controlled sensitive data; operators remain responsible for its storage and transfer. This control is not encryption, persistence, publication, backup, authorization, or mid-transfer cancellation evidence. diff --git a/docs/TRD.md b/docs/TRD.md index 203804b..7d4a5c9 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -6,11 +6,11 @@ This TRD covers the pre-release PolicyWeave browser workspace and the contracts ## Current runtime - React + TypeScript + Vite browser application. - Structured authoring state is in browser memory; no production database or backend exists. -- Local JSON export projects the current draft through `createPolicyExport` into deterministic `schema_version = 1` data with normalized facts, explicit `incomplete`/`review_ready` state, and readiness finding codes. Invalid credential-bearing service URLs export as `null`, accepted service URLs are reduced to origin/path without query or fragment data, and unresolved collection modes export as `null`; download uses a browser Blob/object URL, defers URL revocation until the next task so deferred browser navigation can consume it, and performs no network request. +- Local JSON export projects the current draft through `createPolicyExport` into deterministic `schema_version = 1` data with normalized facts, explicit `incomplete`/`review_ready` state, and readiness finding codes. Invalid credential-bearing service URLs export as `null`, accepted service URLs are reduced to origin/path without query or fragment data, and unresolved collection modes export as `null`; download uses a browser Blob/object URL, defers URL revocation until the next task so deferred browser navigation can consume it, contains activation exceptions with a retry message in the existing live status output, and performs no network request. - `src/policy.ts` owns deterministic review logic for collection selection/no-collection attestation/mode/purpose/path and the non-collection authoring-completeness findings for service identity, explicit retention status/period, transfer statuses/details, and privacy contact. - `src/App.tsx` provides the seven-step authoring flow, review navigation, explicit collection/retention/transfer-status capture, stale dependent-fact invalidation, and deterministic preview projection. - `src/AuthoringFocusController.tsx` keeps explicit step navigation and review-warning jumps aligned with the newly active step by moving programmatic focus to its heading after the React update and allowing the browser to reveal that target; ordinary form controls and the dedicated preview shortcut are outside this behavior. -- The current CI contract is lint, Vitest, TypeScript/Vite build, and Playwright Chromium browser evidence plus live organization-required security/review workflows. Browser cases cover desktop/tablet/mobile rendering, keyboard-triggered focus transfer, the explicit no-collection path, retention-status transitions with stale-period invalidation, effective 200% browser-zoom reflow from the desktop layout viewport, horizontal overflow, serious/critical axe findings, real download events with mouse, keyboard, and touch activation, fixed filename, JSON MIME, byte-stable repeated exports, review-ready payload semantics, object-URL cleanup, and per-project screenshots retained as an exact-head artifact. +- The current CI contract is lint, Vitest, TypeScript/Vite build, and Playwright Chromium browser evidence plus live organization-required security/review workflows. Browser cases cover desktop/tablet/mobile rendering, keyboard-triggered focus transfer, the explicit no-collection path, retention-status transitions with stale-period invalidation, effective 200% browser-zoom reflow from the desktop layout viewport, horizontal overflow, serious/critical axe findings, real download events with mouse, keyboard, and touch activation, fixed filename, JSON MIME, byte-stable repeated exports, review-ready payload semantics, object-URL cleanup on success and simulated activation failure, failure announcement, and per-project screenshots retained as an exact-head artifact. - Muted small text uses one authored color token whose contrast is regression-tested against every current surface background at a minimum 4.5:1 ratio; browser axe remains the integration authority for rendered combinations. ## Functional contracts @@ -39,7 +39,7 @@ The separation between collection and retention follows the PIPC Standard Person - Production does not depend on synthetic demo data. ## Local draft portability -The JSON file is a draft portability artifact, not a publication receipt, immutable revision, legal approval, or persistence backup. It may be exported while incomplete so operators can inspect and transfer their authored work without converting blanks into `none`. Contract changes require a new schema version and compatibility evidence; the current fixed filename avoids using customer-controlled text as a filesystem name. +The JSON file is a draft portability artifact, not a publication receipt, immutable revision, legal approval, or persistence backup. It may be exported while incomplete so operators can inspect and transfer their authored work without converting blanks into `none`. Contract changes require a new schema version and compatibility evidence; the current fixed filename avoids using customer-controlled text as a filesystem name. Activation-error recovery does not establish cancellation of an in-progress browser transfer. ## Hosted persistence/publication entry criteria Before network persistence lands, define a versioned policy-data schema, migration policy, 3NF relational model, per-item UPSERT/idempotency rules, immutable publication receipt, supersession/rollback semantics, tenant/purpose authorization, audit evidence, encryption/key management, retention/deletion behavior, and backup/restore testing. Use two-or-more-word semantic persistence object names in `snake_case` by default. The revision model must preserve explicit no-collection and explicit retention status independently; `none` must not be materialized from collection absence, and an inapplicable/non-retained state must not carry a live `retention_rule` value. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index b066328..d08ecd3 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -54,14 +54,16 @@ The local-draft portability slice converts the previously disabled JSON affordan Design-assurance then identified that the local download itself had only jsdom/mocked-anchor evidence. Test-only head `7be6d366540ae87ee2504dba0020bf9d31ab811c` adds a Playwright contract that consumes the real Chromium download event in desktop, tablet, and mobile projects, checks the fixed filename, reads and parses the downloaded file, and verifies schema version, incomplete state, authored service name, explicit null, and finding codes. Immutable test-head run `34215521539` passed 69/69 Vitest contracts, the TypeScript/Vite build, PostgreSQL 18.6 migration/concurrency/restart/custom restore, and 16 Playwright/axe cases with two scoped reflow skips; artifact `10051741653` is bound by digest `sha256:9210de5928ff1ab16a0862c8f988b872517af9f28368288e074df187aa04a615`. No production change was needed; this closes the browser-download evidence gap without claiming import, publication, or hosted persistence. -The next exact-head design review correctly limited that evidence to mouse activation and one incomplete Korean-language state. Test-only head `798f2c486d19968f7f858219670279cec636fd6b` retains the real download path and adds keyboard activation across desktop/tablet/mobile, touch activation in the touch-enabled mobile project, JSON MIME inspection, byte equality across repeated exports, complete `review_ready` state with no findings, and one-to-one object-URL revocation. CI `34220766193` passed 69/69 Vitest contracts, TypeScript/Vite build, PostgreSQL 18.6 migration/concurrency/restart/custom restore, and 21 Playwright/axe cases with six intentional project-scope skips. Artifact `10053675937` is bound by digest `sha256:0f600da884ed4a42e2839e05e1dec5c6deda2be6747fb6550a0161315c1e709a`. No production change was needed. Error/cancel behavior and versioned ko/en/ja/zh/vi/es/de/fr resources remain open rather than being inferred from this bounded evidence. +The next exact-head design review correctly limited that evidence to mouse activation and one incomplete Korean-language state. Test-only head `798f2c486d19968f7f858219670279cec636fd6b` retains the real download path and adds keyboard activation across desktop/tablet/mobile, touch activation in the touch-enabled mobile project, JSON MIME inspection, byte equality across repeated exports, complete `review_ready` state with no findings, and one-to-one object-URL revocation. CI `34220766193` passed 69/69 Vitest contracts, TypeScript/Vite build, PostgreSQL 18.6 migration/concurrency/restart/custom restore, and 21 Playwright/axe cases with six intentional project-scope skips. Artifact `10053675937` is bound by digest `sha256:0f600da884ed4a42e2839e05e1dec5c6deda2be6747fb6550a0161315c1e709a`. No production change was needed. + +The activation-failure pass then found that a browser exception from the generated download link still revoked its object URL but escaped as an unhandled page error and left the operator without a next action. Test-only head `bc4c7f1e16cf5cb2ae8606efe81d8201bbd578de` produced exact RED CI `34223146923`: 69/69 Vitest and every PostgreSQL step passed, while the new desktop Chromium case received an empty live output instead of the required retry guidance. Minimal implementation head `d8117e2a9a52a7c42941252d77e2327b18d903a2` catches only activation exceptions, reuses the existing live status output, and preserves deferred cleanup in `finally`. CI `34223403382` passed 69/69 Vitest, the production build, all PostgreSQL evidence, and 22 Playwright/axe cases with eight intentional project-scope skips. Artifact `10054717353` is bound by digest `sha256:9e58d8a112d4228128794cc35bcb9600e1715c1efb491d0ec9d382d2d2598a3d`. In-progress transfer cancellation and versioned ko/en/ja/zh/vi/es/de/fr resources remain open rather than being inferred from activation-error recovery. ## Current baseline | Area | Evidence | Status | Commercialization gap | Owner/action | Next verification | | --- | --- | --- | --- | --- | --- | | Guided authoring | PRD, ADR-0002, seven routed editors, first-responsibility startup, `getReview`, explicit retention `getDraftReview`, no-collection/transfer states | Repaired foundation | Fresh state no longer skips or falsely completes step 1; collection, retention, transfer and other responsibilities fail closed independently; collection-path evidence remains unstructured free text and legal sufficiency is deliberately separate | Policy Fact Authoring: preserve deterministic completeness and independent authority; add structured path-evidence types only when a real integration/use case proves the need | Exact-head unit/UI edge tests, then browser E2E | -| Draft portability | Versioned `createPolicyExport`, fixed-name browser Blob download, explicit readiness state/finding codes, credential-URL omission, keyboard/touch activation, JSON MIME, repeat-byte and URL-cleanup checks | Implemented foundation; bounded exact-head browser download GREEN | No import/migration contract, error/cancel evidence, localized resource contract, or immutable publication semantics; exported files remain operator-controlled sensitive data | Policy Fact Authoring: preserve deterministic schema-versioned projection without network transfer or inferred facts | Future version compatibility, error/cancel, locale, and fail-closed import tests before schema evolution | +| Draft portability | Versioned `createPolicyExport`, fixed-name browser Blob download, explicit readiness state/finding codes, credential-URL omission, keyboard/touch activation, JSON MIME, repeat-byte checks, activation-error guidance, and success/error URL cleanup | Implemented foundation; bounded exact-head browser download GREEN | No import/migration contract, in-progress cancellation evidence, localized resource contract, or immutable publication semantics; exported files remain operator-controlled sensitive data | Policy Fact Authoring: preserve deterministic schema-versioned projection without network transfer or inferred facts | Future version compatibility, cancellation, locale, and fail-closed import tests before schema evolution | | Customer-fact authority | Zero-inferred startup facts; truthful initial rail state; explicit no-collection; independent explicit retention status; explicit transfer states; stale dependent-fact invalidation | Repaired | No known buyer-facing authority dead end remains in the in-memory seven-step fact model; hosted persistence must preserve these independent states without deriving one from another | Policy Fact Authoring: encode collection and retention as separate revision-owned facts; no automatic no-collection→no-retention rule | Persistence/schema invariant tests and exact-head UI tests | | Review workspace | Live preview, total blocker count, warning-to-owner navigation, buyer-facing readiness guidance, deterministic step-heading focus transfer, Playwright viewport/screenshot harness | Implemented foundation; offscreen focus repaired and exact-head browser verified | Automated desktop/tablet/mobile focus-scroll evidence is GREEN; broader interaction coverage remains bounded | UX: retain exact-head artifacts, then extend interaction coverage | Exact-head screenshots, keyboard/focus and accessibility checks | | Accessibility | Semantic controls, visible focus behavior, focus-token >=3:1 regression, muted-text >=4.5:1 authored-surface regression, jsdom focus transition, axe/browser, focused-heading viewport checks, responsive retention transitions, and effective 200% browser-zoom reflow | Partial; bounded exact-head browser GREEN | Native browser UI zoom automation, screen-reader, and manual WCAG evidence remain absent | UX/Test Engineering: add a manual interaction record and remaining cases without claiming conformance from automation alone | Exact-head WCAG/browser matrix plus screen-reader and manual evidence | @@ -69,7 +71,7 @@ The next exact-head design review correctly limited that evidence to mouse activ | Policy model | ADRs, ARCHITECTURE, TRD, ADR-0003, Proposed ERD, up/down migration, schema, runtime, two-session concurrency, restart, and dump/restore contract tests | Proposed 3NF foundation; PostgreSQL 18 exact-head CI execution required; browser runtime remains memory-only | Apply/down/apply, exact negative errors, observed lock waits, conflicting-fact rejection, same-item UPSERT convergence with NULL-safe complete label/mode/path assertions, process restart, and custom-format restore with a collection/no-retention cross-state, NULL-safe complete service name/URL and item assertions, and restored no-collection plus both retention contradiction checks are implemented; authorization, audit, encryption, deletion, and production-scale contention remain unproved | Platform: retain exact-head PostgreSQL evidence, then add the hosted authorization/audit boundary while keeping the adapter disabled | Tenant authorization, immutable audit, and encryption tests | | Publication | Readiness CTA truthfully does not pretend to publish; immutable `publication_revision` is designed | Planned | No authenticated approval, immutable publication, supersession, rollback, or public URL lifecycle | Review & Publication: implement after persistence/security entry criteria | Authorization, replay/digest, supersession tests | | Security/privacy | `docs/SECURITY.md`; local-first runtime; SHA-pinned checkout | Baseline documented | Hosted tenant model, encryption/key handling, audit/incident/retention evidence absent | Platform/Security: threat-model hosted boundary before backend | Exact-head security tests and org scans | -| Tests | 69 unit/UI/schema/workflow regressions plus PostgreSQL runtime/concurrency/restore scripts, 21 Playwright/axe passes, six intentional project-scope skips, real-browser interaction/download contracts, and a screenshot artifact contract | Improved; bounded exact-head unit/build/browser/PostgreSQL GREEN | Repository-wide 100% execution/docstring coverage is not yet evidenced | Test Engineering: measure coverage and extend realistic edge cases | Exact-head coverage + browser/database evidence | +| Tests | 69 unit/UI/schema/workflow regressions plus PostgreSQL runtime/concurrency/restore scripts, 22 Playwright/axe passes, eight intentional project-scope skips, real-browser interaction/download/error contracts, and a screenshot artifact contract | Improved; bounded exact-head unit/build/browser/PostgreSQL GREEN | Repository-wide 100% execution/docstring coverage is not yet evidenced | Test Engineering: measure coverage and extend realistic edge cases | Exact-head coverage + browser/database evidence | | Performance | Static Vite client | Unevidenced | No realistic buyer-flow browser performance baseline; no network backend exists for meaningful k6 endpoint evidence | Operability: record browser baseline now; add k6 only when hosted network surfaces exist | Real measurements before latency claims | | CI/security merge gate | Repo CI plus active organization ruleset-required workflows; immutable Node 24-based checkout, setup/cache, and artifact-upload action releases | Live external gate; warning-free evidence is re-fetched for the exact merge candidate | Every branch movement invalidates predecessor evidence and stale approval; current hosted jobs may remain queued before runner assignment and dependency/reviewer control-plane failures can fail closed independently | Re-fetch exact-head workflows/reviews; use the central owner path for runner/dependency-review incidents rather than leaf-side churn or bypass | Terminal exact-head checks with no action-runtime deprecation warnings + independent approval + resolved threads | From 01ab876a228d8cf9caadce2a4760ca3a1ffeedb7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 21:07:32 +0900 Subject: [PATCH 27/45] test: fail closed on lossy service URL export --- src/policy-export.test.ts | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/policy-export.test.ts b/src/policy-export.test.ts index 68ce89f..4c04874 100644 --- a/src/policy-export.test.ts +++ b/src/policy-export.test.ts @@ -89,15 +89,27 @@ describe('policy JSON export', () => { expect(exported.review_finding_codes).toContain('collection_mode:email') }) - it('removes query and fragment data from an otherwise valid exported service URL', () => { - const exported = createPolicyExport(initialItems, false, { + it('fails closed rather than rewriting query-dependent or fragment-routed service URLs', () => { + for (const serviceUrl of [ + 'https://example.test/app?tenant=acme', + 'https://example.test/#/privacy', + 'https://example.test/privacy?access_token=query-secret#fragment-secret', + ]) { + const exported = createPolicyExport(initialItems, false, { + ...initialFacts, + serviceUrl, + }) + + expect(exported.policy_facts.service_profile.service_url).toBeNull() + expect(exported.review_finding_codes).toContain('service_url_format') + } + + const credentialExport = createPolicyExport(initialItems, false, { ...initialFacts, serviceUrl: 'https://example.test/privacy?access_token=query-secret#fragment-secret', }) - - expect(exported.policy_facts.service_profile.service_url).toBe('https://example.test/privacy') - expect(JSON.stringify(exported)).not.toContain('query-secret') - expect(JSON.stringify(exported)).not.toContain('fragment-secret') + expect(JSON.stringify(credentialExport)).not.toContain('query-secret') + expect(JSON.stringify(credentialExport)).not.toContain('fragment-secret') }) it('does not export credentials embedded in an invalid service URL', () => { @@ -110,4 +122,4 @@ describe('policy JSON export', () => { expect(exported.review_finding_codes).toContain('service_url_format') expect(JSON.stringify(exported)).not.toContain('operator:secret') }) -}) +}) \ No newline at end of file From 3f16e062e5d68dd9ec4b839514ddc2aa396a4358 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 21:08:31 +0900 Subject: [PATCH 28/45] fix: reject lossy service URL export --- src/policy.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/policy.ts b/src/policy.ts index 7d91424..c2e0f4c 100644 --- a/src/policy.ts +++ b/src/policy.ts @@ -67,8 +67,7 @@ function normalizeWebServiceUrl(value: string): string | null { try { const url = new URL(value) if ((url.protocol !== 'https:' && url.protocol !== 'http:') || !url.hostname || url.username || url.password) return null - url.search = '' - url.hash = '' + if (url.search || url.hash) return null return url.toString() } catch { return null @@ -248,4 +247,4 @@ export function createPolicyExport(items: PolicyItem[], noCollectionAttested: bo }, review_finding_codes: reviewFindingCodes, } -} +} \ No newline at end of file From c2f4887080d572d6c16347395b306905f18e6846 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 21:09:50 +0900 Subject: [PATCH 29/45] test: preserve service URL destination semantics --- src/App.test.tsx | 12 ++++++++++++ src/policy-export.test.ts | 27 ++++++++------------------- src/policy.ts | 2 +- 3 files changed, 21 insertions(+), 20 deletions(-) diff --git a/src/App.test.tsx b/src/App.test.tsx index 94c758c..ed4ca98 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -76,6 +76,18 @@ describe('policy editing workflow', () => { expect(reviewDraft).toContain('서비스 URL 형식') }) + it('query 또는 fragment가 있는 서비스 URL은 다른 위치로 재작성하지 않고 수정 대상으로 남긴다', () => { + const { container } = render() + const serviceUrl = container.querySelector('input[name="serviceUrl"]')! + + for (const value of ['https://example.test/app?tenant=acme', 'https://example.test/#/privacy']) { + fireEvent.change(serviceUrl, { target: { value } }) + const reviewDraft = container.querySelector('.paper')?.textContent ?? '' + expect(reviewDraft).not.toContain(value) + expect(reviewDraft).toContain('서비스 URL 형식') + } + }) + it('작성 사실을 JSON 파일로 로컬 내보내고 제공하지 않는 생성 기능은 노출하지 않는다', () => { vi.useFakeTimers() const createObjectUrl = vi.spyOn(URL, 'createObjectURL').mockImplementation((fileBlob) => { diff --git a/src/policy-export.test.ts b/src/policy-export.test.ts index 4c04874..6eab374 100644 --- a/src/policy-export.test.ts +++ b/src/policy-export.test.ts @@ -89,27 +89,16 @@ describe('policy JSON export', () => { expect(exported.review_finding_codes).toContain('collection_mode:email') }) - it('fails closed rather than rewriting query-dependent or fragment-routed service URLs', () => { - for (const serviceUrl of [ - 'https://example.test/app?tenant=acme', - 'https://example.test/#/privacy', - 'https://example.test/privacy?access_token=query-secret#fragment-secret', - ]) { - const exported = createPolicyExport(initialItems, false, { - ...initialFacts, - serviceUrl, - }) - - expect(exported.policy_facts.service_profile.service_url).toBeNull() - expect(exported.review_finding_codes).toContain('service_url_format') - } - - const credentialExport = createPolicyExport(initialItems, false, { + it('rejects query and fragment service URLs instead of rewriting the authored destination', () => { + const exported = createPolicyExport(initialItems, false, { ...initialFacts, serviceUrl: 'https://example.test/privacy?access_token=query-secret#fragment-secret', }) - expect(JSON.stringify(credentialExport)).not.toContain('query-secret') - expect(JSON.stringify(credentialExport)).not.toContain('fragment-secret') + + expect(exported.policy_facts.service_profile.service_url).toBeNull() + expect(exported.review_finding_codes).toContain('service_url_format') + expect(JSON.stringify(exported)).not.toContain('query-secret') + expect(JSON.stringify(exported)).not.toContain('fragment-secret') }) it('does not export credentials embedded in an invalid service URL', () => { @@ -122,4 +111,4 @@ describe('policy JSON export', () => { expect(exported.review_finding_codes).toContain('service_url_format') expect(JSON.stringify(exported)).not.toContain('operator:secret') }) -}) \ No newline at end of file +}) diff --git a/src/policy.ts b/src/policy.ts index c2e0f4c..7bcd8d6 100644 --- a/src/policy.ts +++ b/src/policy.ts @@ -247,4 +247,4 @@ export function createPolicyExport(items: PolicyItem[], noCollectionAttested: bo }, review_finding_codes: reviewFindingCodes, } -} \ No newline at end of file +} From 71ef18702168ad4e4fee5a11977ad5c56749b4d7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 21:14:08 +0900 Subject: [PATCH 30/45] docs: align service URL authority contract --- CHANGELOG.md | 3 ++- docs/SECURITY.md | 4 ++-- docs/TRD.md | 4 ++-- docs/product-technical-gap-baseline.md | 8 +++++--- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a83292d..5588e84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable product changes are recorded here. PolicyWeave is pre-release; entri ## Unreleased ### Added -- Deterministic local JSON draft export with a versioned `snake_case` contract, normalized operator-authored facts, explicit incomplete/review-ready state, readiness finding codes, and fail-closed omission of credential-bearing service URLs plus query/fragment data. Unresolved collection mode is serialized as `null`, not the UI empty-string sentinel, and object-URL cleanup is deferred until after download navigation starts. The browser download performs no network transfer and does not claim publication. +- Deterministic local JSON draft export with a versioned `snake_case` contract, normalized operator-authored facts, explicit incomplete/review-ready state, readiness finding codes, and fail-closed rejection of service URLs containing credentials, query, or fragment components. Unresolved collection mode is serialized as `null`, not the UI empty-string sentinel, and object-URL cleanup is deferred until after download navigation starts. The browser download performs no network transfer and does not claim publication. - PostgreSQL restart and custom-format dump/restore evidence that preserves NULL-safe complete service/collection-item values, a collecting-without-retention cross-state fixture, and independent no-collection and applies-retention facts, then re-executes no-collection plus both retention-status/rule contradictions against the restored database. - PostgreSQL two-session concurrency evidence that observes real lock waits, rejects a collection-item writer racing with a no-collection update, and proves competing same-item UPSERTs converge to one row carrying the second writer's label, mode, and path with NULL-safe complete-value assertions and without timing-based transaction sleeps. - PostgreSQL 18 runtime contract coverage for migration apply/down/apply cycles, item-key UPSERT idempotency, and deferred rejection of no-collection, missing-retention-rule, and revision-owner contradictions. The database remains CI-only and is not a hosted product backend. @@ -37,6 +37,7 @@ All notable product changes are recorded here. PolicyWeave is pre-release; entri - Public-readiness includes product-defined service name/URL, explicit retention status and any required period, transfer-status/detail, and privacy-contact completeness. - Service URL and privacy-contact email are shape-validated as usability contracts without claiming endpoint reachability or mailbox ownership. - Credential-bearing service URLs are rejected and withheld from the review projection so embedded usernames or passwords cannot leak into a generated draft. +- Query- or fragment-bearing service URLs are rejected consistently by readiness, preview, and export so a source fact cannot be silently rewritten to a different destination. - Blank transfer state is no longer treated as an implicit `none`; explicit `없음` confirmation is required, while `있음` requires dependent recipient/purpose or country/recipient facts. - Changing a transfer status away from `있음` clears its dependent details so stale customer facts cannot silently revive. - Disabling a collection item clears its collection mode, processing purpose, and collection-path evidence so re-enabling cannot silently revive stale customer facts. diff --git a/docs/SECURITY.md b/docs/SECURITY.md index 66d4a39..7c1cda0 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -26,11 +26,11 @@ Protected assets include policy facts, contact details, processing descriptions, 6. Validate and encode user-entered content at output boundaries; do not treat imported HTML/Markdown/source material as executable instructions. 7. Define backup/restore, incident response, access review, retention/deletion, and evidence collection before claiming SOC 2 readiness. Map hosted controls toward CSAP and SOC 2 without describing an unassessed product as certified. 8. Tests/docs use fictionalized organizations and people; real personal/institutional names are not fixtures. -9. Credential-bearing service URLs are invalid and are withheld from the review projection; operators must provide a credential-free HTTP(S) location. +9. Service URLs containing credentials, query, or fragment components are invalid and withheld from the review projection; operators must provide a credential-free HTTP(S) location whose destination can be exported without lossy rewriting. ## Verification Security posture is head-specific. A successful predecessor scan, unresolved finding dismissal, or queued security workflow is not passing evidence. Merge/release decisions must reacquire the exact current head's organization-required security/SAST/review checks. ## Local JSON export -The export path serializes only the current in-memory PolicyWeave draft and deterministic readiness codes into a browser Blob. It makes no network request, uses a fixed filename rather than customer-controlled path text, and defers object-URL revocation until the next task after initiating the download so browsers with deferred navigation can consume the Blob. If local download activation throws, the exception is contained, the existing live status output directs the operator to retry, and the same temporary object URL is still revoked. A service URL containing username or password components is omitted from the file and remains represented by the `service_url_format` finding. Query and fragment components are removed from accepted URLs so token-like values are not copied into the export. The file is still customer-controlled sensitive data; operators remain responsible for its storage and transfer. This control is not encryption, persistence, publication, backup, authorization, or mid-transfer cancellation evidence. +The export path serializes only the current in-memory PolicyWeave draft and deterministic readiness codes into a browser Blob. It makes no network request, uses a fixed filename rather than customer-controlled path text, and defers object-URL revocation until the next task after initiating the download so browsers with deferred navigation can consume the Blob. If local download activation throws, the exception is contained, the existing live status output directs the operator to retry, and the same temporary object URL is still revoked. A service URL containing username, password, query, or fragment components is omitted from the file and remains represented by the `service_url_format` finding. The same shared validator withholds it from preview and readiness, preventing token disclosure and destination-changing rewrites. The file is still customer-controlled sensitive data; operators remain responsible for its storage and transfer. This control is not encryption, persistence, publication, backup, authorization, or mid-transfer cancellation evidence. diff --git a/docs/TRD.md b/docs/TRD.md index 7d4a5c9..b348dfe 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -6,7 +6,7 @@ This TRD covers the pre-release PolicyWeave browser workspace and the contracts ## Current runtime - React + TypeScript + Vite browser application. - Structured authoring state is in browser memory; no production database or backend exists. -- Local JSON export projects the current draft through `createPolicyExport` into deterministic `schema_version = 1` data with normalized facts, explicit `incomplete`/`review_ready` state, and readiness finding codes. Invalid credential-bearing service URLs export as `null`, accepted service URLs are reduced to origin/path without query or fragment data, and unresolved collection modes export as `null`; download uses a browser Blob/object URL, defers URL revocation until the next task so deferred browser navigation can consume it, contains activation exceptions with a retry message in the existing live status output, and performs no network request. +- Local JSON export projects the current draft through `createPolicyExport` into deterministic `schema_version = 1` data with normalized facts, explicit `incomplete`/`review_ready` state, and readiness finding codes. Service URLs containing credentials, query, or fragment components export as `null` and remain blocking rather than being rewritten to another destination; unresolved collection modes export as `null`. Download uses a browser Blob/object URL, defers URL revocation until the next task so deferred browser navigation can consume it, contains activation exceptions with a retry message in the existing live status output, and performs no network request. - `src/policy.ts` owns deterministic review logic for collection selection/no-collection attestation/mode/purpose/path and the non-collection authoring-completeness findings for service identity, explicit retention status/period, transfer statuses/details, and privacy contact. - `src/App.tsx` provides the seven-step authoring flow, review navigation, explicit collection/retention/transfer-status capture, stale dependent-fact invalidation, and deterministic preview projection. - `src/AuthoringFocusController.tsx` keeps explicit step navigation and review-warning jumps aligned with the newly active step by moving programmatic focus to its heading after the React update and allowing the browser to reveal that target; ordinary form controls and the dedicated preview shortcut are outside this behavior. @@ -19,7 +19,7 @@ This TRD covers the pre-release PolicyWeave browser workspace and the contracts 3. Collection readiness requires either at least one explicitly selected collection item or an explicit no-collection attestation. The no-collection attestation and selected items are mutually exclusive; contradictory state fails closed. Turning the attestation on clears selected items and their mode/purpose/path evidence, and later turning it off does not revive those stale facts. 4. Every selected collection item requires an explicit collection mode, nonblank processing purpose, and nonblank collection-path evidence. Each missing responsibility is counted independently and navigates to its owning step. 5. Retention is an independent operator fact, not a consequence of collection state. The retention step uses explicit unresolved/`applies`/`none` status. Unresolved blocks readiness. `applies` requires a nonblank retention period or end condition; `none` requires no period. Changing away from `applies` clears the previous period so stale retention evidence cannot revive. No-collection never auto-selects `none`. -6. Service name, service URL, third-party provision status, international-transfer status, privacy-contact owner, and privacy-contact email are product-defined readiness facts and block readiness while unresolved. Service URL must be an absolute HTTP(S) URL; contact email must satisfy a minimal address-shape check. These syntax checks do not claim endpoint reachability or mailbox ownership. +6. Service name, service URL, third-party provision status, international-transfer status, privacy-contact owner, and privacy-contact email are product-defined readiness facts and block readiness while unresolved. Service URL must be an absolute HTTP(S) URL without credentials, query, or fragment components so preview and export preserve one destination; contact email must satisfy a minimal address-shape check. These syntax checks do not claim endpoint reachability or mailbox ownership. 7. Third-party provision and international transfer use explicit unresolved/yes/no status. `no` is an operator attestation; `yes` requires its dependent facts. Changing either status away from `yes` clears dependent details to prevent stale evidence revival. 8. Blank/whitespace authoring facts are normalized as unresolved where that fact is required by the explicit governing status. 9. Disabling a collection item invalidates dependent collection-mode, processing-purpose, and collection-path evidence; re-enabling requires renewed confirmation. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index d08ecd3..e24ea7e 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -50,7 +50,7 @@ Queue RCA then observed distinct stale heads `cbfbdae4a4bb72433d7bdcc51afdbd8a29 The restart/restore slice remains bounded CI evidence and does not enable a hosted adapter. After the initial atomic-seed repair, review found that paired collection/applies and no-collection/none fixtures could not prove collection and retention are independent, the restored database re-exercised only the no-collection trigger, and a nullable restored `service_name` could evade `<>` through SQL three-valued logic. Test-only head `5e54834873e125b3e3ce4f599e4037e017330638` added the missing cross-state and NULL-safe assertions; exact-head CI `34204279846` was RED only in the restore step with `restart did not preserve independent collection and retention facts`. The next commit seeds a valid collecting revision with `retention_status = none`, keeps authored service and collection-item assertions NULL-safe, and executes status-side missing-rule plus rule-side unexpected-rule transactions against the restored database. Pre-documentation head `202e69d95c94e4432365d6599016a371c0f2cbc3` CI `34204464388` then passed the complete suite. A later exact-head review found that the nullable authored service URL was not selected or asserted even though the evidence claim covered complete service values. Mutation-probe head `aaef3b5489493669cdb53c08a72b6a109fc0b687` deliberately nulled that URL after restart; CI `34205653966` passed every preceding step and failed only the new NULL-safe restore assertion. Commit `57732c6dbec872ad29e97a7f22096dbba9613e9a` removes the probe while retaining literal name/URL checks. These immutable runs establish the TDD transitions but are not substitutes for the final current-head verdict. This is CI durability evidence, not operational backup, tenant authorization, or a released datastore. -The local-draft portability slice converts the previously disabled JSON affordance into a deterministic versioned export. Test-only head `553c1a62514286b0a2e57621072a192556594314` produced exact RED CI `34210028140`: the four new contracts failed while 62 predecessor tests passed. The minimal projection normalizes authored values, keeps unresolved states explicit, exports readiness finding codes, omits credential-bearing service URLs, and downloads through a fixed-name browser Blob without network transfer. This is a mutable draft artifact, not `publication_revision`, durable persistence, backup, authorization, or legal approval. Exact-head review then found two portability defects: immediate object-URL revocation could race deferred WebKit navigation, and an unresolved collection mode leaked the UI sentinel `""` into schema v1. Test-only head `e3861409e3d5f786a1a6756178db71912d1edec0` produced RED CI `34212535756` with precisely 65 passed/2 failed. The minimal repair defers revocation to the next task and narrows the exported mode to the selected enum or `null`; implementation head `eaf18cb4115c1419263b132b5693325e2be42a8d` passed lint, 67/67 tests, build, and all PostgreSQL evidence before final documentation sealing. A later CodeRabbit review found that direct `Object.defineProperty` URL mocks survived `restoreAllMocks()` and that accepted service URLs could copy query/fragment secrets into JSON. Test-first head `0f1a608428681aebfc9187f3d867c59f9905171b` added both regression contracts, but CI `34213620870` was cancelled before execution when the concurrent writer advanced the same branch, so it is not claimed as RED evidence. Concurrent child `5c803b1d2369d4149566b1bf30b83248a46c74c1` preserves that test head and canonicalizes accepted URLs to credential-free origin/path; `90a3d6810ab213af7fb332df0e88f3475109488e` replaces the URL overrides with restorable Vitest spies. +The local-draft portability slice converts the previously disabled JSON affordance into a deterministic versioned export. Test-only head `553c1a62514286b0a2e57621072a192556594314` produced exact RED CI `34210028140`: the four new contracts failed while 62 predecessor tests passed. The minimal projection normalizes authored values, keeps unresolved states explicit, exports readiness finding codes, omits credential-bearing service URLs, and downloads through a fixed-name browser Blob without network transfer. This is a mutable draft artifact, not `publication_revision`, durable persistence, backup, authorization, or legal approval. Exact-head review then found two portability defects: immediate object-URL revocation could race deferred WebKit navigation, and an unresolved collection mode leaked the UI sentinel `""` into schema v1. Test-only head `e3861409e3d5f786a1a6756178db71912d1edec0` produced RED CI `34212535756` with precisely 65 passed/2 failed. The minimal repair defers revocation to the next task and narrows the exported mode to the selected enum or `null`; implementation head `eaf18cb4115c1419263b132b5693325e2be42a8d` passed lint, 67/67 tests, build, and all PostgreSQL evidence before final documentation sealing. A later CodeRabbit review found that direct `Object.defineProperty` URL mocks survived `restoreAllMocks()` and that accepted service URLs could copy query/fragment secrets into JSON. Test-first head `0f1a608428681aebfc9187f3d867c59f9905171b` added both regression contracts, but CI `34213620870` was cancelled before execution when the concurrent writer advanced the same branch, so it is not claimed as RED evidence. Concurrent child `5c803b1d2369d4149566b1bf30b83248a46c74c1` preserves that test head and initially canonicalized accepted URLs to credential-free origin/path; `90a3d6810ab213af7fb332df0e88f3475109488e` replaces the URL overrides with restorable Vitest spies. The later P1 correction below supersedes that lossy canonicalization. Design-assurance then identified that the local download itself had only jsdom/mocked-anchor evidence. Test-only head `7be6d366540ae87ee2504dba0020bf9d31ab811c` adds a Playwright contract that consumes the real Chromium download event in desktop, tablet, and mobile projects, checks the fixed filename, reads and parses the downloaded file, and verifies schema version, incomplete state, authored service name, explicit null, and finding codes. Immutable test-head run `34215521539` passed 69/69 Vitest contracts, the TypeScript/Vite build, PostgreSQL 18.6 migration/concurrency/restart/custom restore, and 16 Playwright/axe cases with two scoped reflow skips; artifact `10051741653` is bound by digest `sha256:9210de5928ff1ab16a0862c8f988b872517af9f28368288e074df187aa04a615`. No production change was needed; this closes the browser-download evidence gap without claiming import, publication, or hosted persistence. @@ -58,12 +58,14 @@ The next exact-head design review correctly limited that evidence to mouse activ The activation-failure pass then found that a browser exception from the generated download link still revoked its object URL but escaped as an unhandled page error and left the operator without a next action. Test-only head `bc4c7f1e16cf5cb2ae8606efe81d8201bbd578de` produced exact RED CI `34223146923`: 69/69 Vitest and every PostgreSQL step passed, while the new desktop Chromium case received an empty live output instead of the required retry guidance. Minimal implementation head `d8117e2a9a52a7c42941252d77e2327b18d903a2` catches only activation exceptions, reuses the existing live status output, and preserves deferred cleanup in `finally`. CI `34223403382` passed 69/69 Vitest, the production build, all PostgreSQL evidence, and 22 Playwright/axe cases with eight intentional project-scope skips. Artifact `10054717353` is bound by digest `sha256:9e58d8a112d4228128794cc35bcb9600e1715c1efb491d0ec9d382d2d2598a3d`. In-progress transfer cancellation and versioned ko/en/ja/zh/vi/es/de/fr resources remain open rather than being inferred from activation-error recovery. +The subsequent Codex P1 review found that the validator and preview accepted query-dependent or hash-routed service URLs while export silently removed those components, allowing a `review_ready` artifact to name a different destination from the authored source fact. Local test-first execution failed both export and preview contracts against sealed predecessor `f7c3ee276e53a39177e49e6d73cb9958f46a386f`: export returned the shortened path and preview rendered the original URL. Concurrent writer `3f16e062e5d68dd9ec4b839514ddc2aa396a4358` supplied the shared root fix without rewriting history; canonical successor `c2f4887080d572d6c16347395b306905f18e6846` carries export and UI regressions proving query/fragment URLs remain `service_url_format` blockers, are withheld from preview, and export as `null` rather than being rewritten. CI `34224552580` passed 70/70 Vitest, the production build, all PostgreSQL evidence, and 22 Playwright/axe cases with eight intentional project-scope skips; artifact `10055164721` is bound by digest `sha256:2168ac2ff07225a553ceb2f893a0f9dc5e1fe18eba39277583f720c9ba34ab7b`. This intentionally narrows admissible v1 URLs instead of manufacturing a different customer fact. + ## Current baseline | Area | Evidence | Status | Commercialization gap | Owner/action | Next verification | | --- | --- | --- | --- | --- | --- | | Guided authoring | PRD, ADR-0002, seven routed editors, first-responsibility startup, `getReview`, explicit retention `getDraftReview`, no-collection/transfer states | Repaired foundation | Fresh state no longer skips or falsely completes step 1; collection, retention, transfer and other responsibilities fail closed independently; collection-path evidence remains unstructured free text and legal sufficiency is deliberately separate | Policy Fact Authoring: preserve deterministic completeness and independent authority; add structured path-evidence types only when a real integration/use case proves the need | Exact-head unit/UI edge tests, then browser E2E | -| Draft portability | Versioned `createPolicyExport`, fixed-name browser Blob download, explicit readiness state/finding codes, credential-URL omission, keyboard/touch activation, JSON MIME, repeat-byte checks, activation-error guidance, and success/error URL cleanup | Implemented foundation; bounded exact-head browser download GREEN | No import/migration contract, in-progress cancellation evidence, localized resource contract, or immutable publication semantics; exported files remain operator-controlled sensitive data | Policy Fact Authoring: preserve deterministic schema-versioned projection without network transfer or inferred facts | Future version compatibility, cancellation, locale, and fail-closed import tests before schema evolution | +| Draft portability | Versioned `createPolicyExport`, fixed-name browser Blob download, explicit readiness state/finding codes, unsafe-URL rejection across preview/readiness/export, keyboard/touch activation, JSON MIME, repeat-byte checks, activation-error guidance, and success/error URL cleanup | Implemented foundation; bounded exact-head browser download GREEN | No import/migration contract, in-progress cancellation evidence, localized resource contract, or immutable publication semantics; exported files remain operator-controlled sensitive data | Policy Fact Authoring: preserve deterministic schema-versioned projection without network transfer, lossy URL rewriting, or inferred facts | Future version compatibility, cancellation, locale, and fail-closed import tests before schema evolution | | Customer-fact authority | Zero-inferred startup facts; truthful initial rail state; explicit no-collection; independent explicit retention status; explicit transfer states; stale dependent-fact invalidation | Repaired | No known buyer-facing authority dead end remains in the in-memory seven-step fact model; hosted persistence must preserve these independent states without deriving one from another | Policy Fact Authoring: encode collection and retention as separate revision-owned facts; no automatic no-collection→no-retention rule | Persistence/schema invariant tests and exact-head UI tests | | Review workspace | Live preview, total blocker count, warning-to-owner navigation, buyer-facing readiness guidance, deterministic step-heading focus transfer, Playwright viewport/screenshot harness | Implemented foundation; offscreen focus repaired and exact-head browser verified | Automated desktop/tablet/mobile focus-scroll evidence is GREEN; broader interaction coverage remains bounded | UX: retain exact-head artifacts, then extend interaction coverage | Exact-head screenshots, keyboard/focus and accessibility checks | | Accessibility | Semantic controls, visible focus behavior, focus-token >=3:1 regression, muted-text >=4.5:1 authored-surface regression, jsdom focus transition, axe/browser, focused-heading viewport checks, responsive retention transitions, and effective 200% browser-zoom reflow | Partial; bounded exact-head browser GREEN | Native browser UI zoom automation, screen-reader, and manual WCAG evidence remain absent | UX/Test Engineering: add a manual interaction record and remaining cases without claiming conformance from automation alone | Exact-head WCAG/browser matrix plus screen-reader and manual evidence | @@ -71,7 +73,7 @@ The activation-failure pass then found that a browser exception from the generat | Policy model | ADRs, ARCHITECTURE, TRD, ADR-0003, Proposed ERD, up/down migration, schema, runtime, two-session concurrency, restart, and dump/restore contract tests | Proposed 3NF foundation; PostgreSQL 18 exact-head CI execution required; browser runtime remains memory-only | Apply/down/apply, exact negative errors, observed lock waits, conflicting-fact rejection, same-item UPSERT convergence with NULL-safe complete label/mode/path assertions, process restart, and custom-format restore with a collection/no-retention cross-state, NULL-safe complete service name/URL and item assertions, and restored no-collection plus both retention contradiction checks are implemented; authorization, audit, encryption, deletion, and production-scale contention remain unproved | Platform: retain exact-head PostgreSQL evidence, then add the hosted authorization/audit boundary while keeping the adapter disabled | Tenant authorization, immutable audit, and encryption tests | | Publication | Readiness CTA truthfully does not pretend to publish; immutable `publication_revision` is designed | Planned | No authenticated approval, immutable publication, supersession, rollback, or public URL lifecycle | Review & Publication: implement after persistence/security entry criteria | Authorization, replay/digest, supersession tests | | Security/privacy | `docs/SECURITY.md`; local-first runtime; SHA-pinned checkout | Baseline documented | Hosted tenant model, encryption/key handling, audit/incident/retention evidence absent | Platform/Security: threat-model hosted boundary before backend | Exact-head security tests and org scans | -| Tests | 69 unit/UI/schema/workflow regressions plus PostgreSQL runtime/concurrency/restore scripts, 22 Playwright/axe passes, eight intentional project-scope skips, real-browser interaction/download/error contracts, and a screenshot artifact contract | Improved; bounded exact-head unit/build/browser/PostgreSQL GREEN | Repository-wide 100% execution/docstring coverage is not yet evidenced | Test Engineering: measure coverage and extend realistic edge cases | Exact-head coverage + browser/database evidence | +| Tests | 70 unit/UI/schema/workflow regressions plus PostgreSQL runtime/concurrency/restore scripts, 22 Playwright/axe passes, eight intentional project-scope skips, real-browser interaction/download/error contracts, and a screenshot artifact contract | Improved; bounded exact-head unit/build/browser/PostgreSQL GREEN | Repository-wide 100% execution/docstring coverage is not yet evidenced | Test Engineering: measure coverage and extend realistic edge cases | Exact-head coverage + browser/database evidence | | Performance | Static Vite client | Unevidenced | No realistic buyer-flow browser performance baseline; no network backend exists for meaningful k6 endpoint evidence | Operability: record browser baseline now; add k6 only when hosted network surfaces exist | Real measurements before latency claims | | CI/security merge gate | Repo CI plus active organization ruleset-required workflows; immutable Node 24-based checkout, setup/cache, and artifact-upload action releases | Live external gate; warning-free evidence is re-fetched for the exact merge candidate | Every branch movement invalidates predecessor evidence and stale approval; current hosted jobs may remain queued before runner assignment and dependency/reviewer control-plane failures can fail closed independently | Re-fetch exact-head workflows/reviews; use the central owner path for runner/dependency-review incidents rather than leaf-side churn or bypass | Terminal exact-head checks with no action-runtime deprecation warnings + independent approval + resolved threads | From 170a65830fddcdc688ef53cdc793a84dbc4d0025 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 21:17:50 +0900 Subject: [PATCH 31/45] test: preserve complete service URL rejection matrix --- src/policy-export.test.ts | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/policy-export.test.ts b/src/policy-export.test.ts index 6eab374..c138e23 100644 --- a/src/policy-export.test.ts +++ b/src/policy-export.test.ts @@ -90,15 +90,23 @@ describe('policy JSON export', () => { }) it('rejects query and fragment service URLs instead of rewriting the authored destination', () => { - const exported = createPolicyExport(initialItems, false, { + for (const serviceUrl of [ + 'https://example.test/app?tenant=acme', + 'https://example.test/#/privacy', + 'https://example.test/privacy?access_token=query-secret#fragment-secret', + ]) { + const exported = createPolicyExport(initialItems, false, { ...initialFacts, serviceUrl }) + + expect(exported.policy_facts.service_profile.service_url).toBeNull() + expect(exported.review_finding_codes).toContain('service_url_format') + } + + const secretExport = createPolicyExport(initialItems, false, { ...initialFacts, serviceUrl: 'https://example.test/privacy?access_token=query-secret#fragment-secret', }) - - expect(exported.policy_facts.service_profile.service_url).toBeNull() - expect(exported.review_finding_codes).toContain('service_url_format') - expect(JSON.stringify(exported)).not.toContain('query-secret') - expect(JSON.stringify(exported)).not.toContain('fragment-secret') + expect(JSON.stringify(secretExport)).not.toContain('query-secret') + expect(JSON.stringify(secretExport)).not.toContain('fragment-secret') }) it('does not export credentials embedded in an invalid service URL', () => { From bc539d75e4e26c301bd24b949cd22fa2a1417289 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 21:20:40 +0900 Subject: [PATCH 32/45] docs: preserve service URL rejection lineage --- docs/product-technical-gap-baseline.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index e24ea7e..65cd3c5 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -58,7 +58,7 @@ The next exact-head design review correctly limited that evidence to mouse activ The activation-failure pass then found that a browser exception from the generated download link still revoked its object URL but escaped as an unhandled page error and left the operator without a next action. Test-only head `bc4c7f1e16cf5cb2ae8606efe81d8201bbd578de` produced exact RED CI `34223146923`: 69/69 Vitest and every PostgreSQL step passed, while the new desktop Chromium case received an empty live output instead of the required retry guidance. Minimal implementation head `d8117e2a9a52a7c42941252d77e2327b18d903a2` catches only activation exceptions, reuses the existing live status output, and preserves deferred cleanup in `finally`. CI `34223403382` passed 69/69 Vitest, the production build, all PostgreSQL evidence, and 22 Playwright/axe cases with eight intentional project-scope skips. Artifact `10054717353` is bound by digest `sha256:9e58d8a112d4228128794cc35bcb9600e1715c1efb491d0ec9d382d2d2598a3d`. In-progress transfer cancellation and versioned ko/en/ja/zh/vi/es/de/fr resources remain open rather than being inferred from activation-error recovery. -The subsequent Codex P1 review found that the validator and preview accepted query-dependent or hash-routed service URLs while export silently removed those components, allowing a `review_ready` artifact to name a different destination from the authored source fact. Local test-first execution failed both export and preview contracts against sealed predecessor `f7c3ee276e53a39177e49e6d73cb9958f46a386f`: export returned the shortened path and preview rendered the original URL. Concurrent writer `3f16e062e5d68dd9ec4b839514ddc2aa396a4358` supplied the shared root fix without rewriting history; canonical successor `c2f4887080d572d6c16347395b306905f18e6846` carries export and UI regressions proving query/fragment URLs remain `service_url_format` blockers, are withheld from preview, and export as `null` rather than being rewritten. CI `34224552580` passed 70/70 Vitest, the production build, all PostgreSQL evidence, and 22 Playwright/axe cases with eight intentional project-scope skips; artifact `10055164721` is bound by digest `sha256:2168ac2ff07225a553ceb2f893a0f9dc5e1fe18eba39277583f720c9ba34ab7b`. This intentionally narrows admissible v1 URLs instead of manufacturing a different customer fact. +The subsequent Codex P1 review found that the validator and preview accepted query-dependent or hash-routed service URLs while export silently removed those components, allowing a `review_ready` artifact to name a different destination from the authored source fact. Test-only head `01ab876a228d8cf9caadce2a4760ca3a1ffeedb7` covered a normal query-dependent URL, a hash-routed URL, and a secret-bearing combination; exact RED CI `34224341164` failed that contract before concurrent writer `3f16e062e5d68dd9ec4b839514ddc2aa396a4358` supplied the shared root fix without rewriting history. Successor `c2f4887080d572d6c16347395b306905f18e6846` added the missing buyer-facing preview regression, but its first integration accidentally narrowed the export matrix to the secret-bearing combination. Canonical child `170a65830fddcdc688ef53cdc793a84dbc4d0025` restores every valid matrix case while retaining the preview contract: query/fragment URLs remain `service_url_format` blockers, are withheld from preview, and export as `null` rather than being rewritten. CI `34225294729` passed 70/70 Vitest, the production build, all PostgreSQL evidence, and 22 Playwright/axe cases with eight intentional project-scope skips; artifact `10055464028` is bound by digest `sha256:90e30f973dcf490e226412039c8b3e43d50f0655dd7f5dc901f8d8f93100d845`. This intentionally narrows admissible v1 URLs instead of manufacturing a different customer fact. ## Current baseline From 0580f466837bc5b000a54e79732b08bf83a3fd2f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 22:07:19 +0900 Subject: [PATCH 33/45] test: reject empty service URL delimiters --- src/policy-export.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/policy-export.test.ts b/src/policy-export.test.ts index c138e23..0ec0487 100644 --- a/src/policy-export.test.ts +++ b/src/policy-export.test.ts @@ -93,6 +93,8 @@ describe('policy JSON export', () => { for (const serviceUrl of [ 'https://example.test/app?tenant=acme', 'https://example.test/#/privacy', + 'https://example.test/privacy?', + 'https://example.test/privacy#', 'https://example.test/privacy?access_token=query-secret#fragment-secret', ]) { const exported = createPolicyExport(initialItems, false, { ...initialFacts, serviceUrl }) From 9d92a9ea716c5427161450df34ef142caaaf64e6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 22:09:47 +0900 Subject: [PATCH 34/45] fix: reject empty service URL delimiters --- src/policy.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/policy.ts b/src/policy.ts index 7bcd8d6..e6f6574 100644 --- a/src/policy.ts +++ b/src/policy.ts @@ -67,8 +67,9 @@ function normalizeWebServiceUrl(value: string): string | null { try { const url = new URL(value) if ((url.protocol !== 'https:' && url.protocol !== 'http:') || !url.hostname || url.username || url.password) return null - if (url.search || url.hash) return null - return url.toString() + const normalized = url.toString() + if (normalized.includes('?') || normalized.includes('#')) return null + return normalized } catch { return null } From 60321021b3d259674925a90243a6310ba0ba3eb1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 22:19:47 +0900 Subject: [PATCH 35/45] docs: record empty URL delimiter repair evidence --- docs/product-technical-gap-baseline.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 65cd3c5..60f55d9 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -60,6 +60,8 @@ The activation-failure pass then found that a browser exception from the generat The subsequent Codex P1 review found that the validator and preview accepted query-dependent or hash-routed service URLs while export silently removed those components, allowing a `review_ready` artifact to name a different destination from the authored source fact. Test-only head `01ab876a228d8cf9caadce2a4760ca3a1ffeedb7` covered a normal query-dependent URL, a hash-routed URL, and a secret-bearing combination; exact RED CI `34224341164` failed that contract before concurrent writer `3f16e062e5d68dd9ec4b839514ddc2aa396a4358` supplied the shared root fix without rewriting history. Successor `c2f4887080d572d6c16347395b306905f18e6846` added the missing buyer-facing preview regression, but its first integration accidentally narrowed the export matrix to the secret-bearing combination. Canonical child `170a65830fddcdc688ef53cdc793a84dbc4d0025` restores every valid matrix case while retaining the preview contract: query/fragment URLs remain `service_url_format` blockers, are withheld from preview, and export as `null` rather than being rewritten. CI `34225294729` passed 70/70 Vitest, the production build, all PostgreSQL evidence, and 22 Playwright/axe cases with eight intentional project-scope skips; artifact `10055464028` is bound by digest `sha256:90e30f973dcf490e226412039c8b3e43d50f0655dd7f5dc901f8d8f93100d845`. This intentionally narrows admissible v1 URLs instead of manufacturing a different customer fact. +The final URL-boundary follow-up found that WHATWG `URL.search` and `URL.hash` are empty for authored bare delimiters even though serialization retains `?` or `#`. Test-first head `0580f466837bc5b000a54e79732b08bf83a3fd2f` added both empty-delimiter cases to the existing query/hash/secret matrix; CI `34230043117` passed lint and failed in `npm test` before the source repair. Minimal child `9d92a9ea716c5427161450df34ef142caaaf64e6` checks the canonical serialization for actual delimiters, preserving encoded `%3F`/`%23` pathname data while rejecting bare query/fragment markers through the one readiness/preview/export boundary. Exact source-fix CI `34230291396` passed 70/70 Vitest, build, PostgreSQL migration/concurrency/restart/restore, and 22 Playwright/axe cases with eight intentional project-scope skips; artifact `10057683181` is bound by digest `sha256:6d2e7fc1ddafefdbd55e9c13b716dca123ba148dc753d65efb26d1bb5e3c26f4`. This closes the reviewed delimiter bypass without widening URL parsing or adding another validator. + ## Current baseline | Area | Evidence | Status | Commercialization gap | Owner/action | Next verification | From a6cf635fcfc082fafacff13b658108fd076fd474 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 22:22:17 +0900 Subject: [PATCH 36/45] test: preserve encoded service URL path data --- src/policy-export.test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/policy-export.test.ts b/src/policy-export.test.ts index 0ec0487..a2b9f5b 100644 --- a/src/policy-export.test.ts +++ b/src/policy-export.test.ts @@ -111,6 +111,16 @@ describe('policy JSON export', () => { expect(JSON.stringify(secretExport)).not.toContain('fragment-secret') }) + it('preserves encoded question marks and hashes as pathname data', () => { + const exported = createPolicyExport(initialItems, false, { + ...initialFacts, + serviceUrl: 'https://example.test/privacy%3Fpolicy%23section', + }) + + expect(exported.policy_facts.service_profile.service_url).toBe('https://example.test/privacy%3Fpolicy%23section') + expect(exported.review_finding_codes).not.toContain('service_url_format') + }) + it('does not export credentials embedded in an invalid service URL', () => { const exported = createPolicyExport(initialItems, false, { ...initialFacts, From a7cac949509be5d4adfa5962f8440b1628132e60 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 22:22:40 +0900 Subject: [PATCH 37/45] docs: bind encoded URL path regression --- docs/product-technical-gap-baseline.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 60f55d9..e01b068 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -60,7 +60,7 @@ The activation-failure pass then found that a browser exception from the generat The subsequent Codex P1 review found that the validator and preview accepted query-dependent or hash-routed service URLs while export silently removed those components, allowing a `review_ready` artifact to name a different destination from the authored source fact. Test-only head `01ab876a228d8cf9caadce2a4760ca3a1ffeedb7` covered a normal query-dependent URL, a hash-routed URL, and a secret-bearing combination; exact RED CI `34224341164` failed that contract before concurrent writer `3f16e062e5d68dd9ec4b839514ddc2aa396a4358` supplied the shared root fix without rewriting history. Successor `c2f4887080d572d6c16347395b306905f18e6846` added the missing buyer-facing preview regression, but its first integration accidentally narrowed the export matrix to the secret-bearing combination. Canonical child `170a65830fddcdc688ef53cdc793a84dbc4d0025` restores every valid matrix case while retaining the preview contract: query/fragment URLs remain `service_url_format` blockers, are withheld from preview, and export as `null` rather than being rewritten. CI `34225294729` passed 70/70 Vitest, the production build, all PostgreSQL evidence, and 22 Playwright/axe cases with eight intentional project-scope skips; artifact `10055464028` is bound by digest `sha256:90e30f973dcf490e226412039c8b3e43d50f0655dd7f5dc901f8d8f93100d845`. This intentionally narrows admissible v1 URLs instead of manufacturing a different customer fact. -The final URL-boundary follow-up found that WHATWG `URL.search` and `URL.hash` are empty for authored bare delimiters even though serialization retains `?` or `#`. Test-first head `0580f466837bc5b000a54e79732b08bf83a3fd2f` added both empty-delimiter cases to the existing query/hash/secret matrix; CI `34230043117` passed lint and failed in `npm test` before the source repair. Minimal child `9d92a9ea716c5427161450df34ef142caaaf64e6` checks the canonical serialization for actual delimiters, preserving encoded `%3F`/`%23` pathname data while rejecting bare query/fragment markers through the one readiness/preview/export boundary. Exact source-fix CI `34230291396` passed 70/70 Vitest, build, PostgreSQL migration/concurrency/restart/restore, and 22 Playwright/axe cases with eight intentional project-scope skips; artifact `10057683181` is bound by digest `sha256:6d2e7fc1ddafefdbd55e9c13b716dca123ba148dc753d65efb26d1bb5e3c26f4`. This closes the reviewed delimiter bypass without widening URL parsing or adding another validator. +The final URL-boundary follow-up found that WHATWG `URL.search` and `URL.hash` are empty for authored bare delimiters even though serialization retains `?` or `#`. Test-first head `0580f466837bc5b000a54e79732b08bf83a3fd2f` added both empty-delimiter cases to the existing query/hash/secret matrix; CI `34230043117` passed lint and failed in `npm test` before the source repair. Minimal child `9d92a9ea716c5427161450df34ef142caaaf64e6` checks the canonical serialization for actual delimiters, rejecting bare query/fragment markers through the one readiness/preview/export boundary. Positive edge coverage at `a6cf635fcfc082fafacff13b658108fd076fd474` separately proves encoded `%3F`/`%23` pathname data remains admissible and byte-preserved, preventing a later raw-input substring check from over-blocking legitimate path data. Exact source-fix CI `34230291396` passed 70/70 Vitest, build, PostgreSQL migration/concurrency/restart/restore, and 22 Playwright/axe cases with eight intentional project-scope skips; artifact `10057683181` is bound by digest `sha256:6d2e7fc1ddafefdbd55e9c13b716dca123ba148dc753d65efb26d1bb5e3c26f4`. This closes the reviewed delimiter bypass without widening URL parsing or adding another validator. ## Current baseline @@ -75,7 +75,7 @@ The final URL-boundary follow-up found that WHATWG `URL.search` and `URL.hash` a | Policy model | ADRs, ARCHITECTURE, TRD, ADR-0003, Proposed ERD, up/down migration, schema, runtime, two-session concurrency, restart, and dump/restore contract tests | Proposed 3NF foundation; PostgreSQL 18 exact-head CI execution required; browser runtime remains memory-only | Apply/down/apply, exact negative errors, observed lock waits, conflicting-fact rejection, same-item UPSERT convergence with NULL-safe complete label/mode/path assertions, process restart, and custom-format restore with a collection/no-retention cross-state, NULL-safe complete service name/URL and item assertions, and restored no-collection plus both retention contradiction checks are implemented; authorization, audit, encryption, deletion, and production-scale contention remain unproved | Platform: retain exact-head PostgreSQL evidence, then add the hosted authorization/audit boundary while keeping the adapter disabled | Tenant authorization, immutable audit, and encryption tests | | Publication | Readiness CTA truthfully does not pretend to publish; immutable `publication_revision` is designed | Planned | No authenticated approval, immutable publication, supersession, rollback, or public URL lifecycle | Review & Publication: implement after persistence/security entry criteria | Authorization, replay/digest, supersession tests | | Security/privacy | `docs/SECURITY.md`; local-first runtime; SHA-pinned checkout | Baseline documented | Hosted tenant model, encryption/key handling, audit/incident/retention evidence absent | Platform/Security: threat-model hosted boundary before backend | Exact-head security tests and org scans | -| Tests | 70 unit/UI/schema/workflow regressions plus PostgreSQL runtime/concurrency/restore scripts, 22 Playwright/axe passes, eight intentional project-scope skips, real-browser interaction/download/error contracts, and a screenshot artifact contract | Improved; bounded exact-head unit/build/browser/PostgreSQL GREEN | Repository-wide 100% execution/docstring coverage is not yet evidenced | Test Engineering: measure coverage and extend realistic edge cases | Exact-head coverage + browser/database evidence | +| Tests | 71 unit/UI/schema/workflow regressions plus PostgreSQL runtime/concurrency/restore scripts, 22 Playwright/axe passes, eight intentional project-scope skips, real-browser interaction/download/error contracts, and a screenshot artifact contract | Improved; bounded exact-head unit/build/browser/PostgreSQL GREEN | Repository-wide 100% execution/docstring coverage is not yet evidenced | Test Engineering: measure coverage and extend realistic edge cases | Exact-head coverage + browser/database evidence | | Performance | Static Vite client | Unevidenced | No realistic buyer-flow browser performance baseline; no network backend exists for meaningful k6 endpoint evidence | Operability: record browser baseline now; add k6 only when hosted network surfaces exist | Real measurements before latency claims | | CI/security merge gate | Repo CI plus active organization ruleset-required workflows; immutable Node 24-based checkout, setup/cache, and artifact-upload action releases | Live external gate; warning-free evidence is re-fetched for the exact merge candidate | Every branch movement invalidates predecessor evidence and stale approval; current hosted jobs may remain queued before runner assignment and dependency/reviewer control-plane failures can fail closed independently | Re-fetch exact-head workflows/reviews; use the central owner path for runner/dependency-review incidents rather than leaf-side churn or bypass | Terminal exact-head checks with no action-runtime deprecation warnings + independent approval + resolved threads | From 83f189ebf78aab9124745bc4209a281cf6f11fa0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 22:29:08 +0900 Subject: [PATCH 38/45] test: expose pre-activation export failure --- tests/e2e/authoring.spec.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/e2e/authoring.spec.ts b/tests/e2e/authoring.spec.ts index 8768f3e..891dd70 100644 --- a/tests/e2e/authoring.spec.ts +++ b/tests/e2e/authoring.spec.ts @@ -221,6 +221,23 @@ test('reports a download activation failure and revokes its JSON object URL', as expect(pageErrors).toEqual([]) }) +test('reports an object URL creation failure without leaking a page error', async ({ page }, testInfo) => { + test.skip(testInfo.project.name !== 'desktop-chromium', 'One browser profile proves the pre-activation error boundary.') + + await page.addInitScript(() => { + URL.createObjectURL = () => { + throw new Error('simulated object URL creation failure') + } + }) + await page.goto('/') + + const pageErrors: Error[] = [] + page.on('pageerror', (error) => pageErrors.push(error)) + await page.getByRole('button', { name: /JSON 내보내기/ }).click() + await expect(page.locator('output')).toHaveText('JSON 파일을 내보내지 못했습니다. 다시 시도하세요.') + expect(pageErrors).toEqual([]) +}) + test('exports a review-ready no-collection draft without unresolved findings', async ({ page }, testInfo) => { test.skip(testInfo.project.name !== 'desktop-chromium', 'One complete export proves state semantics; layout coverage is exercised separately.') From 8d637f96ad22874645c25b4834b2ce37ae575ddd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 22:31:53 +0900 Subject: [PATCH 39/45] fix: contain export preparation failures --- src/App.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 5bdfcfc..0a904b8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -218,17 +218,21 @@ export default function App() { const [message, setMessage] = useState('') function publish() { setMessage(blockingCount ? '필수 확인 항목을 먼저 입력하세요.' : '필수 확인이 완료되었습니다. 현재 검토본을 책임자와 검토하고 필요한 사실을 보완하세요.') } function exportDraft() { - const fileUrl = URL.createObjectURL(new Blob([`${JSON.stringify(createPolicyExport(items, noCollectionAttested, facts), null, 2)}\n`], { type: 'application/json' })) - const downloadLink = document.createElement('a') - downloadLink.href = fileUrl - downloadLink.download = 'policyweave-draft.json' + let fileUrl: string | null = null try { + fileUrl = URL.createObjectURL(new Blob([`${JSON.stringify(createPolicyExport(items, noCollectionAttested, facts), null, 2)}\n`], { type: 'application/json' })) + const downloadLink = document.createElement('a') + downloadLink.href = fileUrl + downloadLink.download = 'policyweave-draft.json' downloadLink.click() setMessage('') } catch { setMessage('JSON 파일을 내보내지 못했습니다. 다시 시도하세요.') } finally { - setTimeout(() => URL.revokeObjectURL(fileUrl), 0) + if (fileUrl) { + const disposableFileUrl = fileUrl + setTimeout(() => URL.revokeObjectURL(disposableFileUrl), 0) + } } } return
From 751e3d57582ec0d8a2d404e4f1ff91c61b025a33 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 22:34:58 +0900 Subject: [PATCH 40/45] docs: record export preparation recovery --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5588e84..e5ecd76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ All notable product changes are recorded here. PolicyWeave is pre-release; entri - Explicit unresolved/yes/no states for third-party provision and international transfer, with dependent detail capture only for confirmed `yes` cases. - Regression coverage for all seven step routes, zero-inferred startup facts, first-responsibility startup state, explicit no-collection state and stale-item invalidation, independent retention authority and stale-period invalidation, collection-mode/path confirmation, seven-step readiness, explicit no-transfer attestations, transfer-dependent fact invalidation, whitespace normalization, service URL projection, warning navigation, collection-path/purpose separation, stale collection evidence invalidation, buyer-facing publication guidance, non-deceptive handling of unshipped affordances, authored focus-indicator contrast, and authoring-step focus transfer. - Product/technical gap ledger, architecture, technical requirements, security baseline, and legal-source/accessibility traceability. -- Playwright/axe browser evidence harness covering desktop, tablet, and mobile rendering; horizontal overflow; keyboard activation and focus transfer; explicit no-collection progression; retention-status transitions and stale-period invalidation; effective 200% browser-zoom reflow from the desktop profile; serious/critical automated accessibility findings; real-browser JSON download events with mouse, keyboard, and touch activation; fixed filename; JSON MIME; byte-stable repeated exports; review-ready payload semantics; success and activation-error object-URL cleanup; and exact-head screenshot artifacts. +- Playwright/axe browser evidence harness covering desktop, tablet, and mobile rendering; horizontal overflow; keyboard activation and focus transfer; explicit no-collection progression; retention-status transitions and stale-period invalidation; effective 200% browser-zoom reflow from the desktop profile; serious/critical automated accessibility findings; real-browser JSON download events with mouse, keyboard, and touch activation; fixed filename; JSON MIME; byte-stable repeated exports; review-ready payload semantics; success and preparation/activation-error object-URL cleanup; and exact-head screenshot artifacts. ### Changed - PostgreSQL negative-path evidence now matches each expected domain error message, so an unrelated SQL or connection failure cannot masquerade as a passing invariant check. @@ -47,7 +47,7 @@ All notable product changes are recorded here. PolicyWeave is pre-release; entri - Step-rail, previous/next, and review-warning navigation now transfers programmatic focus to the newly active step heading; ordinary form controls and the dedicated preview shortcut are excluded from that transfer. - Review-warning navigation now lets the browser scroll the focused owner heading into view; the previous `preventScroll` option could leave that heading hundreds of pixels above the desktop or mobile viewport. - The publication-area CTA describes a readiness check and directs the operator to responsible review rather than exposing internal implementation boundaries. -- JSON export now downloads the current structured draft locally, contains download-activation exceptions, reports a retry action through the existing live status output, and still revokes the temporary object URL; the redundant no-op `검토본 생성` control remains removed, and the document title remains non-interactive status text. +- JSON export now downloads the current structured draft locally, contains download preparation and activation exceptions, reports a retry action through the existing live status output, and revokes the temporary object URL whenever allocation succeeded; the redundant no-op `검토본 생성` control remains removed, and the document title remains non-interactive status text. - Authored generic and custom-checkbox keyboard focus outlines now use the high-contrast `--green` token; a CSS regression test computes and enforces at least 3:1 contrast against white instead of relying on a low-contrast focus color. - Responsive review behavior and mobile publication feedback were repaired during PR review. - Responsive CSS contract tests use literal media-query regular expressions, removing the Semgrep dynamic-RegExp finding without suppressing or weakening the scanner gate. From 6529617e562329c1003b51d07eaa14ea6afa9583 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 22:35:50 +0900 Subject: [PATCH 41/45] docs: define export preparation failure boundary --- docs/SECURITY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/SECURITY.md b/docs/SECURITY.md index 7c1cda0..4c74b3a 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -33,4 +33,4 @@ Security posture is head-specific. A successful predecessor scan, unresolved fin ## Local JSON export -The export path serializes only the current in-memory PolicyWeave draft and deterministic readiness codes into a browser Blob. It makes no network request, uses a fixed filename rather than customer-controlled path text, and defers object-URL revocation until the next task after initiating the download so browsers with deferred navigation can consume the Blob. If local download activation throws, the exception is contained, the existing live status output directs the operator to retry, and the same temporary object URL is still revoked. A service URL containing username, password, query, or fragment components is omitted from the file and remains represented by the `service_url_format` finding. The same shared validator withholds it from preview and readiness, preventing token disclosure and destination-changing rewrites. The file is still customer-controlled sensitive data; operators remain responsible for its storage and transfer. This control is not encryption, persistence, publication, backup, authorization, or mid-transfer cancellation evidence. +The export path serializes only the current in-memory PolicyWeave draft and deterministic readiness codes into a browser Blob. It makes no network request, uses a fixed filename rather than customer-controlled path text, and defers object-URL revocation until the next task after initiating the download so browsers with deferred navigation can consume the Blob. If local download preparation or activation throws, the exception is contained and the existing live status output directs the operator to retry. A temporary object URL is revoked exactly when allocation succeeded; preparation failure before allocation has no fabricated cleanup target. A service URL containing username, password, query, or fragment components is omitted from the file and remains represented by the `service_url_format` finding. The same shared validator withholds it from preview and readiness, preventing token disclosure and destination-changing rewrites. The file is still customer-controlled sensitive data; operators remain responsible for its storage and transfer. This control is not encryption, persistence, publication, backup, authorization, or mid-transfer cancellation evidence. From fd44c84a2b3d232e7b0827cbb4fac0c2ba14546b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 22:35:53 +0900 Subject: [PATCH 42/45] docs: align export error contract --- docs/TRD.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/TRD.md b/docs/TRD.md index b348dfe..a8abba1 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -6,11 +6,11 @@ This TRD covers the pre-release PolicyWeave browser workspace and the contracts ## Current runtime - React + TypeScript + Vite browser application. - Structured authoring state is in browser memory; no production database or backend exists. -- Local JSON export projects the current draft through `createPolicyExport` into deterministic `schema_version = 1` data with normalized facts, explicit `incomplete`/`review_ready` state, and readiness finding codes. Service URLs containing credentials, query, or fragment components export as `null` and remain blocking rather than being rewritten to another destination; unresolved collection modes export as `null`. Download uses a browser Blob/object URL, defers URL revocation until the next task so deferred browser navigation can consume it, contains activation exceptions with a retry message in the existing live status output, and performs no network request. +- Local JSON export projects the current draft through `createPolicyExport` into deterministic `schema_version = 1` data with normalized facts, explicit `incomplete`/`review_ready` state, and readiness finding codes. Service URLs containing credentials, query, or fragment components export as `null` and remain blocking rather than being rewritten to another destination; unresolved collection modes export as `null`. Download uses a browser Blob/object URL, defers URL revocation until the next task so deferred browser navigation can consume it, contains preparation and activation exceptions with a retry message in the existing live status output, and performs no network request. - `src/policy.ts` owns deterministic review logic for collection selection/no-collection attestation/mode/purpose/path and the non-collection authoring-completeness findings for service identity, explicit retention status/period, transfer statuses/details, and privacy contact. - `src/App.tsx` provides the seven-step authoring flow, review navigation, explicit collection/retention/transfer-status capture, stale dependent-fact invalidation, and deterministic preview projection. - `src/AuthoringFocusController.tsx` keeps explicit step navigation and review-warning jumps aligned with the newly active step by moving programmatic focus to its heading after the React update and allowing the browser to reveal that target; ordinary form controls and the dedicated preview shortcut are outside this behavior. -- The current CI contract is lint, Vitest, TypeScript/Vite build, and Playwright Chromium browser evidence plus live organization-required security/review workflows. Browser cases cover desktop/tablet/mobile rendering, keyboard-triggered focus transfer, the explicit no-collection path, retention-status transitions with stale-period invalidation, effective 200% browser-zoom reflow from the desktop layout viewport, horizontal overflow, serious/critical axe findings, real download events with mouse, keyboard, and touch activation, fixed filename, JSON MIME, byte-stable repeated exports, review-ready payload semantics, object-URL cleanup on success and simulated activation failure, failure announcement, and per-project screenshots retained as an exact-head artifact. +- The current CI contract is lint, Vitest, TypeScript/Vite build, and Playwright Chromium browser evidence plus live organization-required security/review workflows. Browser cases cover desktop/tablet/mobile rendering, keyboard-triggered focus transfer, the explicit no-collection path, retention-status transitions with stale-period invalidation, effective 200% browser-zoom reflow from the desktop layout viewport, horizontal overflow, serious/critical axe findings, real download events with mouse, keyboard, and touch activation, fixed filename, JSON MIME, byte-stable repeated exports, review-ready payload semantics, object-URL cleanup on success and simulated activation failure, contained pre-allocation failure, failure announcement, and per-project screenshots retained as an exact-head artifact. - Muted small text uses one authored color token whose contrast is regression-tested against every current surface background at a minimum 4.5:1 ratio; browser axe remains the integration authority for rendered combinations. ## Functional contracts @@ -39,7 +39,7 @@ The separation between collection and retention follows the PIPC Standard Person - Production does not depend on synthetic demo data. ## Local draft portability -The JSON file is a draft portability artifact, not a publication receipt, immutable revision, legal approval, or persistence backup. It may be exported while incomplete so operators can inspect and transfer their authored work without converting blanks into `none`. Contract changes require a new schema version and compatibility evidence; the current fixed filename avoids using customer-controlled text as a filesystem name. Activation-error recovery does not establish cancellation of an in-progress browser transfer. +The JSON file is a draft portability artifact, not a publication receipt, immutable revision, legal approval, or persistence backup. It may be exported while incomplete so operators can inspect and transfer their authored work without converting blanks into `none`. Contract changes require a new schema version and compatibility evidence; the current fixed filename avoids using customer-controlled text as a filesystem name. Preparation/activation-error recovery does not establish cancellation of an in-progress browser transfer. ## Hosted persistence/publication entry criteria Before network persistence lands, define a versioned policy-data schema, migration policy, 3NF relational model, per-item UPSERT/idempotency rules, immutable publication receipt, supersession/rollback semantics, tenant/purpose authorization, audit evidence, encryption/key management, retention/deletion behavior, and backup/restore testing. Use two-or-more-word semantic persistence object names in `snake_case` by default. The revision model must preserve explicit no-collection and explicit retention status independently; `none` must not be materialized from collection absence, and an inapplicable/non-retained state must not carry a live `retention_rule` value. From 9edb24fb0cdd94926687f5c8913dafcf5336aced Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 22:35:57 +0900 Subject: [PATCH 43/45] docs: bind export preparation RED and GREEN --- docs/product-technical-gap-baseline.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index e01b068..b3afb72 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -58,6 +58,8 @@ The next exact-head design review correctly limited that evidence to mouse activ The activation-failure pass then found that a browser exception from the generated download link still revoked its object URL but escaped as an unhandled page error and left the operator without a next action. Test-only head `bc4c7f1e16cf5cb2ae8606efe81d8201bbd578de` produced exact RED CI `34223146923`: 69/69 Vitest and every PostgreSQL step passed, while the new desktop Chromium case received an empty live output instead of the required retry guidance. Minimal implementation head `d8117e2a9a52a7c42941252d77e2327b18d903a2` catches only activation exceptions, reuses the existing live status output, and preserves deferred cleanup in `finally`. CI `34223403382` passed 69/69 Vitest, the production build, all PostgreSQL evidence, and 22 Playwright/axe cases with eight intentional project-scope skips. Artifact `10054717353` is bound by digest `sha256:9e58d8a112d4228128794cc35bcb9600e1715c1efb491d0ec9d382d2d2598a3d`. In-progress transfer cancellation and versioned ko/en/ja/zh/vi/es/de/fr resources remain open rather than being inferred from activation-error recovery. +The preparation-failure pass then found that Blob/object-URL allocation still occurred before the existing error boundary. A browser allocation exception therefore escaped as a page error and left the live status output empty. Test-only head `83f189ebf78aab9124745bc4209a281cf6f11fa0` produced exact RED CI `34232255893`: 71/71 Vitest, build, and all PostgreSQL evidence passed, while the new desktop Chromium scene failed after receiving an empty output; the browser result was one failed, 22 passed, and ten intentional project-scope skips. Minimal implementation head `8d637f96ad22874645c25b4834b2ce37ae575ddd` extends the same `try` boundary across export preparation and activation, reuses the established retry message, and schedules revocation only when allocation returned a URL. CI `34232543645` passed 71/71 Vitest, build, PostgreSQL migration/concurrency/restart/restore, and 23 Playwright/axe cases with ten scoped skips; artifact `10058444165` is bound by digest `sha256:62b957f180d698b6714b46a89c2adcbb17bbd16152e6ae8820ee55eb84818958`. No retry loop, secondary error channel, or fabricated cleanup target was added. + The subsequent Codex P1 review found that the validator and preview accepted query-dependent or hash-routed service URLs while export silently removed those components, allowing a `review_ready` artifact to name a different destination from the authored source fact. Test-only head `01ab876a228d8cf9caadce2a4760ca3a1ffeedb7` covered a normal query-dependent URL, a hash-routed URL, and a secret-bearing combination; exact RED CI `34224341164` failed that contract before concurrent writer `3f16e062e5d68dd9ec4b839514ddc2aa396a4358` supplied the shared root fix without rewriting history. Successor `c2f4887080d572d6c16347395b306905f18e6846` added the missing buyer-facing preview regression, but its first integration accidentally narrowed the export matrix to the secret-bearing combination. Canonical child `170a65830fddcdc688ef53cdc793a84dbc4d0025` restores every valid matrix case while retaining the preview contract: query/fragment URLs remain `service_url_format` blockers, are withheld from preview, and export as `null` rather than being rewritten. CI `34225294729` passed 70/70 Vitest, the production build, all PostgreSQL evidence, and 22 Playwright/axe cases with eight intentional project-scope skips; artifact `10055464028` is bound by digest `sha256:90e30f973dcf490e226412039c8b3e43d50f0655dd7f5dc901f8d8f93100d845`. This intentionally narrows admissible v1 URLs instead of manufacturing a different customer fact. The final URL-boundary follow-up found that WHATWG `URL.search` and `URL.hash` are empty for authored bare delimiters even though serialization retains `?` or `#`. Test-first head `0580f466837bc5b000a54e79732b08bf83a3fd2f` added both empty-delimiter cases to the existing query/hash/secret matrix; CI `34230043117` passed lint and failed in `npm test` before the source repair. Minimal child `9d92a9ea716c5427161450df34ef142caaaf64e6` checks the canonical serialization for actual delimiters, rejecting bare query/fragment markers through the one readiness/preview/export boundary. Positive edge coverage at `a6cf635fcfc082fafacff13b658108fd076fd474` separately proves encoded `%3F`/`%23` pathname data remains admissible and byte-preserved, preventing a later raw-input substring check from over-blocking legitimate path data. Exact source-fix CI `34230291396` passed 70/70 Vitest, build, PostgreSQL migration/concurrency/restart/restore, and 22 Playwright/axe cases with eight intentional project-scope skips; artifact `10057683181` is bound by digest `sha256:6d2e7fc1ddafefdbd55e9c13b716dca123ba148dc753d65efb26d1bb5e3c26f4`. This closes the reviewed delimiter bypass without widening URL parsing or adding another validator. @@ -67,7 +69,7 @@ The final URL-boundary follow-up found that WHATWG `URL.search` and `URL.hash` a | Area | Evidence | Status | Commercialization gap | Owner/action | Next verification | | --- | --- | --- | --- | --- | --- | | Guided authoring | PRD, ADR-0002, seven routed editors, first-responsibility startup, `getReview`, explicit retention `getDraftReview`, no-collection/transfer states | Repaired foundation | Fresh state no longer skips or falsely completes step 1; collection, retention, transfer and other responsibilities fail closed independently; collection-path evidence remains unstructured free text and legal sufficiency is deliberately separate | Policy Fact Authoring: preserve deterministic completeness and independent authority; add structured path-evidence types only when a real integration/use case proves the need | Exact-head unit/UI edge tests, then browser E2E | -| Draft portability | Versioned `createPolicyExport`, fixed-name browser Blob download, explicit readiness state/finding codes, unsafe-URL rejection across preview/readiness/export, keyboard/touch activation, JSON MIME, repeat-byte checks, activation-error guidance, and success/error URL cleanup | Implemented foundation; bounded exact-head browser download GREEN | No import/migration contract, in-progress cancellation evidence, localized resource contract, or immutable publication semantics; exported files remain operator-controlled sensitive data | Policy Fact Authoring: preserve deterministic schema-versioned projection without network transfer, lossy URL rewriting, or inferred facts | Future version compatibility, cancellation, locale, and fail-closed import tests before schema evolution | +| Draft portability | Versioned `createPolicyExport`, fixed-name browser Blob download, explicit readiness state/finding codes, unsafe-URL rejection across preview/readiness/export, keyboard/touch activation, JSON MIME, repeat-byte checks, preparation/activation-error guidance, and allocation-aware success/error URL cleanup | Implemented foundation; bounded exact-head browser download GREEN | No import/migration contract, in-progress cancellation evidence, localized resource contract, or immutable publication semantics; exported files remain operator-controlled sensitive data | Policy Fact Authoring: preserve deterministic schema-versioned projection without network transfer, lossy URL rewriting, or inferred facts | Future version compatibility, cancellation, locale, and fail-closed import tests before schema evolution | | Customer-fact authority | Zero-inferred startup facts; truthful initial rail state; explicit no-collection; independent explicit retention status; explicit transfer states; stale dependent-fact invalidation | Repaired | No known buyer-facing authority dead end remains in the in-memory seven-step fact model; hosted persistence must preserve these independent states without deriving one from another | Policy Fact Authoring: encode collection and retention as separate revision-owned facts; no automatic no-collection→no-retention rule | Persistence/schema invariant tests and exact-head UI tests | | Review workspace | Live preview, total blocker count, warning-to-owner navigation, buyer-facing readiness guidance, deterministic step-heading focus transfer, Playwright viewport/screenshot harness | Implemented foundation; offscreen focus repaired and exact-head browser verified | Automated desktop/tablet/mobile focus-scroll evidence is GREEN; broader interaction coverage remains bounded | UX: retain exact-head artifacts, then extend interaction coverage | Exact-head screenshots, keyboard/focus and accessibility checks | | Accessibility | Semantic controls, visible focus behavior, focus-token >=3:1 regression, muted-text >=4.5:1 authored-surface regression, jsdom focus transition, axe/browser, focused-heading viewport checks, responsive retention transitions, and effective 200% browser-zoom reflow | Partial; bounded exact-head browser GREEN | Native browser UI zoom automation, screen-reader, and manual WCAG evidence remain absent | UX/Test Engineering: add a manual interaction record and remaining cases without claiming conformance from automation alone | Exact-head WCAG/browser matrix plus screen-reader and manual evidence | @@ -75,7 +77,7 @@ The final URL-boundary follow-up found that WHATWG `URL.search` and `URL.hash` a | Policy model | ADRs, ARCHITECTURE, TRD, ADR-0003, Proposed ERD, up/down migration, schema, runtime, two-session concurrency, restart, and dump/restore contract tests | Proposed 3NF foundation; PostgreSQL 18 exact-head CI execution required; browser runtime remains memory-only | Apply/down/apply, exact negative errors, observed lock waits, conflicting-fact rejection, same-item UPSERT convergence with NULL-safe complete label/mode/path assertions, process restart, and custom-format restore with a collection/no-retention cross-state, NULL-safe complete service name/URL and item assertions, and restored no-collection plus both retention contradiction checks are implemented; authorization, audit, encryption, deletion, and production-scale contention remain unproved | Platform: retain exact-head PostgreSQL evidence, then add the hosted authorization/audit boundary while keeping the adapter disabled | Tenant authorization, immutable audit, and encryption tests | | Publication | Readiness CTA truthfully does not pretend to publish; immutable `publication_revision` is designed | Planned | No authenticated approval, immutable publication, supersession, rollback, or public URL lifecycle | Review & Publication: implement after persistence/security entry criteria | Authorization, replay/digest, supersession tests | | Security/privacy | `docs/SECURITY.md`; local-first runtime; SHA-pinned checkout | Baseline documented | Hosted tenant model, encryption/key handling, audit/incident/retention evidence absent | Platform/Security: threat-model hosted boundary before backend | Exact-head security tests and org scans | -| Tests | 71 unit/UI/schema/workflow regressions plus PostgreSQL runtime/concurrency/restore scripts, 22 Playwright/axe passes, eight intentional project-scope skips, real-browser interaction/download/error contracts, and a screenshot artifact contract | Improved; bounded exact-head unit/build/browser/PostgreSQL GREEN | Repository-wide 100% execution/docstring coverage is not yet evidenced | Test Engineering: measure coverage and extend realistic edge cases | Exact-head coverage + browser/database evidence | +| Tests | 71 unit/UI/schema/workflow regressions plus PostgreSQL runtime/concurrency/restore scripts, 23 Playwright/axe passes, ten intentional project-scope skips, real-browser interaction/download/error contracts, and a screenshot artifact contract | Improved; bounded exact-head unit/build/browser/PostgreSQL GREEN | Repository-wide 100% execution/docstring coverage is not yet evidenced | Test Engineering: measure coverage and extend realistic edge cases | Exact-head coverage + browser/database evidence | | Performance | Static Vite client | Unevidenced | No realistic buyer-flow browser performance baseline; no network backend exists for meaningful k6 endpoint evidence | Operability: record browser baseline now; add k6 only when hosted network surfaces exist | Real measurements before latency claims | | CI/security merge gate | Repo CI plus active organization ruleset-required workflows; immutable Node 24-based checkout, setup/cache, and artifact-upload action releases | Live external gate; warning-free evidence is re-fetched for the exact merge candidate | Every branch movement invalidates predecessor evidence and stale approval; current hosted jobs may remain queued before runner assignment and dependency/reviewer control-plane failures can fail closed independently | Re-fetch exact-head workflows/reviews; use the central owner path for runner/dependency-review incidents rather than leaf-side churn or bypass | Terminal exact-head checks with no action-runtime deprecation warnings + independent approval + resolved threads | From becfd9579c47d12b4b0b474a3af169ce678c75e1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 23:12:40 +0900 Subject: [PATCH 44/45] docs: document authoring helper invariants --- src/App.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/App.tsx b/src/App.tsx index 0a904b8..b05850e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -48,6 +48,7 @@ function FactStep({ current, title, description, fields, facts, setFacts, setCur setFacts: (facts: DraftFacts) => void setCurrent: (step: number) => void }) { + /** Updates one fact and clears dependent facts when its owning state makes them inapplicable. */ const update = (key: keyof DraftFacts, value: string) => { const next = { ...facts, [key]: value } as DraftFacts if (key === 'retentionStatus' && value !== 'applies') next.retentionPeriod = '' @@ -80,7 +81,9 @@ function CollectionForm({ items, setItems, noCollectionAttested, setNoCollection setNoCollectionAttested: (attested: boolean) => void setCurrent: (step: number) => void }) { + /** Applies one collection-item patch while preserving every sibling item. */ const update = (id: string, patch: Partial) => setItems(items.map((item) => item.id === id ? { ...item, ...patch } : item)) + /** Enforces a no-collection attestation by clearing facts that would contradict it. */ const setNoCollection = (attested: boolean) => { if (attested) setItems(items.map((item) => ({ ...item, enabled: false, purpose: '', detail: '', mode: '' }))) setNoCollectionAttested(attested) @@ -110,6 +113,7 @@ function CollectionForm({ items, setItems, noCollectionAttested, setNoCollection /** Captures processing purposes for collection items explicitly selected by the operator. */ function PurposeForm({ items, setItems, noCollectionAttested, setCurrent }: { items: PolicyItem[]; setItems: (items: PolicyItem[]) => void; noCollectionAttested: boolean; setCurrent: (step: number) => void }) { const enabled = items.filter((item) => item.enabled) + /** Updates the processing purpose for one selected collection item only. */ const updatePurpose = (id: string, purpose: string) => setItems(items.map((item) => item.id === id ? { ...item, purpose } : item)) return

3. 처리 목적

선택한 개인정보 항목마다 실제 처리 목적을 연결합니다. 목적이 없는 항목은 공개 검토를 통과할 수 없습니다.

@@ -216,7 +220,9 @@ export default function App() { const completedSteps = useMemo(() => getCompletedSteps(items, noCollectionAttested, facts), [items, noCollectionAttested, facts]) const blockingCount = collectionReview.blockingCount + draftFindings.length const [message, setMessage] = useState('') + /** Reports readiness for responsible review without claiming that a publication occurred. */ function publish() { setMessage(blockingCount ? '필수 확인 항목을 먼저 입력하세요.' : '필수 확인이 완료되었습니다. 현재 검토본을 책임자와 검토하고 필요한 사실을 보완하세요.') } + /** Downloads the deterministic local export and revokes its object URL after activation. */ function exportDraft() { let fileUrl: string | null = null try { From 4c74e5df9f4e4cc4be63790f2a44cdba8124b894 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 23:13:27 +0900 Subject: [PATCH 45/45] docs: document policy helper contracts --- src/policy.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/policy.ts b/src/policy.ts index e6f6574..f74a770 100644 --- a/src/policy.ts +++ b/src/policy.ts @@ -101,6 +101,7 @@ export function getReview(items: PolicyItem[], noCollectionAttested = false) { /** Derives non-collection authoring findings without inferring retention state from collection state. */ export function getDraftReview(facts: DraftFacts, _noCollectionAttested = false): DraftFinding[] { const findings: DraftFinding[] = [] + /** Appends one stable finding only when its owning operator-authored value is blank. */ const addWhenBlank = (value: string, code: string, step: number, label: string) => { if (!value.trim()) findings.push({ code, step, label }) } @@ -199,6 +200,7 @@ export type PolicyDraftExport = { /** Creates a deterministic draft export without network access, inferred facts, or credential-bearing service URLs. */ export function createPolicyExport(items: PolicyItem[], noCollectionAttested: boolean, facts: DraftFacts): PolicyDraftExport { + /** Normalizes optional human-entered text without inventing a non-empty fact. */ const trimOrNull = (value: string) => value.trim() || null const collectionReview = getReview(items, noCollectionAttested) const reviewFindingCodes = [