From ae5ce9ff64dd0b0e9c0e646fd3b3a0f4a6d2217c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 23:18:35 +0900 Subject: [PATCH 001/282] feat: add .github/workflows/ci.yml --- .github/workflows/ci.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d2045c3 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,20 @@ +name: CI +on: + pull_request: + push: + branches: [main, develop] +permissions: + contents: read +jobs: + verify: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci + - run: npm run lint + - run: npm test + - run: npm run build From 95f280b24d24d187f30301b6a091fe2ec05bc9a0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 23:18:37 +0900 Subject: [PATCH 002/282] feat: add .gitignore --- .gitignore | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5430350 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules +dist +.env +coverage +playwright-report +*.tsbuildinfo From 460d82540e1d6c589607ac43fc731e8893439617 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 23:18:38 +0900 Subject: [PATCH 003/282] feat: add LICENSE --- LICENSE | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..17c7167 --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2026 ContextualWisdomLab + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. From f9635dfe1b9f66e7677d090ef948aa0f3a423dc8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 23:18:40 +0900 Subject: [PATCH 004/282] feat: add docs/ADR-0001-policy-as-data.md --- docs/ADR-0001-policy-as-data.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 docs/ADR-0001-policy-as-data.md diff --git a/docs/ADR-0001-policy-as-data.md b/docs/ADR-0001-policy-as-data.md new file mode 100644 index 0000000..0aab9d2 --- /dev/null +++ b/docs/ADR-0001-policy-as-data.md @@ -0,0 +1,15 @@ +# ADR-0001: 정책 문서보다 정책 데이터를 원본으로 둔다 + +상태: 채택 + +## 결정 + +수집 항목, 목적, 보유 기간, 제공 대상, 이전 국가와 연락처를 구조화된 정책 데이터로 저장한다. 화면의 문서는 이 데이터에서 결정적으로 렌더링한다. + +## 이유 + +문자열 문서를 직접 편집하면 동일 항목이 표·본문·요약에서 불일치할 수 있다. 구조화된 원본은 검증 규칙, 변경 이력, 다국어 렌더러와 공개 API를 같은 계약 위에 올릴 수 있다. + +## 결과 + +자유 문구가 필요한 예외에는 출처와 검토 상태를 함께 저장해야 한다. 법령 변경은 템플릿 버전과 규칙 세트 버전으로 추적한다. From d7008b7450925b78aad031c4d75b41593e5e232d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 23:18:42 +0900 Subject: [PATCH 005/282] feat: add docs/PRD.md --- docs/PRD.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 docs/PRD.md diff --git a/docs/PRD.md b/docs/PRD.md new file mode 100644 index 0000000..ef6bb4e --- /dev/null +++ b/docs/PRD.md @@ -0,0 +1,32 @@ +# PolicyWeave 제품 요구사항 + +## 문제 + +소규모 웹·앱 운영자는 실제 데이터 처리 흐름을 법정 고지 문구로 옮기는 과정에서 누락을 만들기 쉽다. 범용 템플릿은 서비스가 수집하지 않는 항목까지 남기거나, 처리 목적·보유 기간·제3자 제공을 서로 모순되게 작성한다. + +## 제품 원칙 + +PolicyWeave는 법률 문장을 임의로 창작하는 도구가 아니다. 운영자가 입력한 사실을 구조화하고, 적용 근거와 확인이 필요한 부분을 분리해 검토본을 만든다. 자동 생성 결과는 변호사·개인정보보호책임자의 검토를 대체하지 않는다. + +## MVP + +- 7단계 질문 흐름: 서비스 정보, 수집 항목, 처리 목적, 보유 기간, 제3자 제공, 국외 이전, 담당자 +- 입력과 동시에 갱신되는 개인정보처리방침 검토본 +- 필수 입력 누락과 권장 검토 항목의 구분 +- 처리 항목별 목적·근거 연결 +- 공개 전 검토 요약과 버전 정보 +- 정적 공개 URL 발행 계약(후속 백엔드에서 구현) +- JSON 내보내기 가능한 정책 데이터 모델 + +## 비목표 + +- 법률 자문 또는 준법 보장 +- 사용자의 실제 서비스·SDK를 자동 추측 +- 동의가 필요 없는 처리에 무조건 동의를 요구하는 방식 + +## 성공 기준 + +- 사용자는 누락 없이 검토본을 생성할 수 있다. +- 모든 경고는 해당 입력 단계로 이동할 수 있다. +- 모바일에서도 작성과 미리보기를 전환할 수 있다. +- 키보드만으로 모든 입력과 주요 동작을 수행할 수 있다. From cc82ccd93f872b75d27da38d46a515096c47903e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 23:18:44 +0900 Subject: [PATCH 006/282] feat: add eslint.config.js --- eslint.config.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 eslint.config.js diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..5d28fa8 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,15 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' + +export default tseslint.config( + { ignores: ['dist'] }, + { + files: ['**/*.{ts,tsx}'], + languageOptions: { parser: tseslint.parser, ecmaVersion: 2022, globals: globals.browser, parserOptions: { ecmaFeatures: { jsx: true }, sourceType: 'module' } }, + plugins: { '@typescript-eslint': tseslint.plugin, 'react-hooks': reactHooks, 'react-refresh': reactRefresh }, + rules: { ...js.configs.recommended.rules, ...reactHooks.configs.recommended.rules, ...reactRefresh.configs.vite.rules, 'no-unused-vars': 'off', '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], 'no-undef': 'off' }, + }, +) From b67e1a3377c9fe2757291dd062facc3edf06ea3f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 23:18:46 +0900 Subject: [PATCH 007/282] feat: add index.html --- index.html | 1 + 1 file changed, 1 insertion(+) create mode 100644 index.html diff --git a/index.html b/index.html new file mode 100644 index 0000000..c41b5d7 --- /dev/null +++ b/index.html @@ -0,0 +1 @@ +
From d0a84aee6158936288ab4b6ea4f6854d5ffc8a63 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 23:18:48 +0900 Subject: [PATCH 008/282] feat: add package-lock.json --- package-lock.json | 3787 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 3787 insertions(+) create mode 100644 package-lock.json diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..f240b8e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3787 @@ +{ + "name": "policyweave", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "policyweave", + "version": "0.1.0", + "dependencies": { + "@vitejs/plugin-react": "latest", + "lucide-react": "latest", + "react": "latest", + "react-dom": "latest", + "typescript": "latest", + "vite": "latest" + }, + "devDependencies": { + "@eslint/js": "latest", + "@testing-library/jest-dom": "latest", + "@testing-library/react": "latest", + "@types/react": "latest", + "@types/react-dom": "latest", + "eslint": "latest", + "eslint-plugin-react-hooks": "latest", + "eslint-plugin-react-refresh": "latest", + "globals": "latest", + "jsdom": "latest", + "typescript-eslint": "^8.68.0", + "vitest": "latest" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "6.0.7", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-6.0.7.tgz", + "integrity": "sha512-vC/bk1Lz7Tn/EfU9/apOTBk80/8dyGyWMowPoV1tJ52muDGsDqt2HPT2klrFUiY60MQmQv9q8yIht15JnBgDGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.3.0", + "@csstools/css-color-parser": "^4.1.10", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.5.2" + }, + "engines": { + "node": "^22.13.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-8.3.2.tgz", + "integrity": "sha512-93Z1N+BQNXysodoicpOIyNh2drHfz/CTf9nnT0FEx72GJcIiwgydD7tGAr78j41LsYn3hlRn+LdGPuBLn1Bl8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.5.2" + }, + "engines": { + "node": "^22.13.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.1.tgz", + "integrity": "sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz", + "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.2.tgz", + "integrity": "sha512-3QKjR/vxyjcSXBLgb6lP0S3MGdvwbmqSsvLPbYdVORqPDc8FX1HAJ0Spk38bxaRXgvENTA47tlhhbb5Z2e8hEg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.1.1", + "@csstools/css-calc": "^3.3.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.10.tgz", + "integrity": "sha512-xBja6gaAaH2R2c7eNyl0TY4dhnnZ2uhj+KXpLdEQ6M/wuk9bYFZM8wY0ykw3VO4TgEJ56KGlerXS/9KBKVR/Cg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.147.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.147.0.tgz", + "integrity": "sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.6.tgz", + "integrity": "sha512-b+jTcARdTiFLI6jB4a5XjTm0RWd6KcRfQj/I2356fxUZemiho9zQLxo0RtCuMDAyKcLo6cEltkgbQp6d1+sjjQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.6.tgz", + "integrity": "sha512-lkWU8ZJaRk9q3CIEY1Tc7vIFALp3Xw5NfGJo2hQg5oIqNgxWi1zI+IiDEK3r70BF5Dzol1tcXsnzsRc8NLhG+Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.6.tgz", + "integrity": "sha512-dgR56NYnvAszm7Ob1B2/Vn0e8bUQYZH2UjVaMMtMVOCKFSfjhfLmuA/9+O+F+ajUdG6B/bSssrKW6JJYASa8jA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.6.tgz", + "integrity": "sha512-vpVxFvUCFioJqug7OTvqptkc4yb8UX0AwfDmJpaR/0sWz+BUmqSVAf7c8JkUgnN8YLspb4a/N6NhTyMAmdyQ7Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.6.tgz", + "integrity": "sha512-h1wG6Y6K3JlRswxsI64qQJqBAy4vrLuHgRbc8CZMGSWTOFRY6ghMApM1NKzB2I0n5xV1fjkE18SuVl2QpLeNpA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.6.tgz", + "integrity": "sha512-tbCiqub0q2MVWJKgF5PoAlNWCtQydiOYSLIkd8sByqK/6MMYLJRcSXSYodqYtd0O+Fw7QaVmKKlS4oL94YRZ0w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.6.tgz", + "integrity": "sha512-oxK9+baEBPhZG5HB4URY+uU04zJWeZlH6Tb9rB5DK4DF9XR1uXNLXt5Q5ZsugTKayNCNLhkcwz/ye74hRI98dg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.6.tgz", + "integrity": "sha512-muWCk27FVBEZtv0MsK8gnfSmgczA8KQ0uRVJbTABKhkRfQc38aUrcb7fhi3BNiyseFmgcRsoMfQsSNJ+DbZdSw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.6.tgz", + "integrity": "sha512-eWDoSfU7Co2qj3vgB3Dt4lj1mG6CoWbcJQkRMP3XJplyCMtuaq3LHvPFjS9QIPvMGWVadJC04Xiy0IdcVPtnwQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.6.tgz", + "integrity": "sha512-2bWNjRSIayvupRKxXUY2tWG9fYdoUlTqWywHRvE8Eq3GvuQ+f2HeIkve697fIt+IQs/PV8yFsdWuhp1aJ1PdnA==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.6.tgz", + "integrity": "sha512-KekI0gS0wLxe1UBSQSjenBVwou/JkcQPDzBPICGZjxUv9k3RteHDPBQaiOicZUFKRIH2wKEimGwVpnJsbPzu7w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.6.tgz", + "integrity": "sha512-TvtPnfVr+HtyGiDmPK4VWmlNm7QhNNAcK5Q9A7aOXsI8545yCyaoMaicXrFZ72JzeYjaUVk7yT243zT0jzjFKQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.6.tgz", + "integrity": "sha512-iOo0VEay2XFhaCcH0sps5XIimkSuOnNaZrf6+ZkoSOQBJPKNU48RkmJv0/lSpipexu5P+ouFgafe5IGr/DiQfg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.6.tgz", + "integrity": "sha512-y5NTmmasMS455JlOCO4ZM9krIchv3Mvm1crL1iUPGOPgEzSkves9n0SdC5Sjz6+qWDFhd8/JpfWMH8NSWNHe+A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.6.tgz", + "integrity": "sha512-np8iZSLfXlAD4kWhiyq/u0Yt8oZDtRQ8lGhQaCXo2rl37KNjeU0GjJuwr4P3oeZ++ROfofsKNBqR5LTO8aXyWQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-7.0.1.tgz", + "integrity": "sha512-oMDTC3oA+6CXSO2JZnvOI7CA6oVub6kij5ggk9ohwye5slmkwxYDXcPOVxgMw/RQlticjtO0C1RZkR97HgrWMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=22", + "npm": ">=6", + "yarn": ">=1" + }, + "peerDependencies": { + "@testing-library/dom": ">=10 <11", + "vitest": ">= 0.32" + }, + "peerDependenciesMeta": { + "vitest": { + "optional": true + } + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.3", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.3.tgz", + "integrity": "sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-fMPwH9v7r/pp43yUd2/Mbiex5KouJwwR3dzHkhLREUC6764VyDsqxhAxv6OFEYR1RhjOyD1naqba8ECDBe7ZQg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.68.0.tgz", + "integrity": "sha512-WASHDpCm6qO5jj9g1a+8NiW5+GCkAyLReR56/4VruYmNgfUmqpxOfZ2Yfb8xGfJPWv5Qi6LSD8sXdces3vbp/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.68.0", + "@typescript-eslint/type-utils": "8.68.0", + "@typescript-eslint/utils": "8.68.0", + "@typescript-eslint/visitor-keys": "8.68.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.68.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.8.tgz", + "integrity": "sha512-YYNsSlXBjMk92SKnkwvB5LOVSa6OznlFUGcsvrFgNJbJCd0M1XKeFVRc8ZByeCqz32FivYNHJVooLmdqrmvp/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.68.0.tgz", + "integrity": "sha512-fHq2VC1kpyYfvEcbiMjOpySY4WS7voEp89yAThrHRX5sm9j2lzYppCb2umFMEed4fWcyeLjHxrz0mpjNBaBxMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.68.0", + "@typescript-eslint/types": "8.68.0", + "@typescript-eslint/typescript-estree": "8.68.0", + "@typescript-eslint/visitor-keys": "8.68.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.68.0.tgz", + "integrity": "sha512-5GQtWZCXFcFYux955pvoS02WLc49pXNlvIxocKjS0clvwo3in1RdlzVKyiqQH9vE5AKWFLTaUgeQkOrTS+0Qxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.68.0", + "@typescript-eslint/types": "^8.68.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.68.0.tgz", + "integrity": "sha512-T5eXpcaJNg8bhjHJ8Rjp68Vq/QBteYtTKY8TZqVNPaUbuz0f6jI9t6aDkylwvalpAB9XTTFeFOjrjXAZ3YvmVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.68.0", + "@typescript-eslint/visitor-keys": "8.68.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.68.0.tgz", + "integrity": "sha512-F7zrGQfiJHojPwi8vhxZQC1tWtJzvL74cK/nqri2lk8YUXvYaYwl263xOJ69jDWPUk1hmcdoayFwk9lX09npVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.68.0.tgz", + "integrity": "sha512-X77zqoY1EjeWGs/0JNxeaMfp5C5lIz4Tw8y66F1Ne8Faq6g424sBNYM6xBAqElfGZPLpWS+CZAp0DXyKDzWiHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.68.0", + "@typescript-eslint/typescript-estree": "8.68.0", + "@typescript-eslint/utils": "8.68.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.68.0.tgz", + "integrity": "sha512-9RnpsGJjrAllCMefGVVsImJM24YurhC0Q1h4UbvivtvOqXmR/vEJge2OoE++z9m6hyg8T1Q8t5SNT6tHSbrxcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.68.0.tgz", + "integrity": "sha512-OKKsD0tYmoNiU5PW2zehO1yO56jYOm1ShYlxon/Z0SJNidAkdVg86eg9ruRuoXf8xfnuWZGbwDsStkoXbZtIIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.68.0", + "@typescript-eslint/tsconfig-utils": "8.68.0", + "@typescript-eslint/types": "8.68.0", + "@typescript-eslint/visitor-keys": "8.68.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.68.0.tgz", + "integrity": "sha512-PB5gJMMOg0Q5P1tsgWtEAqQacJXq0qEqRHDX/YJ4FaTMLfZPpHB3gjl2EJuiZyPABxmj4ZQYiY9m1bdAJ5y7tQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.68.0", + "@typescript-eslint/types": "8.68.0", + "@typescript-eslint/typescript-estree": "8.68.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.68.0.tgz", + "integrity": "sha512-YR65gGdGvTUAWLldC3xLOvOzamdGzB4A5/N8rehEaHs3Zvoe39BhgY+u0SPch1OvrVTfLcc55wsSgK2NcnTS/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.68.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.1.tgz", + "integrity": "sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw==", + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "oxc-transform-react": "^0.145.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "oxc-transform-react": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", + "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", + "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.11", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", + "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", + "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.11", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", + "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.11", + "@vitest/utils": "4.1.11", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", + "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", + "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.11", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.20", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.20.tgz", + "integrity": "sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/electron-to-chromium": { + "version": "1.5.417", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.417.tgz", + "integrity": "sha512-4T+DTDWuMPM4aHlHwWdAVCVWwp7LDilnhzkj+c/Lbj91XSQrLuOmZSLtS9Q4iIqjlPUbPOnC624zDVVHCHaolQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.9.1.tgz", + "integrity": "sha512-9VaAkDURekixUQJy0oJYl2DcN6oKMfxay7XzaGYAWQwsb6qfKf+x76R2k1L8kb1boc+FyCAaTA9GmiKaaiaF+A==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.5.tgz", + "integrity": "sha512-vG7yLURXNvCHy0FBdbZRwIu0BLPJMlUUJS2Ep7ud9w1YCLftFZtuEjyjhym0Qq9yuZ6LJUitNlu/hMk0gakXAw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.11.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.11.0.tgz", + "integrity": "sha512-Z2I8hM+PbJDXQDq3Icgpzv+mPdwr68iZUU9d5WW4FuXfDUQfkZaZuvjMv42/5crNyw154+9+VWXbYrUgDXbxNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.0.1.tgz", + "integrity": "sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^6.0.5", + "@asamuzakjp/dom-selector": "^8.3.0", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.7", + "@exodus/bytes": "^1.15.1", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.5.2", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.2", + "undici": "^8.9.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^17.1.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "canvas": "^3.2.3" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.38.0.tgz", + "integrity": "sha512-xZCyBd/wiVUDactoCc+42TjL0aB7EBOXsuX+tjz+W/sGzw2KhHpL1NOH3FIaVUcpimvUBpIYfz34Ofj9S5JEzQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.54", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz", + "integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rolldown": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.6.tgz", + "integrity": "sha512-vMM4q3aixf46GiF1Kok8jDPFsEpXgFWGjUHXNkNHNm+Y2adXAG2dbX91jkti3i0ZRsOlcmbuzAz1poObSHCmUA==", + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.147.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.6", + "@rolldown/binding-android-arm64": "1.2.6", + "@rolldown/binding-darwin-arm64": "1.2.6", + "@rolldown/binding-darwin-x64": "1.2.6", + "@rolldown/binding-freebsd-x64": "1.2.6", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.6", + "@rolldown/binding-linux-arm64-gnu": "1.2.6", + "@rolldown/binding-linux-arm64-musl": "1.2.6", + "@rolldown/binding-linux-ppc64-gnu": "1.2.6", + "@rolldown/binding-linux-s390x-gnu": "1.2.6", + "@rolldown/binding-linux-x64-gnu": "1.2.6", + "@rolldown/binding-linux-x64-musl": "1.2.6", + "@rolldown/binding-openharmony-arm64": "1.2.6", + "@rolldown/binding-win32-arm64-msvc": "1.2.6", + "@rolldown/binding-win32-x64-msvc": "1.2.6" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.4.11", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.11.tgz", + "integrity": "sha512-aBiNayCfTQxuIJBm06M+xR14cYaYlDlSXZbgsnKzKNxDKUVq7KFwTjwBSsb7m9Y5xO8WfPnBc63WaYFMTGlvqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.11" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.11", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.11.tgz", + "integrity": "sha512-CW3WN2rIIE/Of21mulhgnGOwoDyEFNygyIBOONSdyAuSATgMMUCpLeUlB+E8sAwA5xRV9hYPl+kyZ9citHCaKg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.68.0.tgz", + "integrity": "sha512-MHy0Y0ynqeEbx/S45+i/bBssdy3X6KNBfmJAP35GrgtNxu2TQ5K5xsFDhAnmsq1jvpdoZOPG1LGtJo0HWqYCrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.68.0", + "@typescript-eslint/parser": "8.68.0", + "@typescript-eslint/typescript-estree": "8.68.0", + "@typescript-eslint/utils": "8.68.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici": { + "version": "8.10.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.10.1.tgz", + "integrity": "sha512-YQ3WlbqjYMmNpdvDH64jAgLjxuAR9+649calDWhbshYaeQGO2bR4nI94ORJmwI3J9YhoKQnpyGOK+0zlWS5N5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", + "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", + "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==", + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.26", + "rolldown": "~1.2.4", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0 || ^0.5.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", + "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.11", + "@vitest/mocker": "4.1.11", + "@vitest/pretty-format": "4.1.11", + "@vitest/runner": "4.1.11", + "@vitest/snapshot": "4.1.11", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.11", + "@vitest/browser-preview": "4.1.11", + "@vitest/browser-webdriverio": "4.1.11", + "@vitest/coverage-istanbul": "4.1.11", + "@vitest/coverage-v8": "4.1.11", + "@vitest/ui": "4.1.11", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.1.0.tgz", + "integrity": "sha512-3GeworPmc2ZfEEHP7lEbUfBX/L75wdEsi0rLNhXcXxnoN5jyq0SL5gCy06SGW2cyTIZdTvWIDQNQoza++vKeaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.15.1", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^22.14.0 || >=24.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.5.4.tgz", + "integrity": "sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} From a5bcd73a7dd749df6cf391cc23a360bb6eb1d892 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 23:18:50 +0900 Subject: [PATCH 009/282] feat: add package.json --- package.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 package.json diff --git a/package.json b/package.json new file mode 100644 index 0000000..aa506bd --- /dev/null +++ b/package.json @@ -0,0 +1 @@ +{"name":"policyweave","private":true,"version":"0.1.0","type":"module","scripts":{"dev":"vite --host 0.0.0.0","build":"tsc -b && vite build","lint":"eslint .","test":"vitest run","test:watch":"vitest"},"dependencies":{"@vitejs/plugin-react":"latest","lucide-react":"latest","react":"latest","react-dom":"latest","typescript":"latest","vite":"latest"},"devDependencies":{"@eslint/js":"latest","@testing-library/jest-dom":"latest","@testing-library/react":"latest","@types/react":"latest","@types/react-dom":"latest","eslint":"latest","eslint-plugin-react-hooks":"latest","eslint-plugin-react-refresh":"latest","globals":"latest","jsdom":"latest","typescript-eslint":"^8.68.0","vitest":"latest"}} \ No newline at end of file From c11d50b2d7ca9796501e35c7eedcafd1e16fa900 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 23:18:52 +0900 Subject: [PATCH 010/282] feat: add src/App.tsx --- src/App.tsx | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 src/App.tsx diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..b18f015 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,69 @@ +import { useMemo, useState } from 'react' +import { AlertTriangle, Check, ChevronDown, ExternalLink, FileText, Link, Save } from 'lucide-react' +import { getReview, initialItems, PolicyItem, steps } from './policy' + +function StepRail({ current, setCurrent }: { current: number; setCurrent: (step: number) => void }) { + return +} + +function CollectionForm({ items, setItems, setCurrent }: { items: PolicyItem[]; setItems: (items: PolicyItem[]) => void; setCurrent: (step: number) => void }) { + const update = (id: string, patch: Partial) => setItems(items.map((item) => item.id === id ? { ...item, ...patch } : item)) + return
+

2. 수집 항목

서비스에서 실제로 수집하는 개인정보만 선택하세요. 선택한 항목에 따라 다음 단계가 달라집니다.

+
입력 원칙서비스 코드와 운영 절차에서 확인한 항목만 반영하세요. 추정으로 선택하지 않습니다.
+

기본 정보

+
수집 항목설명수집 여부
+
{items.map((item) =>
+
+ + {item.description} + +
+ {item.enabled && !item.purpose &&
} +
)}
+
+
+} + +function DocumentPreview({ items }: { items: PolicyItem[] }) { + const review = useMemo(() => getReview(items), [items]) + return
+

개인정보처리방침 미리보기

+
근거 법령 개인정보 보호법{review.blocking.length ? `검토 필요 ${review.blocking.length}` : '필수 확인 완료'}버전 0.1.0
+
+

개인정보처리방침 (검토본)

+

회사는 이용자의 개인정보를 중요하게 여기며, 관련 법령을 준수하기 위해 다음과 같이 개인정보 처리에 관한 사항을 알립니다.

+

제1조 (개인정보의 처리 목적)

+

회사는 아래 목적을 위해 개인정보를 처리합니다. 목적이 변경되는 경우 필요한 절차를 거쳐 별도로 알립니다.

+ {review.enabled.map((item) => )}
수집 항목처리 목적근거 확인
{item.label}{item.purpose || '처리 목적 입력 필요'}제15조 검토
+ {review.blocking.length > 0 &&
공개 전 확인{review.blocking.map((item) => item.label).join(', ')}의 처리 목적이 입력되지 않았습니다.
} +

제2조 (처리 및 보유 기간)

각 개인정보의 보유 기간은 다음 작성 단계에서 입력한 법정 보존 기간과 이용 목적 달성 시점을 기준으로 정합니다.

+

제3조 (제3자 제공)

제3자 제공 여부와 제공받는 자, 목적, 항목 및 보유 기간은 확인된 운영 사실에 따라 별도 표에 반영합니다.

+
+ + 생성된 문서는 법률 자문이 아닙니다. 공개 전 책임자의 검토가 필요합니다. +
+} + +export default function App() { + const [items, setItems] = useState(initialItems) + const [current, setCurrent] = useState(2) + const review = useMemo(() => getReview(items), [items]) + const [message, setMessage] = useState('') + function publish() { setMessage(review.blocking.length ? '필수 확인 항목을 먼저 입력하세요.' : '공개 URL 발행에는 저장소 백엔드 연결이 필요합니다.') } + return
+
PolicyWeave작성 중버전 0.1.0 (임시저장) 자동 저장됨
+
+
검토 요약확인을 마친 뒤 공개 URL을 발행하세요.
필수 확인 {review.blocking.length}건
권장 검토 {review.recommended.length}건
{message}
+
+} From e3b81b233b859897c4638ca726c619b6df67563d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 23:18:53 +0900 Subject: [PATCH 011/282] feat: add src/main.tsx --- src/main.tsx | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 src/main.tsx diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..571dd17 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,6 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import App from './App' +import './styles.css' + +createRoot(document.getElementById('root')!).render() From 49ddaea94ad1712d40a350a1c90a36f00dad926e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 23:18:55 +0900 Subject: [PATCH 012/282] feat: add src/policy.test.ts --- src/policy.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/policy.test.ts diff --git a/src/policy.test.ts b/src/policy.test.ts new file mode 100644 index 0000000..7259e24 --- /dev/null +++ b/src/policy.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, it } from 'vitest' +import { getReview, initialItems } from './policy' + +describe('policy review', () => { + it('includes only selected collection items', () => { + expect(getReview(initialItems).enabled.map((item) => item.id)).toEqual(['name', 'email', 'usage']) + }) + it('blocks publishing when a selected item has no purpose', () => { + const items = initialItems.map((item) => item.id === 'phone' ? { ...item, enabled: true } : item) + expect(getReview(items).blocking.map((item) => item.id)).toEqual(['phone']) + }) +}) From f3e2a3acd065d8aca23b6fd5aecea6947c4902c2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 23:18:56 +0900 Subject: [PATCH 013/282] feat: add src/policy.ts --- src/policy.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/policy.ts diff --git a/src/policy.ts b/src/policy.ts new file mode 100644 index 0000000..4f39d95 --- /dev/null +++ b/src/policy.ts @@ -0,0 +1,23 @@ +export type CollectionMode = '필수' | '선택' +export type PolicyItem = { id: string; label: string; description: string; purpose: string; enabled: boolean; mode: CollectionMode; detail?: string } + +export const initialItems: PolicyItem[] = [ + { id: 'name', label: '이름', description: '서비스 이용자 식별', purpose: '서비스 이용자 식별', enabled: true, mode: '필수' }, + { id: 'email', label: '이메일 주소', description: '계정 식별, 로그인, 중요 고지 수신', purpose: '계정 식별 및 중요 고지', enabled: true, mode: '필수' }, + { id: 'phone', label: '휴대전화 번호', description: '본인 확인, 알림 발송', purpose: '', enabled: false, mode: '선택' }, + { id: 'usage', label: '서비스 이용 기록', description: '접속 로그, 이용 내역, 클릭 기록 등', purpose: '서비스 개선 및 이용 통계 분석', enabled: true, mode: '필수' }, + { id: 'ip', label: '접속 IP 주소', description: '보안, 부정 이용 방지', purpose: '보안 및 부정 이용 방지', enabled: false, mode: '선택' }, + { id: 'cookie', label: '쿠키 및 유사 기술', description: '설정 유지, 통계 분석', purpose: '', enabled: false, mode: '선택' }, + { id: 'address', label: '주소', description: '배송, 청구서 발송', purpose: '', enabled: false, mode: '선택' }, + { id: 'payment', label: '결제 정보', description: '결제 처리, 환불 처리', purpose: '', enabled: false, mode: '선택' }, + { id: 'content', label: '게시물 및 문의 내용', description: '게시 기능, 고객 문의 처리', purpose: '', enabled: false, mode: '선택' }, +] + +export const steps = ['서비스 정보', '수집 항목', '처리 목적', '보유 기간', '제3자 제공', '국외 이전', '개인정보 보호 담당자'] + +export function getReview(items: PolicyItem[]) { + const enabled = items.filter((item) => item.enabled) + const blocking = enabled.filter((item) => !item.purpose.trim()) + const recommended = enabled.filter((item) => item.mode === '선택' && item.id !== 'usage') + return { enabled, blocking, recommended } +} From ec0d3f50e83fa493ad4de5a094cc07c920e53177 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 23:18:58 +0900 Subject: [PATCH 014/282] feat: add src/styles.css --- src/styles.css | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 src/styles.css diff --git a/src/styles.css b/src/styles.css new file mode 100644 index 0000000..60cd32f --- /dev/null +++ b/src/styles.css @@ -0,0 +1,3 @@ +:root{font-family:Pretendard,"Noto Sans KR",system-ui,sans-serif;color:#202622;background:#f8f9f7;font-synthesis:none;--green:#174f35;--green-soft:#eaf1ec;--line:#d9ded9;--muted:#6d756f;--paper:#fffefb;--amber:#a85b00}*{box-sizing:border-box}body{margin:0;min-width:320px}button,input,select{font:inherit}button{cursor:pointer}.topbar{height:58px;display:flex;align-items:center;gap:18px;padding:0 20px;border-bottom:1px solid var(--line);background:white}.brand{font:700 27px Georgia,serif;color:var(--green);text-decoration:none;margin-right:10px}.document-name{display:flex;align-items:center;gap:7px;background:none;border:0;font-weight:700}.status{background:var(--green-soft);color:var(--green);padding:5px 10px;border-radius:4px;font-size:12px}.version,.save-state{font-size:12px;color:var(--muted)}.save-state{margin-left:auto;display:flex;align-items:center;gap:5px}.outline,.primary{min-height:38px;padding:0 16px;border-radius:4px;border:1px solid #cfd5d0;display:inline-flex;align-items:center;justify-content:center;gap:7px;background:white;color:#29302b}.primary{background:var(--green);color:white;border-color:var(--green);font-weight:700}.primary:disabled{background:#aeb8b1;border-color:#aeb8b1;cursor:not-allowed}.workspace{display:grid;grid-template-columns:240px minmax(520px,1fr) minmax(540px,1.05fr);height:calc(100vh - 152px);min-height:630px}.rail,.form-panel,.preview{min-width:0;background:white}.rail{border-right:1px solid var(--line);padding:22px 14px 14px;display:flex;flex-direction:column}.progress-copy{display:flex;justify-content:space-between;font-size:12px}.progress-copy span{color:var(--muted)}.progress{height:7px;background:#eceeec;border-radius:5px;margin:15px 0 18px;overflow:hidden}.progress i{display:block;height:100%;background:var(--green)}.rail ol{padding:0;margin:0;list-style:none}.rail li{position:relative}.rail li:not(:last-child)::after{content:"";position:absolute;left:13px;top:40px;height:24px;border-left:1px solid var(--line)}.rail li button{width:100%;display:flex;gap:13px;align-items:flex-start;text-align:left;border:0;background:transparent;padding:12px 5px;border-radius:4px}.rail li.active button{background:#f4f7f4;outline:1px solid #cad5cc}.step-number{width:26px;height:26px;flex:0 0 auto;display:flex;align-items:center;justify-content:center;border:1px solid #cbd1cc;border-radius:50%;font-size:12px}.done .step-number,.active .step-number{background:var(--green);border-color:var(--green);color:white}.rail b{display:block;font-size:13px;margin-top:3px}.rail small{display:block;color:var(--muted);font-size:11px;margin-top:5px}.rail .full{margin-top:auto;width:100%}.form-panel{overflow:auto;padding:24px 20px;border-right:1px solid var(--line)}.section-head h1{font-size:20px;margin:0 0 7px}.section-head p{font-size:12px;color:var(--muted);margin:0 0 17px}.notice{border:1px solid #cfd9e3;background:#f7fafc;padding:12px 14px;display:flex;flex-direction:column;gap:4px;font-size:12px}.notice span{color:var(--muted)}.form-panel h2{font-size:13px;margin:20px 0 9px}.table-head,.item-row{display:grid;grid-template-columns:1.05fr 1.55fr 92px;gap:14px;align-items:center}.table-head{border-bottom:1px solid var(--line);padding:0 8px 8px;color:#555f58;font-size:11px}.item{border-bottom:1px solid #e4e7e4}.item-row{min-height:42px;padding:6px 7px;font-size:12px}.check-label{display:flex;align-items:center;gap:8px}.check-label input{position:absolute;opacity:0}.box{width:17px;height:17px;border:1px solid #b8beba;display:flex;align-items:center;justify-content:center;color:transparent}.check-label input:focus-visible+.box{outline:3px solid #8eb59e;outline-offset:2px}.check-label input:checked+.box{background:var(--green);border-color:var(--green);color:white}.item-row>span{color:#5f6862}.select-wrap{position:relative}.select-wrap select{width:100%;height:32px;padding:0 28px 0 10px;border:1px solid #d1d6d2;background:white;appearance:none}.select-wrap svg{position:absolute;right:8px;top:9px;pointer-events:none}.conditional{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin:0 7px 10px;padding:10px;background:#f7f8f7;border:1px solid #e1e4e1}.conditional label{font-size:10px;color:var(--muted)}.conditional input{display:block;width:100%;height:32px;margin-top:5px;border:1px solid #d5d9d6;padding:0 8px}.form-actions{display:flex;justify-content:space-between;margin-top:18px}.preview{padding:14px 20px 18px;overflow:auto;background:#fafbfa}.preview-title,.meta{display:flex;align-items:center;gap:12px}.preview-title{justify-content:space-between}.preview-title h2{font-size:14px}.meta{font-size:11px;color:var(--muted);margin:8px 0 12px}.meta b,.warn-tag,.ok-tag{background:#eef1ee;padding:5px 8px;border-radius:4px;font-weight:500}.warn-tag{color:var(--amber);background:#fff4e7;border:1px solid #f3d5ae}.ok-tag{color:var(--green);background:var(--green-soft)}.paper{background:var(--paper);border:1px solid #d6d7d4;padding:23px 27px;min-height:500px;font-family:"Noto Serif KR",Batang,serif;box-shadow:0 2px 5px #00000008}.paper h2{text-align:center;font-size:20px;margin:0 0 20px}.paper h3{font-size:14px;margin:22px 0 8px}.paper p{font-size:11px;line-height:1.85}.paper table{border-collapse:collapse;width:100%;font-size:10px;margin:14px 0}.paper th,.paper td{border:1px solid #d4d4d0;padding:7px;text-align:left}.paper th{background:#f5f4f0}.missing{color:#9b4e00;background:#fff8ee}.document-warning{display:flex;align-items:flex-start;gap:10px;border:1px solid #edc791;background:#fff9ef;color:#874c08;padding:11px;font-family:Pretendard,sans-serif;font-size:11px}.document-warning span{display:flex;flex-direction:column;gap:3px}.wide{width:100%;margin-top:14px}.legal-note{display:block;text-align:center;color:var(--muted);margin-top:8px}.review-bar{height:94px;border-top:1px solid var(--line);background:white;padding:12px 20px;display:grid;grid-template-columns:230px 170px 170px minmax(260px,1fr);gap:15px;align-items:center;position:relative}.review-bar>div:first-child{display:flex;flex-direction:column;gap:5px}.review-bar small{color:var(--muted)}.review-stat{border-left:1px solid var(--line);padding-left:18px;display:flex;gap:12px;align-items:center;color:var(--green)}.review-stat span{font-size:11px;color:var(--muted)}.review-stat b{display:block;font-size:20px;color:#222}.review-stat.blocking{color:var(--amber)}.publish{height:44px}.review-bar output{position:absolute;right:22px;bottom:2px;font-size:10px;color:var(--amber)}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}:focus-visible{outline:3px solid #8eb59e;outline-offset:2px} +@media(max-width:1100px){.workspace{grid-template-columns:210px 1fr}.preview{display:none}.review-bar{grid-template-columns:1fr 150px 150px 240px}} +@media(max-width:720px){.topbar{height:auto;min-height:58px;flex-wrap:wrap;padding:10px 14px;gap:9px}.version,.save-state{display:none}.workspace{display:block;height:auto;min-height:0}.rail{border-right:0;border-bottom:1px solid var(--line);padding:12px}.rail ol{display:flex;overflow-x:auto;gap:6px}.rail li{min-width:145px}.rail li::after{display:none}.rail .full{display:none}.form-panel{border:0;padding:18px 14px}.table-head{display:none}.item-row{grid-template-columns:1fr 78px}.item-row>span{grid-row:2;grid-column:1/-1;padding-left:25px}.conditional{grid-template-columns:1fr}.review-bar{height:auto;grid-template-columns:1fr 1fr;padding:12px 14px}.review-bar>div:first-child{grid-column:1/-1}.publish{grid-column:1/-1}.document-name{max-width:190px;overflow:hidden;white-space:nowrap}.brand{font-size:23px}} From 61494fe998b6c1a1540a9057a6ebb530bd67942e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 23:19:00 +0900 Subject: [PATCH 015/282] feat: add src/vite-env.d.ts --- src/vite-env.d.ts | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/vite-env.d.ts diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +/// From c8387b5504f322625bc6a0bc6e3ce522e9aa304d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 23:19:02 +0900 Subject: [PATCH 016/282] feat: add tsconfig.app.json --- tsconfig.app.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 tsconfig.app.json diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..b7f2999 --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1 @@ +{"compilerOptions":{"target":"ES2022","useDefineForClassFields":true,"lib":["ES2022","DOM","DOM.Iterable"],"allowJs":false,"skipLibCheck":true,"esModuleInterop":true,"allowSyntheticDefaultImports":true,"strict":true,"forceConsistentCasingInFileNames":true,"module":"ESNext","moduleResolution":"Bundler","resolveJsonModule":true,"isolatedModules":true,"noEmit":true,"jsx":"react-jsx"},"include":["src"]} From 1c8bf32d054f6d516b36978dc9ce18781a08a546 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 23:19:03 +0900 Subject: [PATCH 017/282] feat: add tsconfig.json --- tsconfig.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 tsconfig.json diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ff0aea3 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1 @@ +{"files":[],"references":[{"path":"./tsconfig.app.json"},{"path":"./tsconfig.node.json"}]} From fa8a1428b92081d695d76c385e1825e921a0413a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 23:19:05 +0900 Subject: [PATCH 018/282] feat: add tsconfig.node.json --- tsconfig.node.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 tsconfig.node.json diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..39054ef --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1 @@ +{"compilerOptions":{"composite":true,"skipLibCheck":true,"module":"ESNext","moduleResolution":"Bundler","allowImportingTsExtensions":true,"noEmit":true},"include":["vite.config.ts"]} From edd5dfe3b8773feaa2addb4fb8d071d12f638dcd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 23:19:07 +0900 Subject: [PATCH 019/282] feat: add vite.config.ts --- vite.config.ts | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 vite.config.ts diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..93c6afd --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,4 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ plugins: [react()] }) From d1c5114141fcf3adc39a30ee26f1cd7dbe2a3401 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 00:14:05 +0900 Subject: [PATCH 020/282] fix: address initial review feedback --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d2045c3..2284020 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: actions/setup-node@v4 with: node-version: 22 From 4bf1bca9310365bde7ce7d26557caca5b3b40cd9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 00:14:14 +0900 Subject: [PATCH 021/282] fix: address initial review feedback --- index.html | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/index.html b/index.html index c41b5d7..548f60d 100644 --- a/index.html +++ b/index.html @@ -1 +1,13 @@ -
+ + + + + + + PolicyWeave + + +
+ + + From 68f3e191e87efa573b1080610b29e70410d4fa97 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 00:14:19 +0900 Subject: [PATCH 022/282] fix: address initial review feedback --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index f240b8e..205649c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "lucide-react": "latest", "react": "latest", "react-dom": "latest", - "typescript": "latest", + "typescript": "^6.0.3", "vite": "latest" }, "devDependencies": { From e255997d3f8849efb28bff3ce95462a3a25a8d7e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 00:14:24 +0900 Subject: [PATCH 023/282] fix: address initial review feedback --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index aa506bd..494a5d0 100644 --- a/package.json +++ b/package.json @@ -1 +1 @@ -{"name":"policyweave","private":true,"version":"0.1.0","type":"module","scripts":{"dev":"vite --host 0.0.0.0","build":"tsc -b && vite build","lint":"eslint .","test":"vitest run","test:watch":"vitest"},"dependencies":{"@vitejs/plugin-react":"latest","lucide-react":"latest","react":"latest","react-dom":"latest","typescript":"latest","vite":"latest"},"devDependencies":{"@eslint/js":"latest","@testing-library/jest-dom":"latest","@testing-library/react":"latest","@types/react":"latest","@types/react-dom":"latest","eslint":"latest","eslint-plugin-react-hooks":"latest","eslint-plugin-react-refresh":"latest","globals":"latest","jsdom":"latest","typescript-eslint":"^8.68.0","vitest":"latest"}} \ No newline at end of file +{"name":"policyweave","private":true,"version":"0.1.0","type":"module","scripts":{"dev":"vite --host 0.0.0.0","build":"tsc -b && vite build","lint":"eslint .","test":"vitest run","test:watch":"vitest"},"dependencies":{"@vitejs/plugin-react":"latest","lucide-react":"latest","react":"latest","react-dom":"latest","typescript":"^6.0.3","vite":"latest"},"devDependencies":{"@eslint/js":"latest","@testing-library/jest-dom":"latest","@testing-library/react":"latest","@types/react":"latest","@types/react-dom":"latest","eslint":"latest","eslint-plugin-react-hooks":"latest","eslint-plugin-react-refresh":"latest","globals":"latest","jsdom":"latest","typescript-eslint":"^8.68.0","vitest":"latest"}} From 080d98ff02c5061ebacaddfcc338653434de16c6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 00:14:29 +0900 Subject: [PATCH 024/282] fix: address initial review feedback --- src/styles.css | 155 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 152 insertions(+), 3 deletions(-) diff --git a/src/styles.css b/src/styles.css index 60cd32f..cfa5b0f 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1,3 +1,152 @@ -:root{font-family:Pretendard,"Noto Sans KR",system-ui,sans-serif;color:#202622;background:#f8f9f7;font-synthesis:none;--green:#174f35;--green-soft:#eaf1ec;--line:#d9ded9;--muted:#6d756f;--paper:#fffefb;--amber:#a85b00}*{box-sizing:border-box}body{margin:0;min-width:320px}button,input,select{font:inherit}button{cursor:pointer}.topbar{height:58px;display:flex;align-items:center;gap:18px;padding:0 20px;border-bottom:1px solid var(--line);background:white}.brand{font:700 27px Georgia,serif;color:var(--green);text-decoration:none;margin-right:10px}.document-name{display:flex;align-items:center;gap:7px;background:none;border:0;font-weight:700}.status{background:var(--green-soft);color:var(--green);padding:5px 10px;border-radius:4px;font-size:12px}.version,.save-state{font-size:12px;color:var(--muted)}.save-state{margin-left:auto;display:flex;align-items:center;gap:5px}.outline,.primary{min-height:38px;padding:0 16px;border-radius:4px;border:1px solid #cfd5d0;display:inline-flex;align-items:center;justify-content:center;gap:7px;background:white;color:#29302b}.primary{background:var(--green);color:white;border-color:var(--green);font-weight:700}.primary:disabled{background:#aeb8b1;border-color:#aeb8b1;cursor:not-allowed}.workspace{display:grid;grid-template-columns:240px minmax(520px,1fr) minmax(540px,1.05fr);height:calc(100vh - 152px);min-height:630px}.rail,.form-panel,.preview{min-width:0;background:white}.rail{border-right:1px solid var(--line);padding:22px 14px 14px;display:flex;flex-direction:column}.progress-copy{display:flex;justify-content:space-between;font-size:12px}.progress-copy span{color:var(--muted)}.progress{height:7px;background:#eceeec;border-radius:5px;margin:15px 0 18px;overflow:hidden}.progress i{display:block;height:100%;background:var(--green)}.rail ol{padding:0;margin:0;list-style:none}.rail li{position:relative}.rail li:not(:last-child)::after{content:"";position:absolute;left:13px;top:40px;height:24px;border-left:1px solid var(--line)}.rail li button{width:100%;display:flex;gap:13px;align-items:flex-start;text-align:left;border:0;background:transparent;padding:12px 5px;border-radius:4px}.rail li.active button{background:#f4f7f4;outline:1px solid #cad5cc}.step-number{width:26px;height:26px;flex:0 0 auto;display:flex;align-items:center;justify-content:center;border:1px solid #cbd1cc;border-radius:50%;font-size:12px}.done .step-number,.active .step-number{background:var(--green);border-color:var(--green);color:white}.rail b{display:block;font-size:13px;margin-top:3px}.rail small{display:block;color:var(--muted);font-size:11px;margin-top:5px}.rail .full{margin-top:auto;width:100%}.form-panel{overflow:auto;padding:24px 20px;border-right:1px solid var(--line)}.section-head h1{font-size:20px;margin:0 0 7px}.section-head p{font-size:12px;color:var(--muted);margin:0 0 17px}.notice{border:1px solid #cfd9e3;background:#f7fafc;padding:12px 14px;display:flex;flex-direction:column;gap:4px;font-size:12px}.notice span{color:var(--muted)}.form-panel h2{font-size:13px;margin:20px 0 9px}.table-head,.item-row{display:grid;grid-template-columns:1.05fr 1.55fr 92px;gap:14px;align-items:center}.table-head{border-bottom:1px solid var(--line);padding:0 8px 8px;color:#555f58;font-size:11px}.item{border-bottom:1px solid #e4e7e4}.item-row{min-height:42px;padding:6px 7px;font-size:12px}.check-label{display:flex;align-items:center;gap:8px}.check-label input{position:absolute;opacity:0}.box{width:17px;height:17px;border:1px solid #b8beba;display:flex;align-items:center;justify-content:center;color:transparent}.check-label input:focus-visible+.box{outline:3px solid #8eb59e;outline-offset:2px}.check-label input:checked+.box{background:var(--green);border-color:var(--green);color:white}.item-row>span{color:#5f6862}.select-wrap{position:relative}.select-wrap select{width:100%;height:32px;padding:0 28px 0 10px;border:1px solid #d1d6d2;background:white;appearance:none}.select-wrap svg{position:absolute;right:8px;top:9px;pointer-events:none}.conditional{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin:0 7px 10px;padding:10px;background:#f7f8f7;border:1px solid #e1e4e1}.conditional label{font-size:10px;color:var(--muted)}.conditional input{display:block;width:100%;height:32px;margin-top:5px;border:1px solid #d5d9d6;padding:0 8px}.form-actions{display:flex;justify-content:space-between;margin-top:18px}.preview{padding:14px 20px 18px;overflow:auto;background:#fafbfa}.preview-title,.meta{display:flex;align-items:center;gap:12px}.preview-title{justify-content:space-between}.preview-title h2{font-size:14px}.meta{font-size:11px;color:var(--muted);margin:8px 0 12px}.meta b,.warn-tag,.ok-tag{background:#eef1ee;padding:5px 8px;border-radius:4px;font-weight:500}.warn-tag{color:var(--amber);background:#fff4e7;border:1px solid #f3d5ae}.ok-tag{color:var(--green);background:var(--green-soft)}.paper{background:var(--paper);border:1px solid #d6d7d4;padding:23px 27px;min-height:500px;font-family:"Noto Serif KR",Batang,serif;box-shadow:0 2px 5px #00000008}.paper h2{text-align:center;font-size:20px;margin:0 0 20px}.paper h3{font-size:14px;margin:22px 0 8px}.paper p{font-size:11px;line-height:1.85}.paper table{border-collapse:collapse;width:100%;font-size:10px;margin:14px 0}.paper th,.paper td{border:1px solid #d4d4d0;padding:7px;text-align:left}.paper th{background:#f5f4f0}.missing{color:#9b4e00;background:#fff8ee}.document-warning{display:flex;align-items:flex-start;gap:10px;border:1px solid #edc791;background:#fff9ef;color:#874c08;padding:11px;font-family:Pretendard,sans-serif;font-size:11px}.document-warning span{display:flex;flex-direction:column;gap:3px}.wide{width:100%;margin-top:14px}.legal-note{display:block;text-align:center;color:var(--muted);margin-top:8px}.review-bar{height:94px;border-top:1px solid var(--line);background:white;padding:12px 20px;display:grid;grid-template-columns:230px 170px 170px minmax(260px,1fr);gap:15px;align-items:center;position:relative}.review-bar>div:first-child{display:flex;flex-direction:column;gap:5px}.review-bar small{color:var(--muted)}.review-stat{border-left:1px solid var(--line);padding-left:18px;display:flex;gap:12px;align-items:center;color:var(--green)}.review-stat span{font-size:11px;color:var(--muted)}.review-stat b{display:block;font-size:20px;color:#222}.review-stat.blocking{color:var(--amber)}.publish{height:44px}.review-bar output{position:absolute;right:22px;bottom:2px;font-size:10px;color:var(--amber)}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}:focus-visible{outline:3px solid #8eb59e;outline-offset:2px} -@media(max-width:1100px){.workspace{grid-template-columns:210px 1fr}.preview{display:none}.review-bar{grid-template-columns:1fr 150px 150px 240px}} -@media(max-width:720px){.topbar{height:auto;min-height:58px;flex-wrap:wrap;padding:10px 14px;gap:9px}.version,.save-state{display:none}.workspace{display:block;height:auto;min-height:0}.rail{border-right:0;border-bottom:1px solid var(--line);padding:12px}.rail ol{display:flex;overflow-x:auto;gap:6px}.rail li{min-width:145px}.rail li::after{display:none}.rail .full{display:none}.form-panel{border:0;padding:18px 14px}.table-head{display:none}.item-row{grid-template-columns:1fr 78px}.item-row>span{grid-row:2;grid-column:1/-1;padding-left:25px}.conditional{grid-template-columns:1fr}.review-bar{height:auto;grid-template-columns:1fr 1fr;padding:12px 14px}.review-bar>div:first-child{grid-column:1/-1}.publish{grid-column:1/-1}.document-name{max-width:190px;overflow:hidden;white-space:nowrap}.brand{font-size:23px}} +:root { + font-family: Pretendard, "Noto Sans KR", system-ui, sans-serif; + color: #202622; + background: #f8f9f7; + font-synthesis: none; + --green: #174f35; + --green-soft: #eaf1ec; + --line: #d9ded9; + --muted: #6d756f; + --paper: #fffefb; + --amber: #a85b00; +} +* { box-sizing: border-box; } +body { margin: 0; min-width: 320px; } +button, input, select, textarea { font: inherit; } +button { cursor: pointer; } +:focus-visible { outline: 3px solid #8eb59e; outline-offset: 2px; } +.topbar { + min-height: 58px; + display: flex; + align-items: center; + gap: 18px; + padding: 0 20px; + border-bottom: 1px solid var(--line); + background: white; +} +.brand { font: 700 27px Georgia, serif; color: var(--green); text-decoration: none; margin-right: 10px; } +.document-name { display: flex; align-items: center; gap: 7px; background: none; border: 0; font-weight: 700; } +.status { background: var(--green-soft); color: var(--green); padding: 5px 10px; border-radius: 4px; font-size: 12px; } +.version, .save-state { font-size: 12px; color: var(--muted); } +.save-state { margin-left: auto; display: flex; align-items: center; gap: 5px; } +.outline, .primary { + min-height: 38px; + padding: 0 16px; + border-radius: 4px; + border: 1px solid #cfd5d0; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 7px; + background: white; + color: #29302b; +} +.primary { background: var(--green); color: white; border-color: var(--green); font-weight: 700; } +.primary:disabled { background: #aeb8b1; border-color: #aeb8b1; cursor: not-allowed; } +.workspace { + display: grid; + grid-template-columns: 240px minmax(520px, 1fr) minmax(540px, 1.05fr); + height: calc(100vh - 152px); + min-height: 630px; +} +.rail, .form-panel, .preview { min-width: 0; background: white; } +.rail { border-right: 1px solid var(--line); padding: 22px 14px 14px; display: flex; flex-direction: column; } +.progress-copy { display: flex; justify-content: space-between; font-size: 12px; } +.progress-copy span { color: var(--muted); } +.progress { height: 7px; background: #eceeec; border-radius: 5px; margin: 15px 0 18px; overflow: hidden; } +.progress i { display: block; height: 100%; background: var(--green); } +.rail ol { padding: 0; margin: 0; list-style: none; } +.rail li { position: relative; } +.rail li:not(:last-child)::after { content: ""; position: absolute; left: 13px; top: 40px; height: 24px; border-left: 1px solid var(--line); } +.rail li button { width: 100%; display: flex; gap: 13px; align-items: flex-start; text-align: left; border: 0; background: transparent; padding: 12px 5px; border-radius: 4px; } +.rail li.active button { background: #f4f7f4; outline: 1px solid #cad5cc; } +.step-number { width: 26px; height: 26px; flex: 0 0 auto; display: flex; align-items: center; justify-content: center; border: 1px solid #cbd1cc; border-radius: 50%; font-size: 12px; } +.done .step-number, .active .step-number { background: var(--green); border-color: var(--green); color: white; } +.rail b { display: block; font-size: 13px; margin-top: 3px; } +.rail small { display: block; color: var(--muted); font-size: 11px; margin-top: 5px; } +.rail .full { margin-top: auto; width: 100%; } +.form-panel { overflow: auto; padding: 24px 20px; border-right: 1px solid var(--line); } +.section-head h1 { font-size: 20px; margin: 0 0 7px; } +.section-head p { font-size: 12px; color: var(--muted); margin: 0 0 17px; } +.notice { border: 1px solid #cfd9e3; background: #f7fafc; padding: 12px 14px; display: flex; flex-direction: column; gap: 4px; font-size: 12px; } +.notice span { color: var(--muted); } +.form-panel h2 { font-size: 13px; margin: 20px 0 9px; } +.table-head, .item-row { display: grid; grid-template-columns: 1.05fr 1.55fr 92px; gap: 14px; align-items: center; } +.table-head { border-bottom: 1px solid var(--line); padding: 0 8px 8px; color: #555f58; font-size: 11px; } +.item { border-bottom: 1px solid #e4e7e4; } +.item-row { min-height: 42px; padding: 6px 7px; font-size: 12px; } +.check-label { display: flex; align-items: center; gap: 8px; } +.check-label input { position: absolute; opacity: 0; } +.box { width: 17px; height: 17px; border: 1px solid #b8beba; display: flex; align-items: center; justify-content: center; color: transparent; } +.check-label input:focus-visible + .box { outline: 3px solid #8eb59e; outline-offset: 2px; } +.check-label input:checked + .box { background: var(--green); border-color: var(--green); color: white; } +.item-row > span { color: #5f6862; } +.select-wrap { position: relative; } +.select-wrap select { width: 100%; height: 32px; padding: 0 28px 0 10px; border: 1px solid #d1d6d2; background: white; appearance: none; } +.select-wrap svg { position: absolute; right: 8px; top: 9px; pointer-events: none; } +.conditional { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin: 0 7px 10px; padding: 10px; background: #f7f8f7; border: 1px solid #e1e4e1; } +.conditional label { font-size: 10px; color: var(--muted); } +.conditional input { display: block; width: 100%; height: 32px; margin-top: 5px; border: 1px solid #d5d9d6; padding: 0 8px; } +.field-stack { display: grid; gap: 18px; margin-top: 22px; } +.field-stack label { display: grid; gap: 7px; font-size: 13px; font-weight: 700; } +.field-stack textarea { min-height: 88px; resize: vertical; border: 1px solid #cfd5d0; padding: 10px; line-height: 1.5; } +.form-actions { display: flex; justify-content: space-between; margin-top: 18px; } +.preview { padding: 14px 20px 18px; overflow: auto; background: #fafbfa; } +.preview-title, .meta { display: flex; align-items: center; gap: 12px; } +.preview-title { justify-content: space-between; } +.preview-title h2 { font-size: 14px; } +.meta { font-size: 11px; color: var(--muted); margin: 8px 0 12px; } +.meta b, .warn-tag, .ok-tag { background: #eef1ee; padding: 5px 8px; border-radius: 4px; font-weight: 500; } +.warn-tag { color: var(--amber); background: #fff4e7; border: 1px solid #f3d5ae; } +.ok-tag { color: var(--green); background: var(--green-soft); } +.paper { background: var(--paper); border: 1px solid #d6d7d4; padding: 23px 27px; min-height: 500px; font-family: "Noto Serif KR", Batang, serif; box-shadow: 0 2px 5px #00000008; } +.paper h2 { text-align: center; font-size: 20px; margin: 0 0 20px; } +.paper h3 { font-size: 14px; margin: 22px 0 8px; } +.paper p { font-size: 11px; line-height: 1.85; } +.paper table { border-collapse: collapse; width: 100%; font-size: 10px; margin: 14px 0; } +.paper th, .paper td { border: 1px solid #d4d4d0; padding: 7px; text-align: left; } +.paper th { background: #f5f4f0; } +.missing { color: #9b4e00; background: #fff8ee; } +.document-warning { display: flex; align-items: flex-start; gap: 10px; border: 1px solid #edc791; background: #fff9ef; color: #874c08; padding: 11px; font-family: Pretendard, sans-serif; font-size: 11px; } +.document-warning span { display: flex; flex: 1; flex-direction: column; gap: 3px; } +.document-warning .outline { flex: 0 0 auto; } +.wide { width: 100%; margin-top: 14px; } +.legal-note { display: block; text-align: center; color: var(--muted); margin-top: 8px; } +.review-bar { min-height: 94px; border-top: 1px solid var(--line); background: white; padding: 12px 20px; display: grid; grid-template-columns: 230px 170px 170px minmax(260px, 1fr); gap: 15px; align-items: center; position: relative; } +.review-bar > div:first-child { display: flex; flex-direction: column; gap: 5px; } +.review-bar small { color: var(--muted); } +.review-stat { border-left: 1px solid var(--line); padding-left: 18px; display: flex; gap: 12px; align-items: center; color: var(--green); } +.review-stat span { font-size: 11px; color: var(--muted); } +.review-stat b { display: block; font-size: 20px; color: #222; } +.review-stat.blocking { color: var(--amber); } +.publish { height: 44px; } +.review-bar output { position: absolute; right: 22px; bottom: 2px; font-size: 10px; color: var(--amber); } +.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; } + +@media (max-width: 1300px) { + .workspace { grid-template-columns: 210px 1fr; } + .preview { display: none; } + .review-bar { grid-template-columns: 1fr 150px 150px 240px; } +} + +@media (max-width: 720px) { + .topbar { height: auto; flex-wrap: wrap; padding: 10px 14px; gap: 9px; } + .version, .save-state { display: none; } + .workspace { display: block; height: auto; min-height: 0; } + .rail { border-right: 0; border-bottom: 1px solid var(--line); padding: 12px; } + .rail ol { display: flex; overflow-x: auto; gap: 6px; } + .rail li { min-width: 145px; } + .rail li::after, .rail .full { display: none; } + .form-panel { border: 0; padding: 18px 14px; } + .preview { display: block; padding: 18px 14px; border-top: 1px solid var(--line); } + .table-head { display: none; } + .item-row { grid-template-columns: 1fr 78px; } + .item-row > span { grid-row: 2; grid-column: 1 / -1; padding-left: 25px; } + .conditional { grid-template-columns: 1fr; } + .review-bar { grid-template-columns: 1fr 1fr; padding: 12px 14px; } + .review-bar > div:first-child, .publish { grid-column: 1 / -1; } + .document-name { max-width: 190px; overflow: hidden; white-space: nowrap; } + .brand { font-size: 23px; } + .paper { padding: 18px 14px; } + .document-warning { flex-wrap: wrap; } +} From 84659b4a76a70215f7b0b28264c5c9a151c49846 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 00:14:33 +0900 Subject: [PATCH 025/282] test: cover seven-step navigation and warning routing --- src/App.test.tsx | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/App.test.tsx diff --git a/src/App.test.tsx b/src/App.test.tsx new file mode 100644 index 0000000..3d547b3 --- /dev/null +++ b/src/App.test.tsx @@ -0,0 +1,26 @@ +// @vitest-environment jsdom +import { cleanup, fireEvent, render } from '@testing-library/react' +import { afterEach, describe, expect, it } from 'vitest' +import App from './App' + +afterEach(cleanup) + +describe('policy editing workflow', () => { + it('일곱 단계의 입력 화면을 전환한다', () => { + const { container } = render() + const buttons = container.querySelectorAll('.rail li button') + const headings = ['1. 서비스 정보', '2. 수집 항목', '3. 처리 목적', '4. 보유 기간', '5. 제3자 제공', '6. 국외 이전', '7. 개인정보 보호 담당자'] + for (const [index, heading] of headings.entries()) { + fireEvent.click(buttons[index]) + expect(container.querySelector('.form-panel h1')?.textContent).toBe(heading) + } + }) + + it('미리보기 경고에서 처리 목적 단계로 이동한다', () => { + const { container } = render() + const phone = container.querySelectorAll('.check-label input')[2] + fireEvent.click(phone) + fireEvent.click(container.querySelector('.document-warning button')!) + expect(container.querySelector('.form-panel h1')?.textContent).toBe('3. 처리 목적') + }) +}) From 1d7aa0eaab91ff440795df74d70598dd099a909a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 00:16:25 +0900 Subject: [PATCH 026/282] ci: pin trusted actions by commit SHA --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2284020..ca856d5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,10 +9,10 @@ jobs: verify: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: persist-credentials: false - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: 22 cache: npm From 2483780c4a35c2e394824a1436fc4a123314e6ec Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 18:16:18 +0900 Subject: [PATCH 027/282] docs: add canonical DeepWiki badge --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index fd97f15..832c69d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # PolicyWeave +[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/ContextualWisdomLab/PolicyWeave) + 웹·앱 운영자가 실제 개인정보 처리 흐름을 입력하면, 누락과 모순을 표시하면서 개인정보처리방침 검토본을 만드는 로컬 우선 웹 앱입니다. > 생성 결과는 법률 자문이나 준법 보장이 아닙니다. 공개 전 개인정보보호책임자 또는 법률 전문가의 검토가 필요합니다. From 726432bd8d5a630d2adb9eecc181fbc9af8588f0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 19:20:26 +0900 Subject: [PATCH 028/282] test: cover responsive policy review layout --- src/styles.test.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/styles.test.ts diff --git a/src/styles.test.ts b/src/styles.test.ts new file mode 100644 index 0000000..c6cccbd --- /dev/null +++ b/src/styles.test.ts @@ -0,0 +1,23 @@ +import { readFileSync } from 'node:fs' +import { describe, expect, it } from 'vitest' + +const css = readFileSync(new URL('./styles.css', import.meta.url), 'utf8') + +function mediaBlock(maxWidth: number) { + const pattern = new RegExp(`@media \\(max-width: ${maxWidth}px\\) \\{([\\s\\S]*?)(?=\\n\\}\\n(?:\\n@media|$))`) + const match = css.match(pattern) + if (!match) throw new Error(`Missing max-width ${maxWidth}px media block`) + return match[1] +} + +describe('responsive review workspace CSS contract', () => { + it('keeps the policy preview available through tablet widths', () => { + const tablet = mediaBlock(1300) + expect(tablet).toContain('.preview { display: block;') + }) + + it('places mobile publish output in normal flow across the full review bar', () => { + const mobile = mediaBlock(720) + expect(mobile).toContain('.review-bar output { position: static; grid-column: 1 / -1;') + }) +}) From 85ea335ae1440f9e4ce3ec03642923590c493d09 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 19:21:35 +0900 Subject: [PATCH 029/282] fix: keep policy review usable on tablet and mobile --- src/styles.css | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/styles.css b/src/styles.css index cfa5b0f..3bc4f14 100644 --- a/src/styles.css +++ b/src/styles.css @@ -124,8 +124,8 @@ button { cursor: pointer; } .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; } @media (max-width: 1300px) { - .workspace { grid-template-columns: 210px 1fr; } - .preview { display: none; } + .workspace { grid-template-columns: 210px minmax(0, 1fr); height: auto; min-height: 630px; } + .preview { display: block; grid-column: 1 / -1; border-top: 1px solid var(--line); max-height: 720px; } .review-bar { grid-template-columns: 1fr 150px 150px 240px; } } @@ -138,13 +138,14 @@ button { cursor: pointer; } .rail li { min-width: 145px; } .rail li::after, .rail .full { display: none; } .form-panel { border: 0; padding: 18px 14px; } - .preview { display: block; padding: 18px 14px; border-top: 1px solid var(--line); } + .preview { display: block; padding: 18px 14px; border-top: 1px solid var(--line); max-height: none; } .table-head { display: none; } .item-row { grid-template-columns: 1fr 78px; } .item-row > span { grid-row: 2; grid-column: 1 / -1; padding-left: 25px; } .conditional { grid-template-columns: 1fr; } .review-bar { grid-template-columns: 1fr 1fr; padding: 12px 14px; } .review-bar > div:first-child, .publish { grid-column: 1 / -1; } + .review-bar output { position: static; grid-column: 1 / -1; text-align: right; } .document-name { max-width: 190px; overflow: hidden; white-space: nowrap; } .brand { font-size: 23px; } .paper { padding: 18px 14px; } From ac2dc84c801c86b1a53f4a6541690f8ba139908a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 19:30:49 +0900 Subject: [PATCH 030/282] docs: add PolicyWeave commercialization gap baseline --- docs/product-technical-gap-baseline.md | 43 ++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 docs/product-technical-gap-baseline.md diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md new file mode 100644 index 0000000..8c69b0e --- /dev/null +++ b/docs/product-technical-gap-baseline.md @@ -0,0 +1,43 @@ +# Product and technical gap baseline + +Last reconciled: 2026-09-01 + +This ledger records PolicyWeave's current commercialization gap from the active PR, product requirements, ADRs, implementation, review findings, and exact-head GitHub evidence. It separates implemented behavior from planned backend, legal-research, security, and release work. + +## Product responsibility + +PolicyWeave is a local-first policy-data workspace that helps a service operator structure actual personal-data processing facts, detect omissions or contradictions, and render a reviewable privacy-policy draft. It is not legal advice and does not claim compliance or certification. The structured policy model is the source of truth; rendered prose is a deterministic view over reviewed facts and rule/template versions. + +## Current exact-head baseline + +| Area | Evidence | Status | Commercialization gap | Next verification | +| --- | --- | --- | --- | --- | +| Guided authoring | Seven-step React workflow and PRD | MVP implemented | Several steps remain thin compared with a complete operational privacy inventory | Add validated retention, third-party transfer, international transfer, controller/contact, legal-basis and review-state contracts | +| Review workspace | Live preview, warning navigation, responsive review pane | Implemented on active PR head | No screenshot/browser matrix evidence yet | Add Playwright visual/interaction tests at desktop, tablet and mobile breakpoints | +| Accessibility | Keyboard focus and semantic controls are present | Partial | No automated WCAG 2.2 audit or screen-reader interaction evidence | Add axe/Playwright accessibility checks and manual keyboard/focus-order evidence | +| Policy model | Structured source-of-truth decision in ADR 0001 | Partial | No versioned persisted policy schema, migration contract or audit history | Define two-or-more-word persistence object names, revision model, UPSERT/idempotency and 3NF backend boundary before storage lands | +| Publication | UI exposes publication contract only | Planned | No authenticated immutable publication/version workflow | Implement review approval, immutable published revision, rollback/supersession and public URL lifecycle without implying legal approval | +| Legal/rule traceability | Product copy avoids compliance guarantee | Partial | Rules/templates are not yet pinned to requirement-level authoritative sources and effective dates | Add research/doctoring ledger and version every rule/template source separately from policy data | +| Security/privacy | Local-first reduces unnecessary transfer | Partial | Threat model, tenant/account model, secrets boundary, audit log and non-masking PII protection are absent | Define threat model and security baseline before any hosted persistence/publication feature | +| Tests | Workflow tests plus responsive CSS regression contract | Partial | Critical domain validation, edge cases, accessibility, browser and E2E coverage remain incomplete | Expand tests before backend/publication implementation; do not suppress warnings | +| Performance | Static Vite application | Unevidenced | No realistic browser/load baseline | Add browser performance budget; add k6 only when network/backend endpoints exist | +| CI | lint/test/build plus security/SAST workflows | Partial | Exact-head runs for the repaired head are queued | Require exact-head success before merge | + +## DDD/context map + +The core bounded context is **Policy Fact Authoring**. Ubiquitous language should distinguish `policy_revision`, `processing_purpose`, `collection_item`, `retention_rule`, `third_party_transfer`, `international_transfer`, `review_finding`, and `publication_revision`. Avoid generic one-word persistence object names. + +The aggregate root should be a minimal `policy_revision` transaction boundary rather than a monolithic account/service aggregate. Rendered policy text is a projection, not an authoritative entity. External legal/rule sources belong behind an anti-corruption boundary so a source update cannot silently reinterpret a previously published revision. + +## Buyer-visible release gates + +A publishable PolicyWeave release requires: complete input-to-warning navigation, deterministic rendering, explicit unresolved-review blocking, immutable reviewed publication revisions, accessible responsive interaction, authoritative source/version traceability, security/privacy threat-model evidence, and exact-head CI/security review. Marketing copy must describe assistance and reviewability rather than legal compliance guarantees. + +## Active gap order + +1. Merge PR #1 only after its current exact-head CI/security/SAST checks succeed. +2. Add browser-level responsive/accessibility screenshot evidence for the repaired tablet/mobile review workspace. +3. Define the versioned policy-data schema and audit/publication revision boundary before adding persistence. +4. Add authoritative legal/rule-source doctoring with effective-date/version traceability. +5. Implement secure publication/review workflow with immutable releases and explicit supersession. +6. Add hosted-service threat model, tenant isolation, audit evidence and realistic performance/load tests when network services exist. From 62b6f5ef9056a111c93a09ba40d205c98f42baef Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 19:40:47 +0900 Subject: [PATCH 031/282] docs: keep live CI state out of PolicyWeave baseline --- docs/product-technical-gap-baseline.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 8c69b0e..193b9be 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -2,18 +2,18 @@ Last reconciled: 2026-09-01 -This ledger records PolicyWeave's current commercialization gap from the active PR, product requirements, ADRs, implementation, review findings, and exact-head GitHub evidence. It separates implemented behavior from planned backend, legal-research, security, and release work. +This ledger records PolicyWeave's current commercialization gap from the active PR, product requirements, ADRs, implementation, review findings, and exact-head GitHub evidence. It separates implemented behavior from planned backend, legal-research, security, and release work. Live GitHub Check state is intentionally not persisted as `queued`/`running`/`passed` because it changes outside the repository; merge decisions must re-fetch the current PR head and required Checks. ## Product responsibility PolicyWeave is a local-first policy-data workspace that helps a service operator structure actual personal-data processing facts, detect omissions or contradictions, and render a reviewable privacy-policy draft. It is not legal advice and does not claim compliance or certification. The structured policy model is the source of truth; rendered prose is a deterministic view over reviewed facts and rule/template versions. -## Current exact-head baseline +## Current baseline | Area | Evidence | Status | Commercialization gap | Next verification | | --- | --- | --- | --- | --- | | Guided authoring | Seven-step React workflow and PRD | MVP implemented | Several steps remain thin compared with a complete operational privacy inventory | Add validated retention, third-party transfer, international transfer, controller/contact, legal-basis and review-state contracts | -| Review workspace | Live preview, warning navigation, responsive review pane | Implemented on active PR head | No screenshot/browser matrix evidence yet | Add Playwright visual/interaction tests at desktop, tablet and mobile breakpoints | +| Review workspace | Live preview, warning navigation, responsive review pane | Implemented on active PR branch | No screenshot/browser matrix evidence yet | Add Playwright visual/interaction tests at desktop, tablet and mobile breakpoints | | Accessibility | Keyboard focus and semantic controls are present | Partial | No automated WCAG 2.2 audit or screen-reader interaction evidence | Add axe/Playwright accessibility checks and manual keyboard/focus-order evidence | | Policy model | Structured source-of-truth decision in ADR 0001 | Partial | No versioned persisted policy schema, migration contract or audit history | Define two-or-more-word persistence object names, revision model, UPSERT/idempotency and 3NF backend boundary before storage lands | | Publication | UI exposes publication contract only | Planned | No authenticated immutable publication/version workflow | Implement review approval, immutable published revision, rollback/supersession and public URL lifecycle without implying legal approval | @@ -21,7 +21,7 @@ PolicyWeave is a local-first policy-data workspace that helps a service operator | Security/privacy | Local-first reduces unnecessary transfer | Partial | Threat model, tenant/account model, secrets boundary, audit log and non-masking PII protection are absent | Define threat model and security baseline before any hosted persistence/publication feature | | Tests | Workflow tests plus responsive CSS regression contract | Partial | Critical domain validation, edge cases, accessibility, browser and E2E coverage remain incomplete | Expand tests before backend/publication implementation; do not suppress warnings | | Performance | Static Vite application | Unevidenced | No realistic browser/load baseline | Add browser performance budget; add k6 only when network/backend endpoints exist | -| CI | lint/test/build plus security/SAST workflows | Partial | Exact-head runs for the repaired head are queued | Require exact-head success before merge | +| CI/security merge gate | lint/test/build plus security/SAST workflows | Live external gate | A committed CI status would become stale immediately; predecessor-head success is never merge evidence | At merge time, re-fetch the exact current head and require all protected checks/reviews to be successful | ## DDD/context map @@ -35,7 +35,7 @@ A publishable PolicyWeave release requires: complete input-to-warning navigation ## Active gap order -1. Merge PR #1 only after its current exact-head CI/security/SAST checks succeed. +1. Merge PR #1 only after its live current-head CI/security/SAST checks and review gates succeed. 2. Add browser-level responsive/accessibility screenshot evidence for the repaired tablet/mobile review workspace. 3. Define the versioned policy-data schema and audit/publication revision boundary before adding persistence. 4. Add authoritative legal/rule-source doctoring with effective-date/version traceability. From 4795f99beb51a95cbc7393a2bb0b9600389776d8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 20:22:49 +0900 Subject: [PATCH 032/282] test: lock seven-step PolicyWeave navigation contract --- src/App.test.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/App.test.tsx b/src/App.test.tsx index 3d547b3..17ed927 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -23,4 +23,20 @@ describe('policy editing workflow', () => { fireEvent.click(container.querySelector('.document-warning button')!) expect(container.querySelector('.form-panel h1')?.textContent).toBe('3. 처리 목적') }) + + it('처리 목적을 보완하면 발행 차단을 해제한다', () => { + const { container } = render() + const phone = container.querySelectorAll('.check-label input')[2] + fireEvent.click(phone) + fireEvent.click(container.querySelector('.document-warning button')!) + + const purpose = container.querySelector('input[name="purpose-phone"]')! + fireEvent.change(purpose, { target: { value: '본인 확인 및 알림 발송' } }) + + expect(container.querySelector('.review-stat.blocking b')?.textContent).toBe('0건') + const publish = container.querySelector('.publish')! + expect(publish.disabled).toBe(false) + fireEvent.click(publish) + expect(container.querySelector('output')?.textContent).toContain('백엔드 연결이 필요합니다') + }) }) From 61f5bac24d0271f382668872140a7ef4a3463db4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 20:23:49 +0900 Subject: [PATCH 033/282] fix: make all seven PolicyWeave steps editable --- src/App.tsx | 148 +++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 128 insertions(+), 20 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index b18f015..13fc2ac 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,20 +2,77 @@ import { useMemo, useState } from 'react' import { AlertTriangle, Check, ChevronDown, ExternalLink, FileText, Link, Save } from 'lucide-react' import { getReview, initialItems, PolicyItem, steps } from './policy' +type DraftFacts = { + serviceName: string + serviceUrl: string + retentionPeriod: string + thirdPartyRecipient: string + thirdPartyPurpose: string + internationalCountry: string + internationalRecipient: string + privacyOfficerName: string + privacyOfficerEmail: string +} + +type FactField = { + key: keyof DraftFacts + label: string + placeholder: string + type?: 'text' | 'email' | 'url' +} + +const initialFacts: DraftFacts = { + serviceName: '', + serviceUrl: '', + retentionPeriod: '', + thirdPartyRecipient: '', + thirdPartyPurpose: '', + internationalCountry: '', + internationalRecipient: '', + privacyOfficerName: '', + privacyOfficerEmail: '', +} + function StepRail({ current, setCurrent }: { current: number; setCurrent: (step: number) => void }) { return } +function StepActions({ current, setCurrent }: { current: number; setCurrent: (step: number) => void }) { + return
+ + +
+} + +function FactStep({ current, title, description, fields, facts, setFacts, setCurrent }: { + current: number + title: string + description: string + fields: FactField[] + facts: DraftFacts + setFacts: (facts: DraftFacts) => void + setCurrent: (step: number) => void +}) { + const update = (key: keyof DraftFacts, value: string) => setFacts({ ...facts, [key]: value }) + return
+

{current}. {title}

{description}

+
사실 기반 입력운영 중인 서비스와 계약·처리 흐름에서 확인한 사실만 입력하세요. 확인되지 않은 내용은 비워 두고 검토 대상으로 남깁니다.
+

확인 정보

+
{fields.map((field) => )}
+ +
+} + function CollectionForm({ items, setItems, setCurrent }: { items: PolicyItem[]; setItems: (items: PolicyItem[]) => void; setCurrent: (step: number) => void }) { const update = (id: string, patch: Partial) => setItems(items.map((item) => item.id === id ? { ...item, ...patch } : item)) return
@@ -25,30 +82,80 @@ function CollectionForm({ items, setItems, setCurrent }: { items: PolicyItem[];
수집 항목설명수집 여부
{items.map((item) =>
- + {item.description} - +
- {item.enabled && !item.purpose &&
} + {item.enabled && !item.purpose &&
}
)}
-
+ +
+} + +function PurposeForm({ items, setItems, setCurrent }: { items: PolicyItem[]; setItems: (items: PolicyItem[]) => void; setCurrent: (step: number) => void }) { + const enabled = items.filter((item) => item.enabled) + const updatePurpose = (id: string, purpose: string) => setItems(items.map((item) => item.id === id ? { ...item, purpose } : item)) + return
+

3. 처리 목적

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

+
검토 원칙포괄적인 문구를 새로 만들기보다 실제 기능·업무 목적과 연결하세요.
+

항목별 처리 목적

+ {enabled.length === 0 ?

수집 항목 단계에서 실제 수집 항목을 먼저 선택하세요.

:
{enabled.map((item) =>
)}
} +
} -function DocumentPreview({ items }: { items: PolicyItem[] }) { +function EditingPanel({ current, items, setItems, facts, setFacts, setCurrent }: { + current: number + items: PolicyItem[] + setItems: (items: PolicyItem[]) => void + facts: DraftFacts + setFacts: (facts: DraftFacts) => void + setCurrent: (step: number) => void +}) { + if (current === 2) return + if (current === 3) return + + const stepConfig: Record = { + 1: { title: '서비스 정보', description: '개인정보처리방침이 적용되는 서비스와 공개 위치를 확인합니다.', fields: [ + { key: 'serviceName', label: '서비스 이름', placeholder: '예: PolicyWeave' }, + { key: 'serviceUrl', label: '서비스 URL', placeholder: 'https://example.com', type: 'url' }, + ] }, + 4: { title: '보유 기간', description: '수집한 개인정보를 언제까지 보유하는지 운영 사실과 근거에 맞춰 기록합니다.', fields: [ + { key: 'retentionPeriod', label: '대표 보유 기간 또는 종료 조건', placeholder: '예: 회원 탈퇴 시까지, 법정 보존 항목은 별도 기간' }, + ] }, + 5: { title: '제3자 제공', description: '제3자에게 개인정보를 제공하는 경우 제공받는 자와 목적을 확인합니다.', fields: [ + { key: 'thirdPartyRecipient', label: '제공받는 자', placeholder: '없으면 비워 두고 검토에서 확인' }, + { key: 'thirdPartyPurpose', label: '제공 목적', placeholder: '실제 제공 목적' }, + ] }, + 6: { title: '국외 이전', description: '개인정보의 국외 이전이 있는 경우 국가와 수령자를 확인합니다.', fields: [ + { key: 'internationalCountry', label: '이전 국가', placeholder: '예: 일본' }, + { key: 'internationalRecipient', label: '국외 수령자', placeholder: '실제 수령 법인 또는 서비스' }, + ] }, + 7: { title: '개인정보 보호 담당자', description: '개인정보 관련 문의를 받을 책임자와 연락 채널을 기록합니다.', fields: [ + { key: 'privacyOfficerName', label: '담당자 또는 담당 부서', placeholder: '예: 개인정보보호 담당' }, + { key: 'privacyOfficerEmail', label: '연락 이메일', placeholder: 'privacy@example.com', type: 'email' }, + ] }, + } + const config = stepConfig[current] ?? stepConfig[1] + return +} + +function DocumentPreview({ items, facts, setCurrent }: { items: PolicyItem[]; facts: DraftFacts; setCurrent: (step: number) => void }) { const review = useMemo(() => getReview(items), [items]) - return
-

개인정보처리방침 미리보기

+ return
+

개인정보처리방침 미리보기

근거 법령 개인정보 보호법{review.blocking.length ? `검토 필요 ${review.blocking.length}` : '필수 확인 완료'}버전 0.1.0
-

개인정보처리방침 (검토본)

-

회사는 이용자의 개인정보를 중요하게 여기며, 관련 법령을 준수하기 위해 다음과 같이 개인정보 처리에 관한 사항을 알립니다.

+

{facts.serviceName || '개인정보처리방침'} (검토본)

+

{facts.serviceName || '서비스 운영자'}는 이용자의 개인정보를 중요하게 여기며, 확인된 실제 처리 사실을 바탕으로 다음 사항을 검토합니다.

제1조 (개인정보의 처리 목적)

-

회사는 아래 목적을 위해 개인정보를 처리합니다. 목적이 변경되는 경우 필요한 절차를 거쳐 별도로 알립니다.

- {review.enabled.map((item) => )}
수집 항목처리 목적근거 확인
{item.label}{item.purpose || '처리 목적 입력 필요'}제15조 검토
- {review.blocking.length > 0 &&
공개 전 확인{review.blocking.map((item) => item.label).join(', ')}의 처리 목적이 입력되지 않았습니다.
} -

제2조 (처리 및 보유 기간)

각 개인정보의 보유 기간은 다음 작성 단계에서 입력한 법정 보존 기간과 이용 목적 달성 시점을 기준으로 정합니다.

-

제3조 (제3자 제공)

제3자 제공 여부와 제공받는 자, 목적, 항목 및 보유 기간은 확인된 운영 사실에 따라 별도 표에 반영합니다.

+

아래 목적은 작성자가 확인한 운영 사실을 기준으로 표시됩니다.

+ {review.enabled.map((item) => )}
수집 항목처리 목적검토 상태
{item.label}{item.purpose || '처리 목적 입력 필요'}{item.purpose ? '입력됨' : '확인 필요'}
+ {review.blocking.length > 0 &&
공개 전 확인{review.blocking.map((item) => item.label).join(', ')}의 처리 목적이 입력되지 않았습니다.
} +

제2조 (처리 및 보유 기간)

{facts.retentionPeriod || '보유 기간 단계에서 확인한 운영 기준을 입력해야 합니다.'}

+

제3조 (제3자 제공)

{facts.thirdPartyRecipient ? `${facts.thirdPartyRecipient}에 ${facts.thirdPartyPurpose || '확인 중인 목적'}으로 제공하는 흐름을 검토 중입니다.` : '제3자 제공 여부를 확인하는 단계가 남아 있습니다.'}

+

제4조 (국외 이전)

{facts.internationalCountry || facts.internationalRecipient ? `${facts.internationalCountry || '국가 확인 필요'} · ${facts.internationalRecipient || '수령자 확인 필요'}` : '국외 이전 여부를 확인하는 단계가 남아 있습니다.'}

+

개인정보 보호 문의

{facts.privacyOfficerName || '담당자 확인 필요'} · {facts.privacyOfficerEmail || '연락처 확인 필요'}

생성된 문서는 법률 자문이 아닙니다. 공개 전 책임자의 검토가 필요합니다. @@ -57,13 +164,14 @@ function DocumentPreview({ items }: { items: PolicyItem[] }) { export default function App() { const [items, setItems] = useState(initialItems) + const [facts, setFacts] = useState(initialFacts) const [current, setCurrent] = useState(2) const review = useMemo(() => getReview(items), [items]) const [message, setMessage] = useState('') function publish() { setMessage(review.blocking.length ? '필수 확인 항목을 먼저 입력하세요.' : '공개 URL 발행에는 저장소 백엔드 연결이 필요합니다.') } return
-
PolicyWeave작성 중버전 0.1.0 (임시저장) 자동 저장됨
-
-
검토 요약확인을 마친 뒤 공개 URL을 발행하세요.
필수 확인 {review.blocking.length}건
권장 검토 {review.recommended.length}건
{message}
+
PolicyWeave작성 중버전 0.1.0 (임시저장) 브라우저 작업 중
+
+
검토 요약확인을 마친 뒤 공개 URL 발행 준비 상태를 확인하세요.
필수 확인 {review.blocking.length}건
권장 검토 {review.recommended.length}건
{message}
} From cd47648808239c8c1233fed10671a4035de013e4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 20:28:51 +0900 Subject: [PATCH 034/282] docs: establish commercialization architecture and traceability --- AGENTS.md | 19 +++++++ ARCHITECTURE.md | 36 ++++++++++++ CHANGELOG.md | 22 ++++++++ CLAUDE.md | 7 +++ .../ADR-0002-seven-step-authoring-workflow.md | 18 ++++++ docs/SECURITY.md | 31 ++++++++++ docs/TRD.md | 37 ++++++++++++ docs/product-technical-gap-baseline.md | 56 ++++++++++--------- docs/research-traceability.md | 31 ++++++++++ 9 files changed, 230 insertions(+), 27 deletions(-) create mode 100644 AGENTS.md create mode 100644 ARCHITECTURE.md create mode 100644 CHANGELOG.md create mode 100644 CLAUDE.md create mode 100644 docs/ADR-0002-seven-step-authoring-workflow.md create mode 100644 docs/SECURITY.md create mode 100644 docs/TRD.md create mode 100644 docs/research-traceability.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..7642482 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,19 @@ +# AGENTS.md + +## Product responsibility +PolicyWeave is a local-first privacy-policy fact-authoring workspace. It structures facts supplied by a service operator, detects missing or contradictory inputs, and renders a review draft. It does not provide legal advice, certify compliance, or infer facts that the operator has not established. + +## Development contract +- Treat structured policy facts as the source of truth; rendered policy prose is a deterministic projection. +- Preserve the seven authoring steps in `docs/PRD.md`: service information, collection items, purposes, retention, third-party transfer, international transfer, and privacy contact. +- Behavior changes require regression or edge-case tests first. Do not resolve a review finding until the exact current head proves the finding obsolete or fixed. +- Re-fetch the PR head before every commit/push. Never force-push or weaken branch/ruleset protections to merge. +- Keep `ARCHITECTURE.md`, `docs/TRD.md`, ADRs, `docs/research-traceability.md`, `CHANGELOG.md`, and `docs/product-technical-gap-baseline.md` aligned with implementation. +- Do not encode legal conclusions from memory. Every legal/rule/template decision needs an authoritative source, effective date, source revision, and implementation/test trace. +- Do not commit real personal or institutional names in tests or documentation. Production must not consume synthetic demo data. +- Keep persistence objects semantically named with at least two words and `snake_case` unless a framework contract requires another convention. Avoid generic named persistence objects such as a standalone `id` table/collection. +- Hosted persistence/publication must be introduced only behind explicit tenant, authorization, audit, encryption, immutable revision, and supersession contracts. +- GitHub Actions dependencies stay SHA pinned and checkout credentials must not persist. + +## Verification +The minimum exact-head gate is `npm run lint`, `npm test`, and `npm run build`, plus every live organization-required workflow, independent approval, and resolved review thread. Queued, skipped, predecessor-head, or stale results are not passing evidence. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..0b0dcc2 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,36 @@ +# Architecture + +## Product boundary +PolicyWeave owns the authoring and review of structured privacy-processing facts and the deterministic generation of a review draft. It does not own legal advice, identity-provider data, payment processing, or a customer's source systems. Hosted publication and durable storage are future supporting capabilities, not authority to invent or reinterpret processing facts. + +## Domain-driven design +The core subdomain is **Policy Fact Authoring**. Supporting subdomains are **Review & Publication** and **Legal Source Registry**. Browser/storage frameworks, authentication infrastructure, observability, and deployment are generic subdomains. + +### Bounded contexts +- **Policy Fact Authoring**: captures `service_profile`, `collection_item`, `processing_purpose`, `retention_rule`, `third_party_transfer`, `international_transfer`, and `privacy_contact` facts. +- **Review & Publication**: derives `review_finding`, controls `policy_revision` review state, and will create immutable `publication_revision` artifacts only after explicit authorization. +- **Legal Source Registry**: versions authoritative sources, effective dates, rule/template revisions, and citations. It is an anti-corruption layer between changing external law/guidance and already-published revisions. + +### Context map +`Policy Fact Authoring -> Review & Publication` is a customer/supplier relationship through a versioned policy-fact contract. `Legal Source Registry -> Review & Publication` supplies versioned rule evidence; source updates cannot silently mutate historical policy revisions. External customer systems remain behind adapters and must not be queried or mutated through hidden coupling. + +## Ubiquitous language and model +A future `policy_revision` is the minimal aggregate root and transaction boundary. `collection_item`, `processing_purpose`, `retention_rule`, `third_party_transfer`, `international_transfer`, and `privacy_contact` are revision-owned facts/value objects unless later evidence requires independent lifecycles. `review_finding` is derived evidence. `publication_revision` is an immutable release receipt, not a mutable policy row. Rendered prose is a projection and never the source of truth. + +Core invariants: +1. A selected collection item cannot pass review without a non-empty processing purpose. +2. Review findings navigate to the fact that caused them. +3. Publication must never upgrade an unreviewed or incomplete draft to a reviewed/authoritative state. +4. A published revision remains reproducible from its policy facts plus rule/template/source versions. +5. 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. `src/policy.ts` owns the current collection-item review rule; `src/App.tsx` owns browser orchestration and deterministic preview rendering. + +## Persistence boundary (planned, not implemented) +Use relational 3NF by default. Named database/schema/persistence objects use at least two semantic words and `snake_case`, for example `policy_revision`, `collection_item`, `processing_purpose`, `review_finding`, `publication_revision`, and `legal_source_revision`. Item-level UPSERTs must declare their natural/idempotency key and conflict behavior. Publication is append-only/immutable with explicit supersession; writes across unrelated aggregates must not share a transaction merely for convenience. + +Separate write-side draft commands from read-side rendered/review projections once hosted traffic justifies it. Account for revision hot spots and optimistic/constrained writes before adding collaborative editing. Keep source/customer integrations behind ACLs; do not form a shared kernel with unrelated ContextualWisdomLab products without demonstrated reuse. + +## Deployment and operability direction +The browser-only MVP needs no service mesh. A hosted backend should be compose-deployable across Docker/Podman/Colima before Kubernetes migration, expose asynchronous/non-blocking request handling, and add realistic k6 evidence for network surfaces before latency claims. No code may depend on an optional `close_connection` instance attribute existing unless the adapter contract guarantees it. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d500bd8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,22 @@ +# Changelog + +All notable product changes are recorded here. PolicyWeave is pre-release; entries describe the active commercialization branch and do not imply a published compliance product. + +## Unreleased + +### Added +- Seven-step authoring workflow with distinct editing surfaces for service information, collection items, processing purposes, retention, third-party transfer, international transfer, and privacy contact. +- Warning-to-source navigation for missing processing purposes. +- Regression coverage for all seven step routes, warning navigation, and clearing the publication blocker after a missing purpose is supplied. +- Product/technical gap ledger, architecture, technical requirements, security baseline, and legal-source traceability. + +### Changed +- Review preview now reflects verified authoring facts instead of presenting the collection step as the entire workflow. +- Responsive review behavior and mobile publication feedback were repaired during PR review. +- GitHub Actions checkout dependencies are SHA pinned and credentials are not persisted in the working tree. + +### Not yet shipped +- Durable versioned policy persistence and audit history. +- Authenticated immutable publication revisions and public URL lifecycle. +- Browser-matrix/accessibility evidence and hosted endpoint load testing. +- Legal/rule engine backed by versioned authoritative requirements. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..bff26e2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,7 @@ +# CLAUDE.md + +Follow `AGENTS.md` as the repository-wide operating contract. + +Before changing code, read `docs/PRD.md`, `ARCHITECTURE.md`, the applicable ADRs, `docs/TRD.md`, `docs/research-traceability.md`, and `docs/product-technical-gap-baseline.md`. Preserve PolicyWeave's boundary: verified operator facts in, deterministic reviewable policy projection out; no legal-advice or compliance-guarantee claims. + +For behavior changes, add or strengthen tests before production code. Reconcile documentation and the product-gap ledger on the same branch. Use the exact current PR head for reviews, checks, and merge decisions, and never bypass governance to compensate for a failing or unassigned check. diff --git a/docs/ADR-0002-seven-step-authoring-workflow.md b/docs/ADR-0002-seven-step-authoring-workflow.md new file mode 100644 index 0000000..8c18d8d --- /dev/null +++ b/docs/ADR-0002-seven-step-authoring-workflow.md @@ -0,0 +1,18 @@ +# ADR-0002: Seven-step authoring is an explicit domain workflow + +Status: Accepted +Date: 2026-09-01 + +## Context +The PRD defines seven authoring stages, but an early implementation changed only the step rail while always rendering the collection-item editor. This made the buyer-facing workflow appear complete while most facts were not editable and review findings could not lead the operator to the responsible input. + +## Decision +Each PRD stage is an explicit editable state of the Policy Fact Authoring context: service information, collection items, processing purposes, retention, third-party transfer, international transfer, and privacy contact. The UI router must render a corresponding editor for the selected stage. Review findings carry enough domain context to navigate to the responsible stage. + +The workflow is not a legal state machine. Completion of a UI step does not mean legal sufficiency. Legal/rule completeness is determined separately by deterministic review rules bound to versioned authoritative source evidence. + +## Consequences +- Navigation and review-to-source behavior are regression-tested. +- The preview remains a projection over structured facts and cannot become an independent source of truth. +- Future persistence stores facts and revision/review state rather than serialized page prose. +- Additional legal validations can grow without redefining the seven buyer-facing authoring responsibilities. diff --git a/docs/SECURITY.md b/docs/SECURITY.md new file mode 100644 index 0000000..1058a1d --- /dev/null +++ b/docs/SECURITY.md @@ -0,0 +1,31 @@ +# Security and Privacy Baseline + +## Current exposure +PolicyWeave is currently a client-only, local-first authoring workspace. There is no production account system, server-side persistence, publication service, or secret-bearing provider integration in this repository. This limits current remote data exposure but does not make the product compliant or production-ready. + +## Assets and trust boundaries +Protected assets include policy facts, contact details, processing descriptions, legal/rule source receipts, review findings, audit events, and future publication artifacts. Browser state is trusted only for the active local editing session. Any future API, datastore, identity provider, legal-source feed, or customer system is an explicit external boundary and requires an ACL/adapter. + +## Threats to address before hosted launch +- Cross-tenant access or confused-deputy publication. +- Unauthorized draft mutation or publication. +- Loss of provenance/source-version evidence. +- Silent mutation of already-published revisions. +- Injection through user-entered policy text, imported source metadata, or rendered markup. +- Credential leakage in CI or runtime. +- Over-broad logging of PII or policy content. +- Retention/deletion behavior inconsistent with the operator's configured lifecycle. +- Supply-chain compromise in npm/GitHub Actions dependencies. + +## Required controls +1. Authenticate users and authorize every tenant/resource/purpose operation server-side before hosted persistence. +2. Encrypt PII and policy content in transit and at rest; use non-masking protections when masking would break legitimate work, while minimizing disclosure in logs/telemetry. +3. Keep immutable audit events for security- and publication-relevant actions with actor, tenant, revision, action, result, and timestamp. +4. Publication operates on an explicit reviewed revision and creates an immutable publication receipt with digest/version evidence. +5. Secrets never enter client bundles or repository content. CI checkout credentials remain non-persistent and Actions are SHA pinned. +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. + +## 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. diff --git a/docs/TRD.md b/docs/TRD.md new file mode 100644 index 0000000..e7dfb6c --- /dev/null +++ b/docs/TRD.md @@ -0,0 +1,37 @@ +# Technical Requirements Document + +## Scope +This TRD covers the pre-release PolicyWeave browser workspace and the contracts that must exist before hosted persistence/publication is added. + +## Current runtime +- React + TypeScript + Vite browser application. +- Structured authoring state is in browser memory; no production database or backend exists. +- `src/policy.ts` contains deterministic review logic for selected collection items and missing processing purposes. +- `src/App.tsx` provides the seven-step authoring flow, review navigation, and deterministic preview projection. +- The current CI contract is lint, Vitest, and TypeScript/Vite build plus live organization-required security/review workflows. + +## Functional contracts +1. Every PRD step must route to an editable surface; selecting a step cannot change only the rail indicator. +2. Selected collection items expose their collection mode and processing-purpose contract. +3. A selected item with blank/whitespace purpose produces a blocking review finding. +4. A blocking finding links to the responsible editing step. +5. Preview text is derived from current structured facts; it does not become an independently editable authority. +6. UI copy distinguishes a review draft from legal advice, certification, or a compliance guarantee. + +## Quality contracts +- Touched production behavior requires regression and edge-case tests first. +- Do not suppress deprecation warnings to pass CI. +- Browser/accessibility validation must eventually include keyboard/focus order, WCAG 2.2 automated checks, responsive desktop/tablet/mobile interactions, and screenshot evidence. +- 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. + +## 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. + +A publication command must operate on a specific reviewed `policy_revision` and create a new immutable `publication_revision`. It must fail closed when blocking findings, incompatible rule/template versions, missing source receipts, or missing authorization exist. Publication never mutates foreign customer databases. + +## External boundaries +Legal/rule data enters through a Legal Source Registry ACL with source identity, effective date, retrieval/version evidence, and an implementation mapping. Customer/application integrations require their own explicit adapters. If future LLM assistance is justified for drafting/explanation, calls must route through `ContextualWisdomLab/contextual-orchestrator`; deterministic validation and publication authority remain outside the model. + +## Verification state +Only exact-current-head checks count. Queued, cancelled, skipped-required, or predecessor-head runs are not evidence of passing. Normal integration requires the live ruleset's independent approval and thread-resolution requirements; no administrative bypass is part of this TRD. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 193b9be..fbabc9a 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -2,42 +2,44 @@ Last reconciled: 2026-09-01 -This ledger records PolicyWeave's current commercialization gap from the active PR, product requirements, ADRs, implementation, review findings, and exact-head GitHub evidence. It separates implemented behavior from planned backend, legal-research, security, and release work. Live GitHub Check state is intentionally not persisted as `queued`/`running`/`passed` because it changes outside the repository; merge decisions must re-fetch the current PR head and required Checks. +This ledger records PolicyWeave's commercialization gap from PRD/ADR/TRD/architecture, implementation, authoritative legal-source evidence, review findings, and live GitHub state. Live Check conclusions are deliberately not committed because they change outside the repository; merge decisions must re-fetch the exact current PR head and live ruleset. ## Product responsibility +PolicyWeave is a local-first policy-fact workspace that helps a service operator structure actual personal-data processing facts, detect omissions or contradictions, and render a reviewable privacy-policy draft. It is not legal advice and does not claim compliance or certification. Structured facts are the source of truth; rendered prose is a deterministic projection over facts plus future versioned rule/template evidence. -PolicyWeave is a local-first policy-data workspace that helps a service operator structure actual personal-data processing facts, detect omissions or contradictions, and render a reviewable privacy-policy draft. It is not legal advice and does not claim compliance or certification. The structured policy model is the source of truth; rendered prose is a deterministic view over reviewed facts and rule/template versions. +## Exact-head implementation evidence +The seven-step routing defect and warning-navigation defect were reproduced on predecessor head `62b6f5ef9056a111c93a09ba40d205c98f42baef`: `App.tsx` always rendered the collection editor while regression tests expected all seven editors and a warning-navigation action. Tests were strengthened first at `4795f99beb51a95cbc7393a2bb0b9600389776d8`, then production routing/review behavior was repaired at `61f5bac24d0271f382668872140a7ef4a3463db4`. This documentation reconciliation changes no production behavior; live checks/reviews must be reacquired for the resulting current head. ## Current baseline -| Area | Evidence | Status | Commercialization gap | Next verification | -| --- | --- | --- | --- | --- | -| Guided authoring | Seven-step React workflow and PRD | MVP implemented | Several steps remain thin compared with a complete operational privacy inventory | Add validated retention, third-party transfer, international transfer, controller/contact, legal-basis and review-state contracts | -| Review workspace | Live preview, warning navigation, responsive review pane | Implemented on active PR branch | No screenshot/browser matrix evidence yet | Add Playwright visual/interaction tests at desktop, tablet and mobile breakpoints | -| Accessibility | Keyboard focus and semantic controls are present | Partial | No automated WCAG 2.2 audit or screen-reader interaction evidence | Add axe/Playwright accessibility checks and manual keyboard/focus-order evidence | -| Policy model | Structured source-of-truth decision in ADR 0001 | Partial | No versioned persisted policy schema, migration contract or audit history | Define two-or-more-word persistence object names, revision model, UPSERT/idempotency and 3NF backend boundary before storage lands | -| Publication | UI exposes publication contract only | Planned | No authenticated immutable publication/version workflow | Implement review approval, immutable published revision, rollback/supersession and public URL lifecycle without implying legal approval | -| Legal/rule traceability | Product copy avoids compliance guarantee | Partial | Rules/templates are not yet pinned to requirement-level authoritative sources and effective dates | Add research/doctoring ledger and version every rule/template source separately from policy data | -| Security/privacy | Local-first reduces unnecessary transfer | Partial | Threat model, tenant/account model, secrets boundary, audit log and non-masking PII protection are absent | Define threat model and security baseline before any hosted persistence/publication feature | -| Tests | Workflow tests plus responsive CSS regression contract | Partial | Critical domain validation, edge cases, accessibility, browser and E2E coverage remain incomplete | Expand tests before backend/publication implementation; do not suppress warnings | -| Performance | Static Vite application | Unevidenced | No realistic browser/load baseline | Add browser performance budget; add k6 only when network/backend endpoints exist | -| CI/security merge gate | lint/test/build plus security/SAST workflows | Live external gate | A committed CI status would become stale immediately; predecessor-head success is never merge evidence | At merge time, re-fetch the exact current head and require all protected checks/reviews to be successful | +| Area | Evidence | Status | Commercialization gap | Owner/action | Next verification | +| --- | --- | --- | --- | --- | --- | +| Guided authoring | PRD, ADR-0002, seven routed editors in `src/App.tsx` | Functional foundation | Retention, third-party transfer, international transfer and contact steps currently capture thin facts rather than requirement-complete domain contracts | Policy Fact Authoring: add typed facts and deterministic validations from authoritative requirements | Unit/integration tests for valid, missing, contradictory and not-applicable cases | +| Processing-purpose review | `src/policy.ts`, `src/App.test.tsx` | Repaired | Current blocker covers missing purpose only | Review & Publication: extend evidence-bound review rule set without heuristic scoring | Exact-head tests plus rule/source traceability | +| Review workspace | Live preview, warning-to-source navigation, responsive CSS fixes | Implemented foundation | No browser screenshot matrix or realistic interaction evidence | UX: add Playwright + accessibility harness | Desktop/tablet/mobile screenshots, keyboard/focus and action-edge verification | +| Accessibility | Semantic controls, focus-visible behavior, keyboard-operable step navigation | Partial | No automated WCAG 2.2 evidence or screen-reader contract | UX: add axe/Playwright and manual interaction record | WCAG audit and browser matrix | +| Policy model | ADR-0001, ARCHITECTURE, TRD | Designed, browser-memory only | No versioned durable schema, migration, audit history, UPSERT/idempotency or 3NF implementation | Platform: define `policy_revision` schema and item-level conflict contracts before storage | Schema/contract tests and migration round trip | +| Publication | UI communicates backend requirement; architecture defines immutable `publication_revision` direction | Planned | No authenticated review approval, immutable publication, supersession or public URL lifecycle | Review & Publication: implement only after persistence/security entry criteria | Authorization, replay/digest, supersession and rollback tests | +| Legal/rule traceability | `docs/research-traceability.md`; official PIPA/PIPC source register | Source baseline established | No article/section-level requirement mapping or versioned legal-rule engine | Legal Source Registry: map each implemented rule to source/effective date/revision | Fixtures across current and future-effective rule-set snapshots | +| Security/privacy | `docs/SECURITY.md`; local-first current runtime | Baseline documented | Hosted tenant model, secrets boundary, encryption/key handling, audit and incident/retention evidence absent | Platform/Security: threat-model hosted boundary before backend | Security tests and exact-head org scans | +| Tests | Vitest workflow, routing/navigation/blocker regressions, responsive CSS regression | Improved | Browser E2E, accessibility and domain edge cases remain incomplete; 100% coverage not evidenced | Test Engineering: expand behavior-first tests | Exact-head coverage and browser evidence | +| Performance | Static Vite client | Unevidenced | No page performance or network/load evidence | Operability: baseline browser performance; add k6 only with backend | Realistic measurements before latency claims | +| CI/security merge gate | SHA-pinned checkout plus repo and central required workflows | External live gate | Current hosted Actions jobs have been observed queued; queue state is non-passing and may be a shared control-plane admission issue | Control plane: diagnose runner/admission policy without weakening rules | Exact-head assigned jobs must execute; independent approval and threads required | ## DDD/context map +Core subdomain: **Policy Fact Authoring**. Supporting: **Review & Publication**, **Legal Source Registry**. Generic infrastructure remains outside domain authority. `Policy Fact Authoring -> Review & Publication` supplies a versioned fact contract. `Legal Source Registry -> Review & Publication` supplies versioned source/rule evidence through an ACL. Customer/source-system integrations remain adapters and may not mutate foreign systems through hidden coupling. -The core bounded context is **Policy Fact Authoring**. Ubiquitous language should distinguish `policy_revision`, `processing_purpose`, `collection_item`, `retention_rule`, `third_party_transfer`, `international_transfer`, `review_finding`, and `publication_revision`. Avoid generic one-word persistence object names. +Ubiquitous language: `policy_revision`, `service_profile`, `collection_item`, `processing_purpose`, `retention_rule`, `third_party_transfer`, `international_transfer`, `privacy_contact`, `review_finding`, `legal_source_revision`, `publication_revision`. -The aggregate root should be a minimal `policy_revision` transaction boundary rather than a monolithic account/service aggregate. Rendered policy text is a projection, not an authoritative entity. External legal/rule sources belong behind an anti-corruption boundary so a source update cannot silently reinterpret a previously published revision. +The future aggregate root is the minimal `policy_revision` boundary. Rendered prose is a read projection. `publication_revision` is immutable and explicitly superseded rather than updated. Relational persistence is 3NF by default; named persistence objects use at least two semantic words and `snake_case`, and item-level UPSERT/idempotency semantics must be explicit. ## Buyer-visible release gates - -A publishable PolicyWeave release requires: complete input-to-warning navigation, deterministic rendering, explicit unresolved-review blocking, immutable reviewed publication revisions, accessible responsive interaction, authoritative source/version traceability, security/privacy threat-model evidence, and exact-head CI/security review. Marketing copy must describe assistance and reviewability rather than legal compliance guarantees. - -## Active gap order - -1. Merge PR #1 only after its live current-head CI/security/SAST checks and review gates succeed. -2. Add browser-level responsive/accessibility screenshot evidence for the repaired tablet/mobile review workspace. -3. Define the versioned policy-data schema and audit/publication revision boundary before adding persistence. -4. Add authoritative legal/rule-source doctoring with effective-date/version traceability. -5. Implement secure publication/review workflow with immutable releases and explicit supersession. -6. Add hosted-service threat model, tenant isolation, audit evidence and realistic performance/load tests when network services exist. +A publishable release requires complete fact-to-warning navigation, deterministic rendering, explicit unresolved-review blocking, immutable reviewed publication revisions, accessible responsive interaction, authoritative source/version traceability, hosted security/privacy evidence, and exact-head CI/security/independent review. Buyer-facing wording describes assistance and reviewability, never a compliance guarantee. + +## Active commercialization order +1. Reacquire exact-current-head CI/security/SAST/central workflow execution and independent approval; merge PR #1 only through ordinary protection. +2. Add browser-level responsive/accessibility evidence for the repaired seven-step workflow. +3. Convert retention/transfer/contact placeholders into typed domain facts and source-backed deterministic validations. +4. Define and test the versioned `policy_revision` persistence/audit schema and explicit item-level UPSERT contracts. +5. Implement secure review/publication with immutable releases and explicit supersession. +6. Add hosted tenant isolation, audit/incident evidence, compose deployment, and realistic performance/load tests when network services exist. diff --git a/docs/research-traceability.md b/docs/research-traceability.md new file mode 100644 index 0000000..feb43ce --- /dev/null +++ b/docs/research-traceability.md @@ -0,0 +1,31 @@ +# Legal and research traceability + +Last reconciled: 2026-09-01 + +PolicyWeave treats legal and regulatory material as versioned evidence, not as implicit knowledge embedded in UI copy. The current MVP does not yet implement a legal/rule engine; this ledger therefore records source authority and the product capability that still needs to be derived and tested. + +## Authoritative source register + +| Source | Current evidence on 2026-09-01 | Product use | Implementation status | +| --- | --- | --- | --- | +| Republic of Korea, Personal Information Protection Act (개인정보 보호법), National Law Information Center | The current act shown by the official search service is effective 2025-10-02; an enacted amendment is scheduled to take effect 2026-09-11. https://www.law.go.kr/unSc.do?query=%EA%B0%9C%EC%9D%B8%EC%A0%95%EB%B3%B4%EB%B3%B4%ED%98%B8%EB%B2%95 | Statutory authority/effective-date anchor for rule derivation | Registry only; no article-level rules are encoded yet | +| Republic of Korea, Enforcement Decree of the Personal Information Protection Act (개인정보 보호법 시행령), National Law Information Center | Official search service lists the decree effective 2026-08-20. Same official search URL as above. | Subordinate-rule authority/effective-date anchor | Registry only; no decree-level rules are encoded yet | +| Personal Information Protection Commission. (2026, April 23). 개인정보 처리방침 작성지침(2026.4. 개정) [Guideline for writing privacy policies, April 2026 revision]. | PIPC marks this as the current guide. https://pipc.go.kr/np/cop/bbs/selectBoardList.do?bbsId=BS217&mCode=D010030000 | Authoring/review guidance and template requirement discovery | Source registered; requirement-by-requirement mapping still required | + +## APA 7 references + +Personal Information Protection Commission. (2026, April 23). *개인정보 처리방침 작성지침(2026.4. 개정)* [Guideline for writing privacy policies, April 2026 revision]. https://pipc.go.kr/np/cop/bbs/selectBoardList.do?bbsId=BS217&mCode=D010030000 + +Republic of Korea. (2025). *개인정보 보호법* [Personal Information Protection Act] (Act No. 20897, effective October 2, 2025; future amendment Act No. 21445 effective September 11, 2026). National Law Information Center. https://www.law.go.kr/unSc.do?query=%EA%B0%9C%EC%9D%B8%EC%A0%95%EB%B3%B4%EB%B3%B4%ED%98%B8%EB%B2%95 + +Republic of Korea. (2026). *개인정보 보호법 시행령* [Enforcement Decree of the Personal Information Protection Act] (Presidential Decree No. 36121, effective August 20, 2026). National Law Information Center. https://www.law.go.kr/unSc.do?query=%EA%B0%9C%EC%9D%B8%EC%A0%95%EB%B3%B4%EB%B3%B4%ED%98%B8%EB%B2%95 + +## Traceability rules +1. Every future legal/rule/template implementation records source identifier, article/section where applicable, effective date, source revision or retrieval digest, implementation symbol, and tests. +2. Future-effective law is never silently treated as already-effective law. A rule set declares the jurisdiction/effective-date snapshot it evaluates. +3. Guidance is not promoted to statute, and product copy does not claim that following a guide guarantees compliance. +4. A source revision creates an explicit evaluation event against existing draft/published revisions; it does not mutate historical publication evidence. +5. LLM output, if later used to explain or propose wording, is never an authoritative legal source and cannot change review/publication state. + +## Current gap +The seven-step workspace now captures the product's intended fact categories, but retention, third-party provision, international transfer, contact/controller information, and legal-basis review still need requirement-level mappings to the authoritative register, deterministic validation, and regression fixtures before PolicyWeave can claim those steps are legally complete. From ec1aead4484018d52ce8bd5bbab04c517d42870d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 20:31:18 +0900 Subject: [PATCH 035/282] test: cover whitespace processing-purpose review --- src/policy.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/policy.test.ts b/src/policy.test.ts index 7259e24..9cc1313 100644 --- a/src/policy.test.ts +++ b/src/policy.test.ts @@ -5,8 +5,19 @@ describe('policy review', () => { it('includes only selected collection items', () => { expect(getReview(initialItems).enabled.map((item) => item.id)).toEqual(['name', 'email', 'usage']) }) + it('blocks publishing when a selected item has no purpose', () => { const items = initialItems.map((item) => item.id === 'phone' ? { ...item, enabled: true } : item) expect(getReview(items).blocking.map((item) => item.id)).toEqual(['phone']) }) + + it('treats a whitespace-only purpose as missing', () => { + const items = initialItems.map((item) => item.id === 'phone' ? { ...item, enabled: true, purpose: ' \t ' } : item) + expect(getReview(items).blocking.map((item) => item.id)).toEqual(['phone']) + }) + + it('accepts a purpose that contains non-whitespace content', () => { + const items = initialItems.map((item) => item.id === 'phone' ? { ...item, enabled: true, purpose: ' 본인 확인 ' } : item) + expect(getReview(items).blocking.map((item) => item.id)).not.toContain('phone') + }) }) From b35e18f3831ea5a257b0a9397c08aa73af8c3f52 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 20:32:15 +0900 Subject: [PATCH 036/282] test: separate collection paths from processing purposes --- src/App.test.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/App.test.tsx b/src/App.test.tsx index 17ed927..7008c87 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -16,6 +16,20 @@ describe('policy editing workflow', () => { } }) + it('수집 단계에서는 선택 항목의 수집 경로를 항상 편집하고 처리 목적은 다음 단계에서 편집한다', () => { + const { container } = render() + expect(container.querySelectorAll('input[placeholder="예: 회원가입 화면"]')).toHaveLength(3) + expect(container.querySelector('input[placeholder="예: 본인 확인, 알림 발송"]')).toBeNull() + + const phone = container.querySelectorAll('.check-label input')[2] + fireEvent.click(phone) + expect(container.querySelectorAll('input[placeholder="예: 회원가입 화면"]')).toHaveLength(4) + expect(container.querySelector('input[placeholder="예: 본인 확인, 알림 발송"]')).toBeNull() + + fireEvent.click(container.querySelectorAll('.rail li button')[2]) + expect(container.querySelector('input[name="purpose-phone"]')).not.toBeNull() + }) + it('미리보기 경고에서 처리 목적 단계로 이동한다', () => { const { container } = render() const phone = container.querySelectorAll('.check-label input')[2] From 87d37b9a219ff50013f9ac46c3b1f11b52a94467 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 20:33:13 +0900 Subject: [PATCH 037/282] fix: keep collection-path editing in the collection step --- src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/App.tsx b/src/App.tsx index 13fc2ac..e0cee58 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -86,7 +86,7 @@ function CollectionForm({ items, setItems, setCurrent }: { items: PolicyItem[]; {item.description} - {item.enabled && !item.purpose &&
} + {item.enabled &&
} )} From aa72570c32cfc2f4605a78f87aa4d3b3a8633232 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 20:34:35 +0900 Subject: [PATCH 038/282] docs: record review-driven authoring repair --- docs/product-technical-gap-baseline.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index fbabc9a..8ab7503 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -8,23 +8,25 @@ This ledger records PolicyWeave's commercialization gap from PRD/ADR/TRD/archite PolicyWeave is a local-first policy-fact workspace that helps a service operator structure actual personal-data processing facts, detect omissions or contradictions, and render a reviewable privacy-policy draft. It is not legal advice and does not claim compliance or certification. Structured facts are the source of truth; rendered prose is a deterministic projection over facts plus future versioned rule/template evidence. ## Exact-head implementation evidence -The seven-step routing defect and warning-navigation defect were reproduced on predecessor head `62b6f5ef9056a111c93a09ba40d205c98f42baef`: `App.tsx` always rendered the collection editor while regression tests expected all seven editors and a warning-navigation action. Tests were strengthened first at `4795f99beb51a95cbc7393a2bb0b9600389776d8`, then production routing/review behavior was repaired at `61f5bac24d0271f382668872140a7ef4a3463db4`. This documentation reconciliation changes no production behavior; live checks/reviews must be reacquired for the resulting current head. +The seven-step routing defect and warning-navigation defect were reproduced on predecessor head `62b6f5ef9056a111c93a09ba40d205c98f42baef`: `App.tsx` always rendered the collection editor while regression tests expected all seven editors and a warning-navigation action. Tests were strengthened first at `4795f99beb51a95cbc7393a2bb0b9600389776d8`, then production routing/review behavior was repaired at `61f5bac24d0271f382668872140a7ef4a3463db4`. + +A later exact-head review found that collection-path editing was incorrectly coupled to an empty processing purpose: enabled items with an existing purpose lost the collection-path field, while entering a purpose made both fields disappear. The regression was added first at `b35e18f3831ea5a257b0a9397c08aa73af8c3f52`; `CollectionForm` was then repaired at `87d37b9a219ff50013f9ac46c3b1f11b52a94467` so collection-path editing remains in step 2 and purpose editing remains in step 3. `ec1aead4484018d52ce8bd5bbab04c517d42870d` also locks the existing whitespace-only-purpose blocking contract. The review thread was resolved only after the repaired code was re-fetched at `87d37b9a219ff50013f9ac46c3b1f11b52a94467`. This documentation reconciliation changes no production behavior; live checks/reviews must be reacquired for the resulting current head. ## Current baseline | Area | Evidence | Status | Commercialization gap | Owner/action | Next verification | | --- | --- | --- | --- | --- | --- | -| Guided authoring | PRD, ADR-0002, seven routed editors in `src/App.tsx` | Functional foundation | Retention, third-party transfer, international transfer and contact steps currently capture thin facts rather than requirement-complete domain contracts | Policy Fact Authoring: add typed facts and deterministic validations from authoritative requirements | Unit/integration tests for valid, missing, contradictory and not-applicable cases | -| Processing-purpose review | `src/policy.ts`, `src/App.test.tsx` | Repaired | Current blocker covers missing purpose only | Review & Publication: extend evidence-bound review rule set without heuristic scoring | Exact-head tests plus rule/source traceability | +| Guided authoring | PRD, ADR-0002, seven routed editors in `src/App.tsx`; step-2 collection-path/step-3 purpose separation regression | Functional foundation | Retention, third-party transfer, international transfer and contact steps currently capture thin facts rather than requirement-complete domain contracts | Policy Fact Authoring: add typed facts and deterministic validations from authoritative requirements | Unit/integration tests for valid, missing, contradictory and not-applicable cases | +| Processing-purpose review | `src/policy.ts`, `src/policy.test.ts`, `src/App.test.tsx` | Repaired | Missing and whitespace-only purposes block review, but the rule set covers only purpose completeness | Review & Publication: extend evidence-bound review rules without heuristic scoring | Exact-head tests plus rule/source traceability | | Review workspace | Live preview, warning-to-source navigation, responsive CSS fixes | Implemented foundation | No browser screenshot matrix or realistic interaction evidence | UX: add Playwright + accessibility harness | Desktop/tablet/mobile screenshots, keyboard/focus and action-edge verification | | Accessibility | Semantic controls, focus-visible behavior, keyboard-operable step navigation | Partial | No automated WCAG 2.2 evidence or screen-reader contract | UX: add axe/Playwright and manual interaction record | WCAG audit and browser matrix | | Policy model | ADR-0001, ARCHITECTURE, TRD | Designed, browser-memory only | No versioned durable schema, migration, audit history, UPSERT/idempotency or 3NF implementation | Platform: define `policy_revision` schema and item-level conflict contracts before storage | Schema/contract tests and migration round trip | | Publication | UI communicates backend requirement; architecture defines immutable `publication_revision` direction | Planned | No authenticated review approval, immutable publication, supersession or public URL lifecycle | Review & Publication: implement only after persistence/security entry criteria | Authorization, replay/digest, supersession and rollback tests | | Legal/rule traceability | `docs/research-traceability.md`; official PIPA/PIPC source register | Source baseline established | No article/section-level requirement mapping or versioned legal-rule engine | Legal Source Registry: map each implemented rule to source/effective date/revision | Fixtures across current and future-effective rule-set snapshots | | Security/privacy | `docs/SECURITY.md`; local-first current runtime | Baseline documented | Hosted tenant model, secrets boundary, encryption/key handling, audit and incident/retention evidence absent | Platform/Security: threat-model hosted boundary before backend | Security tests and exact-head org scans | -| Tests | Vitest workflow, routing/navigation/blocker regressions, responsive CSS regression | Improved | Browser E2E, accessibility and domain edge cases remain incomplete; 100% coverage not evidenced | Test Engineering: expand behavior-first tests | Exact-head coverage and browser evidence | +| Tests | Vitest workflow, routing/navigation/blocker regressions, whitespace-purpose edge case, collection-path/purpose separation, responsive CSS regression | Improved | Browser E2E, accessibility and broader domain edge cases remain incomplete; 100% coverage not evidenced | Test Engineering: expand behavior-first tests | Exact-head coverage and browser evidence | | Performance | Static Vite client | Unevidenced | No page performance or network/load evidence | Operability: baseline browser performance; add k6 only with backend | Realistic measurements before latency claims | -| CI/security merge gate | SHA-pinned checkout plus repo and central required workflows | External live gate | Current hosted Actions jobs have been observed queued; queue state is non-passing and may be a shared control-plane admission issue | Control plane: diagnose runner/admission policy without weakening rules | Exact-head assigned jobs must execute; independent approval and threads required | +| CI/security merge gate | SHA-pinned checkout plus repo and central required workflows | External live gate | Hosted Actions jobs have been observed queued without runner assignment; queue state is non-passing and is consistent with a shared control-plane admission issue | Control plane: diagnose runner/admission policy without weakening rules | Exact-head assigned jobs must execute; independent approval and threads required | ## DDD/context map Core subdomain: **Policy Fact Authoring**. Supporting: **Review & Publication**, **Legal Source Registry**. Generic infrastructure remains outside domain authority. `Policy Fact Authoring -> Review & Publication` supplies a versioned fact contract. `Legal Source Registry -> Review & Publication` supplies versioned source/rule evidence through an ACL. Customer/source-system integrations remain adapters and may not mutate foreign systems through hidden coupling. From 4e631cefccde07eed731b4214d2b413fc989f52f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 20:34:51 +0900 Subject: [PATCH 039/282] docs: record collection-path workflow fix --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d500bd8..48b6266 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,11 @@ All notable product changes are recorded here. PolicyWeave is pre-release; entri ### Added - Seven-step authoring workflow with distinct editing surfaces for service information, collection items, processing purposes, retention, third-party transfer, international transfer, and privacy contact. - Warning-to-source navigation for missing processing purposes. -- Regression coverage for all seven step routes, warning navigation, and clearing the publication blocker after a missing purpose is supplied. +- Regression coverage for all seven step routes, warning navigation, clearing the publication blocker after a missing purpose is supplied, whitespace-only purposes, and collection-path/purpose step separation. - Product/technical gap ledger, architecture, technical requirements, security baseline, and legal-source traceability. ### Changed +- Collection-path editing now remains available for every selected item in the collection step, while processing-purpose editing stays in its dedicated purpose step. - Review preview now reflects verified authoring facts instead of presenting the collection step as the entire workflow. - Responsive review behavior and mobile publication feedback were repaired during PR review. - GitHub Actions checkout dependencies are SHA pinned and credentials are not persisted in the working tree. From ef5f0db691d0d34a66f8bfdcbce3480906aea0ae Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 21:19:43 +0900 Subject: [PATCH 040/282] docs: add public documentation landing source --- docs/index.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 docs/index.md diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..35c1409 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,31 @@ +# PolicyWeave + +PolicyWeave is a local-first privacy-policy fact-authoring workspace for web and app operators. It structures facts the operator has established, highlights missing or contradictory inputs, and renders a deterministic privacy-policy review draft without presenting that draft as legal advice or a compliance certification. + +## Start here + +- [README](../README.md) — local setup, current product scope, and validation commands. +- [Product requirements](PRD.md) — the seven-step authoring workflow and buyer-facing acceptance criteria. +- [Technical requirements](TRD.md) — implementation and quality requirements for the product boundary. +- [Architecture](../ARCHITECTURE.md) — system responsibilities, dependency direction, and trust boundaries. +- [ADR 0001](ADR-0001-policy-as-data.md) — the policy-as-data decision that makes structured operator facts authoritative over rendered prose. +- [Research and legal traceability](research-traceability.md) — authoritative-source, effective-date, and implementation/test traceability for legal and policy decisions. +- [Product and technical gap baseline](product-technical-gap-baseline.md) — current commercialization gaps and evidence status. +- [Security](../SECURITY.md) — security policy and reporting boundary. +- [Changelog](../CHANGELOG.md) — repository change history. +- [Repository releases](https://github.com/ContextualWisdomLab/PolicyWeave/releases) — published release records when they exist. +- [Ask DeepWiki](https://deepwiki.com/ContextualWisdomLab/PolicyWeave) — repository-oriented Q&A and navigation. + +## Product boundary + +PolicyWeave owns structured privacy-processing facts, completeness and contradiction review, and deterministic draft rendering. The operator remains responsible for establishing the underlying facts and obtaining the human or legal review appropriate to publication. The product must not infer unknown facts, turn template text into source-of-truth data, or represent generated prose as a compliance guarantee. + +The repository distinguishes protected-main behavior from active pull-request work and planned commercialization gaps. This landing page does not promote branch-local work, a successful build, or an unpublished artifact to shipped or released status. + +## Onboarding + +Start with the README and PRD, then use the architecture and ADR to understand why structured facts remain authoritative. Contributors should follow `AGENTS.md` and keep product, technical, research-traceability, changelog, and gap-baseline documents aligned with implementation and exact-head evidence. + +## Publication status + +This file is the reviewed source for a future repository documentation landing page. GitHub Pages is not considered published until repository settings, deployment state, and the live HTTPS content are independently verified. From 5b3a56e6242eff8a0fea8048c41b3b139127ef42 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 05:14:38 +0900 Subject: [PATCH 041/282] docs: make PolicyWeave README product-first --- README.md | 117 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 104 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 832c69d..ed94fe3 100644 --- a/README.md +++ b/README.md @@ -2,26 +2,117 @@ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/ContextualWisdomLab/PolicyWeave) -웹·앱 운영자가 실제 개인정보 처리 흐름을 입력하면, 누락과 모순을 표시하면서 개인정보처리방침 검토본을 만드는 로컬 우선 웹 앱입니다. +**실제 개인정보 처리 사실을 구조화해, 누락과 모순을 찾고 검토 가능한 개인정보처리방침 초안을 만드는 로컬 우선 워크스페이스입니다.** -> 생성 결과는 법률 자문이나 준법 보장이 아닙니다. 공개 전 개인정보보호책임자 또는 법률 전문가의 검토가 필요합니다. +PolicyWeave는 범용 법률 문구를 임의로 채우는 생성기가 아닙니다. 운영자가 확인한 서비스 사실을 단계별로 입력하면 수집 항목, 처리 목적, 보유 기간, 제3자 제공, 국외 이전과 담당자 정보를 서로 연결하고, 공개 전에 다시 확인해야 할 지점을 보여 줍니다. -## 실행 +> PolicyWeave의 출력은 법률 자문, 준법 보장 또는 인증이 아닙니다. 공개 전 개인정보보호책임자 또는 적절한 법률 전문가의 검토가 필요합니다. + +## 누구를 위한 제품인가 + +| 사용자 | PolicyWeave가 돕는 일 | +| --- | --- | +| 웹·앱 운영자 | 실제 서비스의 개인정보 처리 흐름을 빠짐없이 정리하고 검토본을 만든다. | +| 개인정보보호 책임자 | 입력 사실, 누락 경고와 검토 필요 항목을 한 흐름에서 확인한다. | +| 제품·개발 팀 | 정책 문구보다 먼저 구조화된 처리 사실과 책임 경계를 합의한다. | +| 통합·유지보수 담당자 | 정책 데이터 모델, 제품 요구사항과 결정 기록을 저장소에서 추적한다. | + +## 현재 할 수 있는 일 + +현재 소스는 다음 7단계 작성 흐름을 구현합니다. + +1. 서비스 정보 +2. 수집 항목 +3. 처리 목적 +4. 보유 기간 +5. 제3자 제공 +6. 국외 이전 +7. 개인정보 보호 담당자 + +선택한 수집 항목에는 수집 경로와 처리 목적을 별도로 기록할 수 있습니다. 필수 사실이 없거나 처리 목적이 비어 있으면 검토본이 이를 숨기지 않고 차단 또는 검토 경고로 드러내며, 경고에서 원인이 있는 입력 단계로 돌아갈 수 있습니다. 작성 내용은 실시간 검토본에 반영되고 모바일·키보드 사용도 고려합니다. + +## 빠른 시작 + +현재 제품은 저장소에서 실행하는 초기 개발 버전입니다. 패키지 메타데이터는 `0.1.0`이지만 GitHub에 게시된 릴리스는 아직 없습니다. ```bash -npm install +npm ci npm run dev ``` -검증은 `npm run lint`, `npm test`, `npm run build`로 수행합니다. +프로덕션 번들 및 검증: + +```bash +npm run lint +npm test +npm run build +``` + +`npm ci`는 체크인된 `package-lock.json`을 사용합니다. 이 저장소는 `private: true` 패키지이므로 npm 배포물을 제품 릴리스로 간주하지 않습니다. + +## 제품 경계 + +PolicyWeave가 책임지는 것은 **운영자가 입력한 개인정보 처리 사실의 구조화, 검토 지원, 결정적인 검토본 생성**입니다. + +PolicyWeave는 다음을 권위 있게 판단하지 않습니다. + +- 실제 서비스가 어떤 SDK·쿠키·데이터를 사용하는지 자동 추측 +- 특정 처리의 법적 적법성에 대한 최종 판단 +- 동의가 필요한지 여부를 모든 상황에 동일하게 결정 +- 법률 자문, 규제기관 판단, 인증 또는 감사 의견 대체 +- 아직 구현되지 않은 정적 공개 URL·호스팅 백엔드를 현재 기능처럼 표시 + +향후 공개·버전 이력·영속화가 추가되더라도, 사람이 확인한 처리 사실과 책임자 검토가 자동 생성 문구보다 우선합니다. + +## 동작 방식 + +```text +운영자가 확인한 처리 사실 + │ + ▼ +7단계 구조화 편집 + │ + ├── 필수 사실/관계 검증 + ├── 차단 오류와 검토 경고 + └── 원인 단계로 이동 + │ + ▼ +실시간 개인정보처리방침 검토본 + │ + ▼ +책임자·법률 검토 후 별도 공개 결정 +``` + +현재 브라우저 애플리케이션은 작성과 검토 경험에 집중합니다. PRD에 기록된 정적 공개 URL, 버전 영속화와 배포 계약은 후속 제품 작업이며 현재 구현으로 과장하지 않습니다. + +## 개인정보와 보안 관점 + +PolicyWeave는 개인정보처리방침을 만들기 위해 불필요한 실제 개인정보를 수집하도록 설계하지 않습니다. 정책 작성에는 서비스의 **처리 범주와 운영 사실**을 입력하고, 사람·고객의 실제 민감 값을 데모나 문서에 복사하지 않는 것이 기본 원칙입니다. + +보안 및 신뢰 경계는 [`SECURITY.md`](SECURITY.md)와 기술 문서에서 관리합니다. 새 저장·호스팅·외부 연동 기능은 별도의 인증·권한·감사·tenant 경계를 갖추기 전까지 현재 기능으로 취급하지 않습니다. + +## 문서 + +- [제품 요구사항](docs/PRD.md) — 사용자 문제, MVP, 비목표와 성공 기준 +- [기술 요구사항](docs/TRD.md) — 구현 및 품질 계약 +- [아키텍처](ARCHITECTURE.md) — 제품 책임과 기술 경계 +- [ADR-0001: Policy as Data](docs/ADR-0001-policy-as-data.md) — 핵심 설계 결정 +- [제품·기술 Gap baseline](docs/product-technical-gap-baseline.md) — 아직 닫히지 않은 상용화 Gap과 완료 증거 +- [공개 문서 홈](docs/index.md) — 저장소 문서 탐색 시작점 +- [변경 이력](CHANGELOG.md) + +## 기여와 검증 + +동작을 바꾸는 변경은 해당 사용자 흐름의 회귀 테스트와 문서 계약을 함께 갱신해야 합니다. 작업 전 [`AGENTS.md`](AGENTS.md)와 [`CLAUDE.md`](CLAUDE.md)를 읽고, 최소한 다음 검증이 동일한 head에서 통과하는지 확인합니다. + +```bash +npm run lint +npm test +npm run build +``` -## 현재 범위 +통과한 로컬 명령이나 PR branch 자체는 배포·인증·규제 준수 또는 게시 릴리스의 증거가 아닙니다. -- 7단계 작성 흐름과 진행 상태 -- 개인정보 수집 항목 선택 및 필수/선택 구분 -- 조건부 상세 입력 -- 실시간 검토본과 법적 근거 표시 -- 차단 오류와 권장 검토 항목 구분 -- 반응형 작성/미리보기 전환 +## 라이선스 -제품 요구사항과 결정 기록은 [`docs/PRD.md`](docs/PRD.md), [`docs/ADR-0001-policy-as-data.md`](docs/ADR-0001-policy-as-data.md)에 있습니다. +PolicyWeave의 ContextualWisdomLab 원저작 소스와 문서는 [MIT License](LICENSE)로 제공됩니다. npm 의존성, 표준 문서, 외부 서비스·자산은 각각의 라이선스와 이용 조건을 유지하며 이 저장소의 MIT grant가 이를 재라이선스하지 않습니다. 상업적 사용과 배포에 적합하지 않은 inbound 소프트웨어·자산은 제품 의존성으로 반입하지 않습니다. From f0abf82fa9fc9f8532b22adc67bddd62b53d00ac Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 05:25:22 +0900 Subject: [PATCH 042/282] chore: align package license metadata --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 494a5d0..3e3a137 100644 --- a/package.json +++ b/package.json @@ -1 +1 @@ -{"name":"policyweave","private":true,"version":"0.1.0","type":"module","scripts":{"dev":"vite --host 0.0.0.0","build":"tsc -b && vite build","lint":"eslint .","test":"vitest run","test:watch":"vitest"},"dependencies":{"@vitejs/plugin-react":"latest","lucide-react":"latest","react":"latest","react-dom":"latest","typescript":"^6.0.3","vite":"latest"},"devDependencies":{"@eslint/js":"latest","@testing-library/jest-dom":"latest","@testing-library/react":"latest","@types/react":"latest","@types/react-dom":"latest","eslint":"latest","eslint-plugin-react-hooks":"latest","eslint-plugin-react-refresh":"latest","globals":"latest","jsdom":"latest","typescript-eslint":"^8.68.0","vitest":"latest"}} +{"name":"policyweave","private":true,"version":"0.1.0","license":"MIT","type":"module","scripts":{"dev":"vite --host 0.0.0.0","build":"tsc -b && vite build","lint":"eslint .","test":"vitest run","test:watch":"vitest"},"dependencies":{"@vitejs/plugin-react":"latest","lucide-react":"latest","react":"latest","react-dom":"latest","typescript":"^6.0.3","vite":"latest"},"devDependencies":{"@eslint/js":"latest","@testing-library/jest-dom":"latest","@testing-library/react":"latest","@types/react":"latest","@types/react-dom":"latest","eslint":"latest","eslint-plugin-react-hooks":"latest","eslint-plugin-react-refresh":"latest","globals":"latest","jsdom":"latest","typescript-eslint":"^8.68.0","vitest":"latest"}} From 6cf410f980addbaab79eee4daefa0a1a2d284c0d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 05:30:53 +0900 Subject: [PATCH 043/282] docs: clarify public-authority anonymization boundary --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 7642482..b384d91 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,7 +10,7 @@ PolicyWeave is a local-first privacy-policy fact-authoring workspace. It structu - Re-fetch the PR head before every commit/push. Never force-push or weaken branch/ruleset protections to merge. - Keep `ARCHITECTURE.md`, `docs/TRD.md`, ADRs, `docs/research-traceability.md`, `CHANGELOG.md`, and `docs/product-technical-gap-baseline.md` aligned with implementation. - Do not encode legal conclusions from memory. Every legal/rule/template decision needs an authoritative source, effective date, source revision, and implementation/test trace. -- Do not commit real personal or institutional names in tests or documentation. Production must not consume synthetic demo data. +- Do not commit identifying customer, individual, or real operational-organization data in tests, examples, fixtures, or product documentation. Publicly documented legal authorities, official document titles, standards bodies, source publishers, and the repository owner may be named when required for accurate provenance and citation. Production must not consume synthetic demo data. - Keep persistence objects semantically named with at least two words and `snake_case` unless a framework contract requires another convention. Avoid generic named persistence objects such as a standalone `id` table/collection. - Hosted persistence/publication must be introduced only behind explicit tenant, authorization, audit, encryption, immutable revision, and supersession contracts. - GitHub Actions dependencies stay SHA pinned and checkout credentials must not persist. From 272c0aa7502c8768159ec7ba6188348e88d86b4f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 05:31:11 +0900 Subject: [PATCH 044/282] docs: pin PIPA provision effective dates --- docs/research-traceability.md | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/docs/research-traceability.md b/docs/research-traceability.md index feb43ce..128fed7 100644 --- a/docs/research-traceability.md +++ b/docs/research-traceability.md @@ -1,28 +1,30 @@ # Legal and research traceability -Last reconciled: 2026-09-01 +Last reconciled: 2026-09-02 PolicyWeave treats legal and regulatory material as versioned evidence, not as implicit knowledge embedded in UI copy. The current MVP does not yet implement a legal/rule engine; this ledger therefore records source authority and the product capability that still needs to be derived and tested. ## Authoritative source register -| Source | Current evidence on 2026-09-01 | Product use | Implementation status | +| Source | Current evidence on 2026-09-02 | Product use | Implementation status | | --- | --- | --- | --- | -| Republic of Korea, Personal Information Protection Act (개인정보 보호법), National Law Information Center | The current act shown by the official search service is effective 2025-10-02; an enacted amendment is scheduled to take effect 2026-09-11. https://www.law.go.kr/unSc.do?query=%EA%B0%9C%EC%9D%B8%EC%A0%95%EB%B3%B4%EB%B3%B4%ED%98%B8%EB%B2%95 | Statutory authority/effective-date anchor for rule derivation | Registry only; no article-level rules are encoded yet | -| Republic of Korea, Enforcement Decree of the Personal Information Protection Act (개인정보 보호법 시행령), National Law Information Center | Official search service lists the decree effective 2026-08-20. Same official search URL as above. | Subordinate-rule authority/effective-date anchor | Registry only; no decree-level rules are encoded yet | -| Personal Information Protection Commission. (2026, April 23). 개인정보 처리방침 작성지침(2026.4. 개정) [Guideline for writing privacy policies, April 2026 revision]. | PIPC marks this as the current guide. https://pipc.go.kr/np/cop/bbs/selectBoardList.do?bbsId=BS217&mCode=D010030000 | Authoring/review guidance and template requirement discovery | Source registered; requirement-by-requirement mapping still required | +| Republic of Korea, Personal Information Protection Act (개인정보 보호법), National Law Information Center | Current law: Act No. 20897, effective 2025-10-02. Enacted amendment: Act No. 21445, promulgated 2026-03-10. Its general amended provisions take effect 2026-09-11, while Article 32-2(1) proviso and Article 75(2)(15) take effect separately on 2027-07-01. Retrieved 2026-09-02 from the National Law Information Center; canonical amendment identifier: Act No. 21445 / legal-information sequence 283839. https://www.law.go.kr/LSW/lsInfoP.do?ancNo=21445&ancYd=20260310&efYd=20260911&lsiSeq=283839 | Statutory authority and provision-level effective-date anchor for rule derivation | Registry only; no article-level rules are encoded yet. Future rule snapshots must bind each provision to its own effective date. | +| Republic of Korea, Enforcement Decree of the Personal Information Protection Act (개인정보 보호법 시행령), National Law Information Center | Presidential Decree No. 36121, promulgated 2026-02-19 and effective 2026-08-20. Retrieved 2026-09-02; canonical National Law Information Center legal-information sequence 283503. https://www.law.go.kr/LSW/lsSideInfoP.do?docCls=jo&joNo=0032&lsiSeq=283503 | Subordinate-rule authority/effective-date anchor | Registry only; no decree-level rules are encoded yet. Future mappings must record the exact decree article and effective-date snapshot. | +| Personal Information Protection Commission. (2026, April 23). 개인정보 처리방침 작성지침(2026.4. 개정) [Guideline for writing privacy policies, April 2026 revision]. | PIPC marks this as the current guide. Retrieved 2026-09-02. https://pipc.go.kr/np/cop/bbs/selectBoardList.do?bbsId=BS217&mCode=D010030000 | Authoring/review guidance and template requirement discovery | Source registered; requirement-by-requirement mapping still required | ## APA 7 references Personal Information Protection Commission. (2026, April 23). *개인정보 처리방침 작성지침(2026.4. 개정)* [Guideline for writing privacy policies, April 2026 revision]. https://pipc.go.kr/np/cop/bbs/selectBoardList.do?bbsId=BS217&mCode=D010030000 -Republic of Korea. (2025). *개인정보 보호법* [Personal Information Protection Act] (Act No. 20897, effective October 2, 2025; future amendment Act No. 21445 effective September 11, 2026). National Law Information Center. https://www.law.go.kr/unSc.do?query=%EA%B0%9C%EC%9D%B8%EC%A0%95%EB%B3%B4%EB%B3%B4%ED%98%B8%EB%B2%95 +Republic of Korea. (2025). *개인정보 보호법* [Personal Information Protection Act] (Act No. 20897, effective October 2, 2025). National Law Information Center. https://www.law.go.kr/unSc.do?query=%EA%B0%9C%EC%9D%B8%EC%A0%95%EB%B3%B4%EB%B3%B4%ED%98%B8%EB%B2%95 -Republic of Korea. (2026). *개인정보 보호법 시행령* [Enforcement Decree of the Personal Information Protection Act] (Presidential Decree No. 36121, effective August 20, 2026). National Law Information Center. https://www.law.go.kr/unSc.do?query=%EA%B0%9C%EC%9D%B8%EC%A0%95%EB%B3%B4%EB%B3%B4%ED%98%B8%EB%B2%95 +Republic of Korea. (2026). *개인정보 보호법 일부개정법률* [Amendment to the Personal Information Protection Act] (Act No. 21445, promulgated March 10, 2026; general effective date September 11, 2026; Article 32-2(1) proviso and Article 75(2)(15) effective July 1, 2027). National Law Information Center. https://www.law.go.kr/LSW/lsInfoP.do?ancNo=21445&ancYd=20260310&efYd=20260911&lsiSeq=283839 + +Republic of Korea. (2026). *개인정보 보호법 시행령* [Enforcement Decree of the Personal Information Protection Act] (Presidential Decree No. 36121, promulgated February 19, 2026, effective August 20, 2026; legal-information sequence 283503). National Law Information Center. https://www.law.go.kr/LSW/lsSideInfoP.do?docCls=jo&joNo=0032&lsiSeq=283503 ## Traceability rules -1. Every future legal/rule/template implementation records source identifier, article/section where applicable, effective date, source revision or retrieval digest, implementation symbol, and tests. -2. Future-effective law is never silently treated as already-effective law. A rule set declares the jurisdiction/effective-date snapshot it evaluates. +1. Every future legal/rule/template implementation records source identifier, article/section where applicable, effective date, source revision or retrieval timestamp/digest, implementation symbol, and tests. +2. Future-effective law is never silently treated as already-effective law. A rule set declares the jurisdiction/effective-date snapshot it evaluates; provisions with distinct commencement dates remain distinct entries rather than inheriting a statute-level date. 3. Guidance is not promoted to statute, and product copy does not claim that following a guide guarantees compliance. 4. A source revision creates an explicit evaluation event against existing draft/published revisions; it does not mutate historical publication evidence. 5. LLM output, if later used to explain or propose wording, is never an authoritative legal source and cannot change review/publication state. From b0b4835e2c3ea07a72bd0d128e561d8b50b69d5d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 10:03:37 +0900 Subject: [PATCH 045/282] test: make responsive media probes static --- src/styles.test.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/styles.test.ts b/src/styles.test.ts index c6cccbd..a75c76d 100644 --- a/src/styles.test.ts +++ b/src/styles.test.ts @@ -3,9 +3,13 @@ import { describe, expect, it } from 'vitest' const css = readFileSync(new URL('./styles.css', import.meta.url), 'utf8') -function mediaBlock(maxWidth: number) { - const pattern = new RegExp(`@media \\(max-width: ${maxWidth}px\\) \\{([\\s\\S]*?)(?=\\n\\}\\n(?:\\n@media|$))`) - const match = css.match(pattern) +const mediaPatterns = { + 1300: /@media \(max-width: 1300px\) \{([\s\S]*?)(?=\n\}\n(?:\n@media|$))/, + 720: /@media \(max-width: 720px\) \{([\s\S]*?)(?=\n\}\n(?:\n@media|$))/, +} as const + +function mediaBlock(maxWidth: keyof typeof mediaPatterns) { + const match = css.match(mediaPatterns[maxWidth]) if (!match) throw new Error(`Missing max-width ${maxWidth}px media block`) return match[1] } From 45745a15d3d0619f2cae985e89b9b372f6602dd1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 10:04:41 +0900 Subject: [PATCH 046/282] docs: record failed-check RCA --- docs/product-technical-gap-baseline.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 8ab7503..2eec736 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1,6 +1,6 @@ # Product and technical gap baseline -Last reconciled: 2026-09-01 +Last reconciled: 2026-09-02 This ledger records PolicyWeave's commercialization gap from PRD/ADR/TRD/architecture, implementation, authoritative legal-source evidence, review findings, and live GitHub state. Live Check conclusions are deliberately not committed because they change outside the repository; merge decisions must re-fetch the exact current PR head and live ruleset. @@ -12,6 +12,16 @@ The seven-step routing defect and warning-navigation defect were reproduced on p A later exact-head review found that collection-path editing was incorrectly coupled to an empty processing purpose: enabled items with an existing purpose lost the collection-path field, while entering a purpose made both fields disappear. The regression was added first at `b35e18f3831ea5a257b0a9397c08aa73af8c3f52`; `CollectionForm` was then repaired at `87d37b9a219ff50013f9ac46c3b1f11b52a94467` so collection-path editing remains in step 2 and purpose editing remains in step 3. `ec1aead4484018d52ce8bd5bbab04c517d42870d` also locks the existing whitespace-only-purpose blocking contract. The review thread was resolved only after the repaired code was re-fetched at `87d37b9a219ff50013f9ac46c3b1f11b52a94467`. This documentation reconciliation changes no production behavior; live checks/reviews must be reacquired for the resulting current head. +### 2026-09-02 failed-Check RCA: responsive CSS test SAST + +On exact PR head `272c0aa7502c8768159ec7ba6188348e88d86b4f`, SAST run `33555808672`, job `100016223694` completed exact-head checkout and Semgrep analysis, then failed its Medium+ enforcement gate on one unsuppressed finding: `javascript.lang.security.audit.detect-non-literal-regexp.detect-non-literal-regexp` at `src/styles.test.ts:7`. The PR-introduced test helper constructed its media-query matcher with `new RegExp(...)` from a function argument. The only callers were fixed widths (`1300` and `720`), so this was not an attacker-controlled product path, but it was a real repository-owned static-analysis defect and was not suppressed or downgraded. + +The smallest repair replaces the dynamic constructor with a closed map of literal regular expressions for the two supported widths while preserving the existing responsive assertions and missing-block failure. The failing SAST result is the regression evidence; exact-head Vitest and Semgrep must both be reacquired after the repair. No production CSS behavior, warning severity, scanner configuration, review rule, or coverage gate is weakened. + +The same predecessor head also had an independent Security Scan failure in run `33555808490`, dependency-review job `100016223043`. Exact checkout succeeded and the dependency-graph compare request for base `52f4fd6bb68f870d0519cf11dd471573a2f197c0` to head `272c0aa7502c8768159ec7ba6188348e88d86b4f` completed transport successfully (`curl_exit=0`) but GitHub returned HTTP `403`. OSV, Trivy, and Scorecard sibling jobs succeeded. This matches the organization-level Dependency Review availability/authorization incident tracked by `ContextualWisdomLab/.github#810`; the central workflow correctly failed closed, so no PolicyWeave dependency or security gate is changed to work around it. + +Required Noema Review run `33555806837`, job `100016218520` was also independent of the product code. It used trusted central workflow source `ContextualWisdomLab/.github@30f55d10e200673493f824886aa891cb3b099b7e`, successfully minted the repository-scoped reviewer token and started `contextual-orchestrator` with the `orchestrator/free` pool. Five provider routes passed runtime preflight, but the returned verdict lacked the required two concrete adversarial probes and the bounded schema/evidence repair then exceeded its 900-second absolute deadline. That is a fail-closed model/control-plane evidence failure, not permission failure or a PolicyWeave code verdict. A new exact head must exercise the then-current trusted central workflow rather than carrying this predecessor result forward. + ## Current baseline | Area | Evidence | Status | Commercialization gap | Owner/action | Next verification | @@ -26,7 +36,7 @@ A later exact-head review found that collection-path editing was incorrectly cou | Security/privacy | `docs/SECURITY.md`; local-first current runtime | Baseline documented | Hosted tenant model, secrets boundary, encryption/key handling, audit and incident/retention evidence absent | Platform/Security: threat-model hosted boundary before backend | Security tests and exact-head org scans | | Tests | Vitest workflow, routing/navigation/blocker regressions, whitespace-purpose edge case, collection-path/purpose separation, responsive CSS regression | Improved | Browser E2E, accessibility and broader domain edge cases remain incomplete; 100% coverage not evidenced | Test Engineering: expand behavior-first tests | Exact-head coverage and browser evidence | | Performance | Static Vite client | Unevidenced | No page performance or network/load evidence | Operability: baseline browser performance; add k6 only with backend | Realistic measurements before latency claims | -| CI/security merge gate | SHA-pinned checkout plus repo and central required workflows | External live gate | Hosted Actions jobs have been observed queued without runner assignment; queue state is non-passing and is consistent with a shared control-plane admission issue | Control plane: diagnose runner/admission policy without weakening rules | Exact-head assigned jobs must execute; independent approval and threads required | +| CI/security merge gate | SHA-pinned checkout plus repo and central required workflows | External live gate | Dependency Review can fail closed when GitHub's exact base/head dependency-graph compare is unavailable; review/model providers can also fail closed without publishable evidence | Control plane: repair incidents centrally and never substitute sibling scanners or predecessor evidence | Exact-head assigned jobs must execute; independent approval and threads required | ## DDD/context map Core subdomain: **Policy Fact Authoring**. Supporting: **Review & Publication**, **Legal Source Registry**. Generic infrastructure remains outside domain authority. `Policy Fact Authoring -> Review & Publication` supplies a versioned fact contract. `Legal Source Registry -> Review & Publication` supplies versioned source/rule evidence through an ACL. Customer/source-system integrations remain adapters and may not mutate foreign systems through hidden coupling. From 2d4c956b2b7b4cc3607c2724a4be2073393dc692 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 10:04:55 +0900 Subject: [PATCH 047/282] docs: note responsive SAST repair --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48b6266..7e04dfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ All notable product changes are recorded here. PolicyWeave is pre-release; entri - Collection-path editing now remains available for every selected item in the collection step, while processing-purpose editing stays in its dedicated purpose step. - Review preview now reflects verified authoring facts instead of presenting the collection step as the entire workflow. - Responsive review behavior and mobile publication feedback were repaired during PR review. +- Responsive CSS contract tests now use only literal media-query regular expressions, removing the Semgrep dynamic-RegExp SAST finding without suppressing or weakening the scanner gate. - GitHub Actions checkout dependencies are SHA pinned and credentials are not persisted in the working tree. ### Not yet shipped From 10ac51201afb59d677ed546dd7e32ebc7373031f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 11:20:24 +0900 Subject: [PATCH 048/282] test: require stale policy facts to clear on disable --- src/App.test.tsx | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/App.test.tsx b/src/App.test.tsx index 7008c87..2b4d684 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -30,6 +30,30 @@ describe('policy editing workflow', () => { expect(container.querySelector('input[name="purpose-phone"]')).not.toBeNull() }) + it('수집 항목을 해제하면 이전 처리 목적과 수집 경로를 폐기해 재활성화 시 재검토한다', () => { + const { container } = render() + const phoneCheckbox = () => container.querySelectorAll('.check-label input')[2] + + fireEvent.click(phoneCheckbox()) + const phoneItem = container.querySelectorAll('.item-list .item')[2] + fireEvent.change(phoneItem.querySelector('input[placeholder="예: 회원가입 화면"]')!, { target: { value: 'SMS 인증 화면' } }) + + fireEvent.click(container.querySelectorAll('.rail li button')[2]) + fireEvent.change(container.querySelector('input[name="purpose-phone"]')!, { target: { value: '본인 확인 및 알림 발송' } }) + expect(container.querySelector('.review-stat.blocking b')?.textContent).toBe('0건') + + fireEvent.click(container.querySelectorAll('.rail li button')[1]) + fireEvent.click(phoneCheckbox()) + fireEvent.click(phoneCheckbox()) + + const reenabledPhone = container.querySelectorAll('.item-list .item')[2] + expect(reenabledPhone.querySelector('input[placeholder="예: 회원가입 화면"]')?.value).toBe('') + expect(container.querySelector('.review-stat.blocking b')?.textContent).toBe('1건') + + fireEvent.click(container.querySelectorAll('.rail li button')[2]) + expect(container.querySelector('input[name="purpose-phone"]')?.value).toBe('') + }) + it('미리보기 경고에서 처리 목적 단계로 이동한다', () => { const { container } = render() const phone = container.querySelectorAll('.check-label input')[2] @@ -53,4 +77,4 @@ describe('policy editing workflow', () => { fireEvent.click(publish) expect(container.querySelector('output')?.textContent).toContain('백엔드 연결이 필요합니다') }) -}) +}) \ No newline at end of file From 12f4cc8bae8aa1a324ba5238d593acb53e59dd8c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 11:22:27 +0900 Subject: [PATCH 049/282] test: align whitespace purpose preview with review status --- src/App.test.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/App.test.tsx b/src/App.test.tsx index 2b4d684..b96b3c3 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -54,6 +54,20 @@ describe('policy editing workflow', () => { expect(container.querySelector('input[name="purpose-phone"]')?.value).toBe('') }) + it('공백뿐인 처리 목적은 미리보기에서도 미입력으로 표시한다', () => { + const { container } = render() + fireEvent.click(container.querySelectorAll('.rail li button')[2]) + + const purpose = container.querySelector('input[name="purpose-name"]')! + fireEvent.change(purpose, { target: { value: ' ' } }) + + expect(container.querySelector('.review-stat.blocking b')?.textContent).toBe('1건') + const nameRow = Array.from(container.querySelectorAll('.paper tbody tr')).find((row) => row.cells[0]?.textContent === '이름')! + expect(nameRow.cells[1]?.textContent).toBe('처리 목적 입력 필요') + expect(nameRow.cells[1]?.classList.contains('missing')).toBe(true) + expect(nameRow.cells[2]?.textContent).toBe('확인 필요') + }) + it('미리보기 경고에서 처리 목적 단계로 이동한다', () => { const { container } = render() const phone = container.querySelectorAll('.check-label input')[2] From 798ac3672812c5263a44de09a610e0b5cb9a7efc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 11:24:13 +0900 Subject: [PATCH 050/282] fix: invalidate stale collection evidence --- src/App.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index e0cee58..2d0da15 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -82,7 +82,10 @@ function CollectionForm({ items, setItems, setCurrent }: { items: PolicyItem[];
수집 항목설명수집 여부
{items.map((item) =>
- + {item.description}
@@ -150,7 +153,10 @@ function DocumentPreview({ items, facts, setCurrent }: { items: PolicyItem[]; fa

{facts.serviceName || '서비스 운영자'}는 이용자의 개인정보를 중요하게 여기며, 확인된 실제 처리 사실을 바탕으로 다음 사항을 검토합니다.

제1조 (개인정보의 처리 목적)

아래 목적은 작성자가 확인한 운영 사실을 기준으로 표시됩니다.

- {review.enabled.map((item) => )}
수집 항목처리 목적검토 상태
{item.label}{item.purpose || '처리 목적 입력 필요'}{item.purpose ? '입력됨' : '확인 필요'}
+ {review.enabled.map((item) => { + const hasPurpose = item.purpose.trim().length > 0 + return + })}
수집 항목처리 목적검토 상태
{item.label}{hasPurpose ? item.purpose : '처리 목적 입력 필요'}{hasPurpose ? '입력됨' : '확인 필요'}
{review.blocking.length > 0 &&
공개 전 확인{review.blocking.map((item) => item.label).join(', ')}의 처리 목적이 입력되지 않았습니다.
}

제2조 (처리 및 보유 기간)

{facts.retentionPeriod || '보유 기간 단계에서 확인한 운영 기준을 입력해야 합니다.'}

제3조 (제3자 제공)

{facts.thirdPartyRecipient ? `${facts.thirdPartyRecipient}에 ${facts.thirdPartyPurpose || '확인 중인 목적'}으로 제공하는 흐름을 검토 중입니다.` : '제3자 제공 여부를 확인하는 단계가 남아 있습니다.'}

@@ -174,4 +180,4 @@ export default function App() {
검토 요약확인을 마친 뒤 공개 URL 발행 준비 상태를 확인하세요.
필수 확인 {review.blocking.length}건
권장 검토 {review.recommended.length}건
{message}
-} +} \ No newline at end of file From bb4ff95ee7ab2a214a53d2a9a3fac2f4905dbd01 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 11:26:02 +0900 Subject: [PATCH 051/282] docs: define collection evidence invalidation invariant --- ARCHITECTURE.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 0b0dcc2..189b970 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -19,10 +19,11 @@ A future `policy_revision` is the minimal aggregate root and transaction boundar Core invariants: 1. A selected collection item cannot pass review without a non-empty processing purpose. -2. Review findings navigate to the fact that caused them. -3. Publication must never upgrade an unreviewed or incomplete draft to a reviewed/authoritative state. -4. A published revision remains reproducible from its policy facts plus rule/template/source versions. -5. External legal-source updates produce explicit re-evaluation, not silent rewriting. +2. Disabling a collection item invalidates its dependent processing-purpose and collection-path evidence; re-enabling it requires those facts to be captured and reviewed again rather than silently reviving stale evidence. +3. Review findings navigate to the fact that caused them. +4. Publication must never upgrade an unreviewed or incomplete draft to a reviewed/authoritative state. +5. A published revision remains reproducible from its policy facts plus rule/template/source versions. +6. 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. `src/policy.ts` owns the current collection-item review rule; `src/App.tsx` owns browser orchestration and deterministic preview rendering. From 9e7392c9846c94c44b72d0879d2c8b438e04392a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 11:26:18 +0900 Subject: [PATCH 052/282] docs: specify evidence invalidation and preview normalization --- docs/TRD.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/TRD.md b/docs/TRD.md index e7dfb6c..e4bbecc 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -13,10 +13,11 @@ This TRD covers the pre-release PolicyWeave browser workspace and the contracts ## Functional contracts 1. Every PRD step must route to an editable surface; selecting a step cannot change only the rail indicator. 2. Selected collection items expose their collection mode and processing-purpose contract. -3. A selected item with blank/whitespace purpose produces a blocking review finding. -4. A blocking finding links to the responsible editing step. -5. Preview text is derived from current structured facts; it does not become an independently editable authority. -6. UI copy distinguishes a review draft from legal advice, certification, or a compliance guarantee. +3. A selected item with blank/whitespace purpose produces a blocking review finding, and the preview must apply the same blank/whitespace normalization when it renders purpose completeness. +4. Disabling a collection item invalidates dependent processing-purpose and collection-path evidence; re-enabling the item requires those facts to be captured and reviewed again. +5. A blocking finding links to the responsible editing step. +6. Preview text is derived from current structured facts; it does not become an independently editable authority. +7. UI copy distinguishes a review draft from legal advice, certification, or a compliance guarantee. ## Quality contracts - Touched production behavior requires regression and edge-case tests first. From d1e5fd4cc3c850da68a752c7e70b73ab09203a43 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 11:26:32 +0900 Subject: [PATCH 053/282] docs: record dependent policy fact invalidation --- docs/ADR-0001-policy-as-data.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/ADR-0001-policy-as-data.md b/docs/ADR-0001-policy-as-data.md index 0aab9d2..19ec43f 100644 --- a/docs/ADR-0001-policy-as-data.md +++ b/docs/ADR-0001-policy-as-data.md @@ -6,10 +6,12 @@ 수집 항목, 목적, 보유 기간, 제공 대상, 이전 국가와 연락처를 구조화된 정책 데이터로 저장한다. 화면의 문서는 이 데이터에서 결정적으로 렌더링한다. +구조화된 사실 사이의 의존성도 데이터 계약의 일부로 취급한다. 수집 항목을 비활성화하면 그 항목에 종속된 처리 목적과 수집 경로 증거를 함께 무효화하고, 다시 활성화할 때는 최신 사실을 재입력·재검토한다. 미리보기와 검토 규칙은 공백 정규화를 포함해 같은 완전성 계약을 사용한다. + ## 이유 -문자열 문서를 직접 편집하면 동일 항목이 표·본문·요약에서 불일치할 수 있다. 구조화된 원본은 검증 규칙, 변경 이력, 다국어 렌더러와 공개 API를 같은 계약 위에 올릴 수 있다. +문자열 문서를 직접 편집하면 동일 항목이 표·본문·요약에서 불일치할 수 있다. 구조화된 원본은 검증 규칙, 변경 이력, 다국어 렌더러와 공개 API를 같은 계약 위에 올릴 수 있다. 비활성화된 사실에 종속 데이터를 그대로 보존해 자동 복원하면 사용자가 철회한 처리 흐름의 오래된 증거가 새 검토 없이 다시 유효해질 수 있다. ## 결과 -자유 문구가 필요한 예외에는 출처와 검토 상태를 함께 저장해야 한다. 법령 변경은 템플릿 버전과 규칙 세트 버전으로 추적한다. +자유 문구가 필요한 예외에는 출처와 검토 상태를 함께 저장해야 한다. 법령 변경은 템플릿 버전과 규칙 세트 버전으로 추적한다. 향후 영속화 계층도 삭제·비활성화 명령에서 종속 사실의 무효화 또는 명시적 이력 보존을 구분하고, 현재 유효한 정책 사실과 감사 이력을 혼동하지 않아야 한다. From 948d47936c55d19e2b15792942b975025341fb5b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 11:26:45 +0900 Subject: [PATCH 054/282] docs: record stale-evidence and preview consistency fixes --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e04dfd..47cfd95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,11 +7,13 @@ All notable product changes are recorded here. PolicyWeave is pre-release; entri ### Added - Seven-step authoring workflow with distinct editing surfaces for service information, collection items, processing purposes, retention, third-party transfer, international transfer, and privacy contact. - Warning-to-source navigation for missing processing purposes. -- Regression coverage for all seven step routes, warning navigation, clearing the publication blocker after a missing purpose is supplied, whitespace-only purposes, and collection-path/purpose step separation. +- Regression coverage for all seven step routes, warning navigation, clearing the publication blocker after a missing purpose is supplied, whitespace-only purposes, collection-path/purpose step separation, disable/re-enable stale-evidence invalidation, and preview/review whitespace consistency. - Product/technical gap ledger, architecture, technical requirements, security baseline, and legal-source traceability. ### Changed - Collection-path editing now remains available for every selected item in the collection step, while processing-purpose editing stays in its dedicated purpose step. +- Disabling a collection item now clears its dependent processing-purpose and collection-path evidence so re-enabling it cannot silently revive stale policy facts. +- Review preview now applies the same whitespace-normalized purpose-completeness contract as the blocking review rule. - Review preview now reflects verified authoring facts instead of presenting the collection step as the entire workflow. - Responsive review behavior and mobile publication feedback were repaired during PR review. - Responsive CSS contract tests now use only literal media-query regular expressions, removing the Semgrep dynamic-RegExp SAST finding without suppressing or weakening the scanner gate. From 23870d297356475f77b5f62827fbf8fdff3d7e77 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 11:27:17 +0900 Subject: [PATCH 055/282] docs: reconcile live policy-fact integrity repairs --- docs/product-technical-gap-baseline.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 2eec736..1ae679a 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -10,7 +10,9 @@ PolicyWeave is a local-first policy-fact workspace that helps a service operator ## Exact-head implementation evidence The seven-step routing defect and warning-navigation defect were reproduced on predecessor head `62b6f5ef9056a111c93a09ba40d205c98f42baef`: `App.tsx` always rendered the collection editor while regression tests expected all seven editors and a warning-navigation action. Tests were strengthened first at `4795f99beb51a95cbc7393a2bb0b9600389776d8`, then production routing/review behavior was repaired at `61f5bac24d0271f382668872140a7ef4a3463db4`. -A later exact-head review found that collection-path editing was incorrectly coupled to an empty processing purpose: enabled items with an existing purpose lost the collection-path field, while entering a purpose made both fields disappear. The regression was added first at `b35e18f3831ea5a257b0a9397c08aa73af8c3f52`; `CollectionForm` was then repaired at `87d37b9a219ff50013f9ac46c3b1f11b52a94467` so collection-path editing remains in step 2 and purpose editing remains in step 3. `ec1aead4484018d52ce8bd5bbab04c517d42870d` also locks the existing whitespace-only-purpose blocking contract. The review thread was resolved only after the repaired code was re-fetched at `87d37b9a219ff50013f9ac46c3b1f11b52a94467`. This documentation reconciliation changes no production behavior; live checks/reviews must be reacquired for the resulting current head. +A later exact-head review found that collection-path editing was incorrectly coupled to an empty processing purpose: enabled items with an existing purpose lost the collection-path field, while entering a purpose made both fields disappear. The regression was added first at `b35e18f3831ea5a257b0a9397c08aa73af8c3f52`; `CollectionForm` was then repaired at `87d37b9a219ff50013f9ac46c3b1f11b52a94467` so collection-path editing remains in step 2 and purpose editing remains in step 3. `ec1aead4484018d52ce8bd5bbab04c517d42870d` also locks the existing whitespace-only-purpose blocking contract. The review thread was resolved only after the repaired code was re-fetched at `87d37b9a219ff50013f9ac46c3b1f11b52a94467`. + +On exact PR head `2d4c956b2b7b4cc3607c2724a4be2073393dc692`, two additional policy-fact integrity gaps were reproduced from the current implementation. First, disabling an enabled collection item preserved its processing purpose and collection path, so later re-enabling silently revived stale evidence without renewed review. Second, the review engine correctly treated a whitespace-only purpose as missing while the preview used string truthiness and displayed the same whitespace value as entered. Regression tests were added before production changes at `10ac51201afb59d677ed546dd7e32ebc7373031f` and `12f4cc8bae8aa1a324ba5238d593acb53e59dd8c`. Production was then repaired at `798ac3672812c5263a44de09a610e0b5cb9a7efc`: disabling an item now invalidates its dependent purpose/path facts, and preview status uses the same trimmed-purpose completeness contract as review. Architecture, TRD, ADR-0001 and CHANGELOG were reconciled afterward. Because those documentation commits move the PR head, their live Checks/reviews must be reacquired on the resulting exact current head; predecessor-head conclusions are not merge evidence. ### 2026-09-02 failed-Check RCA: responsive CSS test SAST @@ -26,15 +28,15 @@ Required Noema Review run `33555806837`, job `100016218520` was also independent | Area | Evidence | Status | Commercialization gap | Owner/action | Next verification | | --- | --- | --- | --- | --- | --- | -| Guided authoring | PRD, ADR-0002, seven routed editors in `src/App.tsx`; step-2 collection-path/step-3 purpose separation regression | Functional foundation | Retention, third-party transfer, international transfer and contact steps currently capture thin facts rather than requirement-complete domain contracts | Policy Fact Authoring: add typed facts and deterministic validations from authoritative requirements | Unit/integration tests for valid, missing, contradictory and not-applicable cases | -| Processing-purpose review | `src/policy.ts`, `src/policy.test.ts`, `src/App.test.tsx` | Repaired | Missing and whitespace-only purposes block review, but the rule set covers only purpose completeness | Review & Publication: extend evidence-bound review rules without heuristic scoring | Exact-head tests plus rule/source traceability | +| Guided authoring | PRD, ADR-0002, seven routed editors in `src/App.tsx`; step-2 collection-path/step-3 purpose separation; disabled-item dependent-fact invalidation | Functional foundation | Retention, third-party transfer, international transfer and contact steps currently capture thin facts rather than requirement-complete domain contracts | Policy Fact Authoring: add typed facts and deterministic validations from authoritative requirements | Unit/integration tests for valid, missing, contradictory and not-applicable cases | +| Processing-purpose review | `src/policy.ts`, `src/policy.test.ts`, `src/App.test.tsx`; whitespace normalization shared by review and preview | Repaired | Missing and whitespace-only purposes block review consistently, but the rule set covers only purpose completeness | Review & Publication: extend evidence-bound review rules without heuristic scoring | Exact-head tests plus rule/source traceability | | Review workspace | Live preview, warning-to-source navigation, responsive CSS fixes | Implemented foundation | No browser screenshot matrix or realistic interaction evidence | UX: add Playwright + accessibility harness | Desktop/tablet/mobile screenshots, keyboard/focus and action-edge verification | | Accessibility | Semantic controls, focus-visible behavior, keyboard-operable step navigation | Partial | No automated WCAG 2.2 evidence or screen-reader contract | UX: add axe/Playwright and manual interaction record | WCAG audit and browser matrix | | Policy model | ADR-0001, ARCHITECTURE, TRD | Designed, browser-memory only | No versioned durable schema, migration, audit history, UPSERT/idempotency or 3NF implementation | Platform: define `policy_revision` schema and item-level conflict contracts before storage | Schema/contract tests and migration round trip | -| Publication | UI communicates backend requirement; architecture defines immutable `publication_revision` direction | Planned | No authenticated review approval, immutable publication, supersession or public URL lifecycle | Review & Publication: implement only after persistence/security entry criteria | Authorization, replay/digest, supersession and rollback tests | +| Publication | UI communicates that hosted publication is not yet shipped; architecture defines immutable `publication_revision` direction | Planned | No authenticated review approval, immutable publication, supersession or public URL lifecycle | Review & Publication: implement only after persistence/security entry criteria | Authorization, replay/digest, supersession and rollback tests | | Legal/rule traceability | `docs/research-traceability.md`; official PIPA/PIPC source register | Source baseline established | No article/section-level requirement mapping or versioned legal-rule engine | Legal Source Registry: map each implemented rule to source/effective date/revision | Fixtures across current and future-effective rule-set snapshots | | Security/privacy | `docs/SECURITY.md`; local-first current runtime | Baseline documented | Hosted tenant model, secrets boundary, encryption/key handling, audit and incident/retention evidence absent | Platform/Security: threat-model hosted boundary before backend | Security tests and exact-head org scans | -| Tests | Vitest workflow, routing/navigation/blocker regressions, whitespace-purpose edge case, collection-path/purpose separation, responsive CSS regression | Improved | Browser E2E, accessibility and broader domain edge cases remain incomplete; 100% coverage not evidenced | Test Engineering: expand behavior-first tests | Exact-head coverage and browser evidence | +| Tests | Vitest workflow, routing/navigation/blocker regressions, whitespace-purpose edge case, collection-path/purpose separation, disable/re-enable stale-evidence invalidation, preview/review whitespace consistency, responsive CSS regression | Improved | Browser E2E, accessibility and broader domain edge cases remain incomplete; 100% coverage not evidenced | Test Engineering: expand behavior-first tests | Exact-head coverage and browser evidence | | Performance | Static Vite client | Unevidenced | No page performance or network/load evidence | Operability: baseline browser performance; add k6 only with backend | Realistic measurements before latency claims | | CI/security merge gate | SHA-pinned checkout plus repo and central required workflows | External live gate | Dependency Review can fail closed when GitHub's exact base/head dependency-graph compare is unavailable; review/model providers can also fail closed without publishable evidence | Control plane: repair incidents centrally and never substitute sibling scanners or predecessor evidence | Exact-head assigned jobs must execute; independent approval and threads required | From fcd75aa98242158d0abe5d8beb661dd0ff064011 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 11:28:33 +0900 Subject: [PATCH 056/282] test: keep buyer copy free of internal boundaries --- src/App.test.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/App.test.tsx b/src/App.test.tsx index b96b3c3..e8f36d3 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -76,7 +76,7 @@ describe('policy editing workflow', () => { expect(container.querySelector('.form-panel h1')?.textContent).toBe('3. 처리 목적') }) - it('처리 목적을 보완하면 발행 차단을 해제한다', () => { + it('처리 목적을 보완하면 구현 내부가 아닌 고객의 다음 검토 행동을 안내한다', () => { const { container } = render() const phone = container.querySelectorAll('.check-label input')[2] fireEvent.click(phone) @@ -88,7 +88,11 @@ describe('policy editing workflow', () => { expect(container.querySelector('.review-stat.blocking b')?.textContent).toBe('0건') const publish = container.querySelector('.publish')! expect(publish.disabled).toBe(false) + expect(publish.textContent).toContain('공개 준비 확인') fireEvent.click(publish) - expect(container.querySelector('output')?.textContent).toContain('백엔드 연결이 필요합니다') + const message = container.querySelector('output')?.textContent ?? '' + expect(message).toContain('책임자와 검토') + expect(message).not.toContain('백엔드') + expect(message).not.toContain('저장소') }) }) \ No newline at end of file From 9d86a1316b03455c740c4b70296f604c52d9981f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 11:29:27 +0900 Subject: [PATCH 057/282] fix: replace internal publication copy with buyer action --- src/App.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 2d0da15..35e20a2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -174,10 +174,10 @@ export default function App() { const [current, setCurrent] = useState(2) const review = useMemo(() => getReview(items), [items]) const [message, setMessage] = useState('') - function publish() { setMessage(review.blocking.length ? '필수 확인 항목을 먼저 입력하세요.' : '공개 URL 발행에는 저장소 백엔드 연결이 필요합니다.') } + function publish() { setMessage(review.blocking.length ? '필수 확인 항목을 먼저 입력하세요.' : '필수 확인이 완료되었습니다. 현재 검토본을 책임자와 검토하고 필요한 사실을 보완하세요.') } return
PolicyWeave작성 중버전 0.1.0 (임시저장) 브라우저 작업 중
-
검토 요약확인을 마친 뒤 공개 URL 발행 준비 상태를 확인하세요.
필수 확인 {review.blocking.length}건
권장 검토 {review.recommended.length}건
{message}
+
검토 요약확인을 마친 뒤 공개 준비 상태를 확인하세요.
필수 확인 {review.blocking.length}건
권장 검토 {review.recommended.length}건
{message}
} \ No newline at end of file From d649604bf41e6618c8deeba5a83d1b8b973355d6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 11:29:43 +0900 Subject: [PATCH 058/282] docs: record buyer-facing readiness guidance repair --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47cfd95..2f994fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,13 +7,14 @@ All notable product changes are recorded here. PolicyWeave is pre-release; entri ### Added - Seven-step authoring workflow with distinct editing surfaces for service information, collection items, processing purposes, retention, third-party transfer, international transfer, and privacy contact. - Warning-to-source navigation for missing processing purposes. -- Regression coverage for all seven step routes, warning navigation, clearing the publication blocker after a missing purpose is supplied, whitespace-only purposes, collection-path/purpose step separation, disable/re-enable stale-evidence invalidation, and preview/review whitespace consistency. +- Regression coverage for all seven step routes, warning navigation, clearing the publication blocker after a missing purpose is supplied, whitespace-only purposes, collection-path/purpose step separation, disable/re-enable stale-evidence invalidation, preview/review whitespace consistency, and buyer-facing publication guidance that does not expose implementation boundaries. - Product/technical gap ledger, architecture, technical requirements, security baseline, and legal-source traceability. ### Changed - Collection-path editing now remains available for every selected item in the collection step, while processing-purpose editing stays in its dedicated purpose step. - Disabling a collection item now clears its dependent processing-purpose and collection-path evidence so re-enabling it cannot silently revive stale policy facts. - Review preview now applies the same whitespace-normalized purpose-completeness contract as the blocking review rule. +- The publication-area CTA now describes a readiness check and directs the operator to responsible review rather than exposing repository/backend implementation details for an unshipped capability. - Review preview now reflects verified authoring facts instead of presenting the collection step as the entire workflow. - Responsive review behavior and mobile publication feedback were repaired during PR review. - Responsive CSS contract tests now use only literal media-query regular expressions, removing the Semgrep dynamic-RegExp SAST finding without suppressing or weakening the scanner gate. From 345d8634ab053ea75ba249c70cad46ed90e8ce6b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 11:30:42 +0900 Subject: [PATCH 059/282] docs: reconcile buyer-facing publication guidance repair --- docs/product-technical-gap-baseline.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 1ae679a..24fe00b 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -12,7 +12,9 @@ The seven-step routing defect and warning-navigation defect were reproduced on p A later exact-head review found that collection-path editing was incorrectly coupled to an empty processing purpose: enabled items with an existing purpose lost the collection-path field, while entering a purpose made both fields disappear. The regression was added first at `b35e18f3831ea5a257b0a9397c08aa73af8c3f52`; `CollectionForm` was then repaired at `87d37b9a219ff50013f9ac46c3b1f11b52a94467` so collection-path editing remains in step 2 and purpose editing remains in step 3. `ec1aead4484018d52ce8bd5bbab04c517d42870d` also locks the existing whitespace-only-purpose blocking contract. The review thread was resolved only after the repaired code was re-fetched at `87d37b9a219ff50013f9ac46c3b1f11b52a94467`. -On exact PR head `2d4c956b2b7b4cc3607c2724a4be2073393dc692`, two additional policy-fact integrity gaps were reproduced from the current implementation. First, disabling an enabled collection item preserved its processing purpose and collection path, so later re-enabling silently revived stale evidence without renewed review. Second, the review engine correctly treated a whitespace-only purpose as missing while the preview used string truthiness and displayed the same whitespace value as entered. Regression tests were added before production changes at `10ac51201afb59d677ed546dd7e32ebc7373031f` and `12f4cc8bae8aa1a324ba5238d593acb53e59dd8c`. Production was then repaired at `798ac3672812c5263a44de09a610e0b5cb9a7efc`: disabling an item now invalidates its dependent purpose/path facts, and preview status uses the same trimmed-purpose completeness contract as review. Architecture, TRD, ADR-0001 and CHANGELOG were reconciled afterward. Because those documentation commits move the PR head, their live Checks/reviews must be reacquired on the resulting exact current head; predecessor-head conclusions are not merge evidence. +On exact PR head `2d4c956b2b7b4cc3607c2724a4be2073393dc692`, two additional policy-fact integrity gaps were reproduced from the current implementation. First, disabling an enabled collection item preserved its processing purpose and collection path, so later re-enabling silently revived stale evidence without renewed review. Second, the review engine correctly treated a whitespace-only purpose as missing while the preview used string truthiness and displayed the same whitespace value as entered. Regression tests were added before production changes at `10ac51201afb59d677ed546dd7e32ebc7373031f` and `12f4cc8bae8aa1a324ba5238d593acb53e59dd8c`. Production was then repaired at `798ac3672812c5263a44de09a610e0b5cb9a7efc`: disabling an item now invalidates its dependent purpose/path facts, and preview status uses the same trimmed-purpose completeness contract as review. Architecture, TRD, ADR-0001 and CHANGELOG were reconciled afterward. + +A separate buyer-facing workflow review found that the enabled publication-area action exposed an internal implementation boundary (`백엔드`/repository connectivity) even though public URL publication is not shipped. That violated the product-copy boundary and gave the operator no useful next step. A regression was added first at `fcd75aa98242158d0abe5d8beb661dd0ff064011` to require a customer-facing readiness action and reject internal-boundary terms. Production was repaired at `9d86a1316b03455c740c4b70296f604c52d9981f`: the CTA is now `공개 준비 확인`, and the completion message directs the operator to responsible review and fact completion rather than describing backend implementation. `d649604bf41e6618c8deeba5a83d1b8b973355d6` reconciled the CHANGELOG. Because every subsequent documentation commit moves the PR head, live Checks/reviews must be reacquired on the resulting exact current head; predecessor-head conclusions are not merge evidence. ### 2026-09-02 failed-Check RCA: responsive CSS test SAST @@ -30,13 +32,13 @@ Required Noema Review run `33555806837`, job `100016218520` was also independent | --- | --- | --- | --- | --- | --- | | Guided authoring | PRD, ADR-0002, seven routed editors in `src/App.tsx`; step-2 collection-path/step-3 purpose separation; disabled-item dependent-fact invalidation | Functional foundation | Retention, third-party transfer, international transfer and contact steps currently capture thin facts rather than requirement-complete domain contracts | Policy Fact Authoring: add typed facts and deterministic validations from authoritative requirements | Unit/integration tests for valid, missing, contradictory and not-applicable cases | | Processing-purpose review | `src/policy.ts`, `src/policy.test.ts`, `src/App.test.tsx`; whitespace normalization shared by review and preview | Repaired | Missing and whitespace-only purposes block review consistently, but the rule set covers only purpose completeness | Review & Publication: extend evidence-bound review rules without heuristic scoring | Exact-head tests plus rule/source traceability | -| Review workspace | Live preview, warning-to-source navigation, responsive CSS fixes | Implemented foundation | No browser screenshot matrix or realistic interaction evidence | UX: add Playwright + accessibility harness | Desktop/tablet/mobile screenshots, keyboard/focus and action-edge verification | +| Review workspace | Live preview, warning-to-source navigation, responsive CSS fixes, buyer-facing readiness guidance | Implemented foundation | No browser screenshot matrix or realistic interaction evidence; some pre-release controls remain thin affordances rather than complete persisted workflows | UX: add Playwright + accessibility harness and remove/complete non-functional affordances as each workflow is implemented | Desktop/tablet/mobile screenshots, keyboard/focus and action-edge verification | | Accessibility | Semantic controls, focus-visible behavior, keyboard-operable step navigation | Partial | No automated WCAG 2.2 evidence or screen-reader contract | UX: add axe/Playwright and manual interaction record | WCAG audit and browser matrix | | Policy model | ADR-0001, ARCHITECTURE, TRD | Designed, browser-memory only | No versioned durable schema, migration, audit history, UPSERT/idempotency or 3NF implementation | Platform: define `policy_revision` schema and item-level conflict contracts before storage | Schema/contract tests and migration round trip | -| Publication | UI communicates that hosted publication is not yet shipped; architecture defines immutable `publication_revision` direction | Planned | No authenticated review approval, immutable publication, supersession or public URL lifecycle | Review & Publication: implement only after persistence/security entry criteria | Authorization, replay/digest, supersession and rollback tests | +| Publication | CTA truthfully exposes a readiness check rather than pretending to publish; architecture defines immutable `publication_revision` direction | Planned | No authenticated review approval, immutable publication, supersession or public URL lifecycle | Review & Publication: implement only after persistence/security entry criteria | Authorization, replay/digest, supersession and rollback tests | | Legal/rule traceability | `docs/research-traceability.md`; official PIPA/PIPC source register | Source baseline established | No article/section-level requirement mapping or versioned legal-rule engine | Legal Source Registry: map each implemented rule to source/effective date/revision | Fixtures across current and future-effective rule-set snapshots | | Security/privacy | `docs/SECURITY.md`; local-first current runtime | Baseline documented | Hosted tenant model, secrets boundary, encryption/key handling, audit and incident/retention evidence absent | Platform/Security: threat-model hosted boundary before backend | Security tests and exact-head org scans | -| Tests | Vitest workflow, routing/navigation/blocker regressions, whitespace-purpose edge case, collection-path/purpose separation, disable/re-enable stale-evidence invalidation, preview/review whitespace consistency, responsive CSS regression | Improved | Browser E2E, accessibility and broader domain edge cases remain incomplete; 100% coverage not evidenced | Test Engineering: expand behavior-first tests | Exact-head coverage and browser evidence | +| Tests | Vitest workflow, routing/navigation/blocker regressions, whitespace-purpose edge case, collection-path/purpose separation, disable/re-enable stale-evidence invalidation, preview/review whitespace consistency, buyer-copy boundary, responsive CSS regression | Improved | Browser E2E, accessibility and broader domain edge cases remain incomplete; 100% coverage not evidenced | Test Engineering: expand behavior-first tests | Exact-head coverage and browser evidence | | Performance | Static Vite client | Unevidenced | No page performance or network/load evidence | Operability: baseline browser performance; add k6 only with backend | Realistic measurements before latency claims | | CI/security merge gate | SHA-pinned checkout plus repo and central required workflows | External live gate | Dependency Review can fail closed when GitHub's exact base/head dependency-graph compare is unavailable; review/model providers can also fail closed without publishable evidence | Control plane: repair incidents centrally and never substitute sibling scanners or predecessor evidence | Exact-head assigned jobs must execute; independent approval and threads required | @@ -48,11 +50,11 @@ Ubiquitous language: `policy_revision`, `service_profile`, `collection_item`, `p The future aggregate root is the minimal `policy_revision` boundary. Rendered prose is a read projection. `publication_revision` is immutable and explicitly superseded rather than updated. Relational persistence is 3NF by default; named persistence objects use at least two semantic words and `snake_case`, and item-level UPSERT/idempotency semantics must be explicit. ## Buyer-visible release gates -A publishable release requires complete fact-to-warning navigation, deterministic rendering, explicit unresolved-review blocking, immutable reviewed publication revisions, accessible responsive interaction, authoritative source/version traceability, hosted security/privacy evidence, and exact-head CI/security/independent review. Buyer-facing wording describes assistance and reviewability, never a compliance guarantee. +A publishable release requires complete fact-to-warning navigation, deterministic rendering, explicit unresolved-review blocking, immutable reviewed publication revisions, accessible responsive interaction, authoritative source/version traceability, hosted security/privacy evidence, and exact-head CI/security/independent review. Buyer-facing wording describes assistance and reviewability, never a compliance guarantee or an internal implementation boundary. ## Active commercialization order 1. Reacquire exact-current-head CI/security/SAST/central workflow execution and independent approval; merge PR #1 only through ordinary protection. -2. Add browser-level responsive/accessibility evidence for the repaired seven-step workflow. +2. Add browser-level responsive/accessibility evidence for the repaired seven-step workflow and complete or remove remaining non-functional pre-release affordances. 3. Convert retention/transfer/contact placeholders into typed domain facts and source-backed deterministic validations. 4. Define and test the versioned `policy_revision` persistence/audit schema and explicit item-level UPSERT contracts. 5. Implement secure review/publication with immutable releases and explicit supersession. From cc9c8a07881d1a06c81a92c82161b469fa52508c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 11:31:25 +0900 Subject: [PATCH 060/282] test: require service URL in review draft --- src/App.test.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/App.test.tsx b/src/App.test.tsx index e8f36d3..d2a9403 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -16,6 +16,13 @@ describe('policy editing workflow', () => { } }) + it('서비스 URL을 입력하면 검토본의 적용 서비스 정보에 반영한다', () => { + const { container } = render() + fireEvent.click(container.querySelectorAll('.rail li button')[0]) + fireEvent.change(container.querySelector('input[name="serviceUrl"]')!, { target: { value: 'https://privacy.example.test' } }) + expect(container.querySelector('.paper')?.textContent).toContain('https://privacy.example.test') + }) + it('수집 단계에서는 선택 항목의 수집 경로를 항상 편집하고 처리 목적은 다음 단계에서 편집한다', () => { const { container } = render() expect(container.querySelectorAll('input[placeholder="예: 회원가입 화면"]')).toHaveLength(3) From 815bec382af557e8dc9550329d8c71586eddd297 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 11:32:26 +0900 Subject: [PATCH 061/282] fix: project service URL into review draft --- src/App.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/App.tsx b/src/App.tsx index 35e20a2..cf3b0b6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -150,6 +150,7 @@ function DocumentPreview({ items, facts, setCurrent }: { items: PolicyItem[]; fa
근거 법령 개인정보 보호법{review.blocking.length ? `검토 필요 ${review.blocking.length}` : '필수 확인 완료'}버전 0.1.0

{facts.serviceName || '개인정보처리방침'} (검토본)

+ {facts.serviceUrl &&

적용 서비스: {facts.serviceUrl}

}

{facts.serviceName || '서비스 운영자'}는 이용자의 개인정보를 중요하게 여기며, 확인된 실제 처리 사실을 바탕으로 다음 사항을 검토합니다.

제1조 (개인정보의 처리 목적)

아래 목적은 작성자가 확인한 운영 사실을 기준으로 표시됩니다.

From 4ceaf822d37d19957637c9679d7884b552e5edb8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 11:32:40 +0900 Subject: [PATCH 062/282] docs: record service URL review projection --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f994fa..3114f19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,11 @@ All notable product changes are recorded here. PolicyWeave is pre-release; entri ### Added - Seven-step authoring workflow with distinct editing surfaces for service information, collection items, processing purposes, retention, third-party transfer, international transfer, and privacy contact. - Warning-to-source navigation for missing processing purposes. -- Regression coverage for all seven step routes, warning navigation, clearing the publication blocker after a missing purpose is supplied, whitespace-only purposes, collection-path/purpose step separation, disable/re-enable stale-evidence invalidation, preview/review whitespace consistency, and buyer-facing publication guidance that does not expose implementation boundaries. +- Regression coverage for all seven step routes, service URL projection into the review draft, warning navigation, clearing the publication blocker after a missing purpose is supplied, whitespace-only purposes, collection-path/purpose step separation, disable/re-enable stale-evidence invalidation, preview/review whitespace consistency, and buyer-facing publication guidance that does not expose implementation boundaries. - Product/technical gap ledger, architecture, technical requirements, security baseline, and legal-source traceability. ### Changed +- Service URL entered in the service-information step is now projected into the review draft instead of being silently omitted. - Collection-path editing now remains available for every selected item in the collection step, while processing-purpose editing stays in its dedicated purpose step. - Disabling a collection item now clears its dependent processing-purpose and collection-path evidence so re-enabling it cannot silently revive stale policy facts. - Review preview now applies the same whitespace-normalized purpose-completeness contract as the blocking review rule. From 01d661a9f3ca65b682d171b491febc97e03bf6ef Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 11:33:24 +0900 Subject: [PATCH 063/282] docs: reconcile service URL projection repair --- docs/product-technical-gap-baseline.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 24fe00b..993e532 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -14,7 +14,9 @@ A later exact-head review found that collection-path editing was incorrectly cou On exact PR head `2d4c956b2b7b4cc3607c2724a4be2073393dc692`, two additional policy-fact integrity gaps were reproduced from the current implementation. First, disabling an enabled collection item preserved its processing purpose and collection path, so later re-enabling silently revived stale evidence without renewed review. Second, the review engine correctly treated a whitespace-only purpose as missing while the preview used string truthiness and displayed the same whitespace value as entered. Regression tests were added before production changes at `10ac51201afb59d677ed546dd7e32ebc7373031f` and `12f4cc8bae8aa1a324ba5238d593acb53e59dd8c`. Production was then repaired at `798ac3672812c5263a44de09a610e0b5cb9a7efc`: disabling an item now invalidates its dependent purpose/path facts, and preview status uses the same trimmed-purpose completeness contract as review. Architecture, TRD, ADR-0001 and CHANGELOG were reconciled afterward. -A separate buyer-facing workflow review found that the enabled publication-area action exposed an internal implementation boundary (`백엔드`/repository connectivity) even though public URL publication is not shipped. That violated the product-copy boundary and gave the operator no useful next step. A regression was added first at `fcd75aa98242158d0abe5d8beb661dd0ff064011` to require a customer-facing readiness action and reject internal-boundary terms. Production was repaired at `9d86a1316b03455c740c4b70296f604c52d9981f`: the CTA is now `공개 준비 확인`, and the completion message directs the operator to responsible review and fact completion rather than describing backend implementation. `d649604bf41e6618c8deeba5a83d1b8b973355d6` reconciled the CHANGELOG. Because every subsequent documentation commit moves the PR head, live Checks/reviews must be reacquired on the resulting exact current head; predecessor-head conclusions are not merge evidence. +A separate buyer-facing workflow review found that the enabled publication-area action exposed an internal implementation boundary (`백엔드`/repository connectivity) even though public URL publication is not shipped. That violated the product-copy boundary and gave the operator no useful next step. A regression was added first at `fcd75aa98242158d0abe5d8beb661dd0ff064011` to require a customer-facing readiness action and reject internal-boundary terms. Production was repaired at `9d86a1316b03455c740c4b70296f604c52d9981f`: the CTA is now `공개 준비 확인`, and the completion message directs the operator to responsible review and fact completion rather than describing backend implementation. `d649604bf41e6618c8deeba5a83d1b8b973355d6` reconciled the CHANGELOG. + +The live review corpus also contained a still-valid finding that step 1 captured `serviceUrl` but `DocumentPreview` omitted it, so one of the structured source facts disappeared from the buyer-visible deterministic projection. The regression was added first at `cc9c8a07881d1a06c81a92c82161b469fa52508c`; production was repaired at `815bec382af557e8dc9550329d8c71586eddd297` by rendering the entered URL as plain escaped text in the review draft, avoiding creation of an unvalidated navigation target. `4ceaf822d37d19957637c9679d7884b552e5edb8` reconciled the CHANGELOG. Because every subsequent documentation commit moves the PR head, live Checks/reviews must be reacquired on the resulting exact current head; predecessor-head conclusions are not merge evidence. ### 2026-09-02 failed-Check RCA: responsive CSS test SAST @@ -30,15 +32,15 @@ Required Noema Review run `33555806837`, job `100016218520` was also independent | Area | Evidence | Status | Commercialization gap | Owner/action | Next verification | | --- | --- | --- | --- | --- | --- | -| Guided authoring | PRD, ADR-0002, seven routed editors in `src/App.tsx`; step-2 collection-path/step-3 purpose separation; disabled-item dependent-fact invalidation | Functional foundation | Retention, third-party transfer, international transfer and contact steps currently capture thin facts rather than requirement-complete domain contracts | Policy Fact Authoring: add typed facts and deterministic validations from authoritative requirements | Unit/integration tests for valid, missing, contradictory and not-applicable cases | +| Guided authoring | PRD, ADR-0002, seven routed editors in `src/App.tsx`; service URL preserved into review projection; step-2 collection-path/step-3 purpose separation; disabled-item dependent-fact invalidation | Functional foundation | Retention, third-party transfer, international transfer and contact steps currently capture thin facts rather than requirement-complete domain contracts | Policy Fact Authoring: add typed facts and deterministic validations from authoritative requirements | Unit/integration tests for valid, missing, contradictory and not-applicable cases | | Processing-purpose review | `src/policy.ts`, `src/policy.test.ts`, `src/App.test.tsx`; whitespace normalization shared by review and preview | Repaired | Missing and whitespace-only purposes block review consistently, but the rule set covers only purpose completeness | Review & Publication: extend evidence-bound review rules without heuristic scoring | Exact-head tests plus rule/source traceability | -| Review workspace | Live preview, warning-to-source navigation, responsive CSS fixes, buyer-facing readiness guidance | Implemented foundation | No browser screenshot matrix or realistic interaction evidence; some pre-release controls remain thin affordances rather than complete persisted workflows | UX: add Playwright + accessibility harness and remove/complete non-functional affordances as each workflow is implemented | Desktop/tablet/mobile screenshots, keyboard/focus and action-edge verification | +| Review workspace | Live preview, service URL projection, warning-to-source navigation, responsive CSS fixes, buyer-facing readiness guidance | Implemented foundation | No browser screenshot matrix or realistic interaction evidence; some pre-release controls remain thin affordances rather than complete persisted workflows | UX: add Playwright + accessibility harness and remove/complete non-functional affordances as each workflow is implemented | Desktop/tablet/mobile screenshots, keyboard/focus and action-edge verification | | Accessibility | Semantic controls, focus-visible behavior, keyboard-operable step navigation | Partial | No automated WCAG 2.2 evidence or screen-reader contract | UX: add axe/Playwright and manual interaction record | WCAG audit and browser matrix | | Policy model | ADR-0001, ARCHITECTURE, TRD | Designed, browser-memory only | No versioned durable schema, migration, audit history, UPSERT/idempotency or 3NF implementation | Platform: define `policy_revision` schema and item-level conflict contracts before storage | Schema/contract tests and migration round trip | | Publication | CTA truthfully exposes a readiness check rather than pretending to publish; architecture defines immutable `publication_revision` direction | Planned | No authenticated review approval, immutable publication, supersession or public URL lifecycle | Review & Publication: implement only after persistence/security entry criteria | Authorization, replay/digest, supersession and rollback tests | | Legal/rule traceability | `docs/research-traceability.md`; official PIPA/PIPC source register | Source baseline established | No article/section-level requirement mapping or versioned legal-rule engine | Legal Source Registry: map each implemented rule to source/effective date/revision | Fixtures across current and future-effective rule-set snapshots | | Security/privacy | `docs/SECURITY.md`; local-first current runtime | Baseline documented | Hosted tenant model, secrets boundary, encryption/key handling, audit and incident/retention evidence absent | Platform/Security: threat-model hosted boundary before backend | Security tests and exact-head org scans | -| Tests | Vitest workflow, routing/navigation/blocker regressions, whitespace-purpose edge case, collection-path/purpose separation, disable/re-enable stale-evidence invalidation, preview/review whitespace consistency, buyer-copy boundary, responsive CSS regression | Improved | Browser E2E, accessibility and broader domain edge cases remain incomplete; 100% coverage not evidenced | Test Engineering: expand behavior-first tests | Exact-head coverage and browser evidence | +| Tests | Vitest workflow, routing/navigation/blocker regressions, service URL projection, whitespace-purpose edge case, collection-path/purpose separation, disable/re-enable stale-evidence invalidation, preview/review whitespace consistency, buyer-copy boundary, responsive CSS regression | Improved | Browser E2E, accessibility and broader domain edge cases remain incomplete; 100% coverage not evidenced | Test Engineering: expand behavior-first tests | Exact-head coverage and browser evidence | | Performance | Static Vite client | Unevidenced | No page performance or network/load evidence | Operability: baseline browser performance; add k6 only with backend | Realistic measurements before latency claims | | CI/security merge gate | SHA-pinned checkout plus repo and central required workflows | External live gate | Dependency Review can fail closed when GitHub's exact base/head dependency-graph compare is unavailable; review/model providers can also fail closed without publishable evidence | Control plane: repair incidents centrally and never substitute sibling scanners or predecessor evidence | Exact-head assigned jobs must execute; independent approval and threads required | @@ -50,7 +52,7 @@ Ubiquitous language: `policy_revision`, `service_profile`, `collection_item`, `p The future aggregate root is the minimal `policy_revision` boundary. Rendered prose is a read projection. `publication_revision` is immutable and explicitly superseded rather than updated. Relational persistence is 3NF by default; named persistence objects use at least two semantic words and `snake_case`, and item-level UPSERT/idempotency semantics must be explicit. ## Buyer-visible release gates -A publishable release requires complete fact-to-warning navigation, deterministic rendering, explicit unresolved-review blocking, immutable reviewed publication revisions, accessible responsive interaction, authoritative source/version traceability, hosted security/privacy evidence, and exact-head CI/security/independent review. Buyer-facing wording describes assistance and reviewability, never a compliance guarantee or an internal implementation boundary. +A publishable release requires complete fact-to-warning navigation, deterministic rendering of captured source facts, explicit unresolved-review blocking, immutable reviewed publication revisions, accessible responsive interaction, authoritative source/version traceability, hosted security/privacy evidence, and exact-head CI/security/independent review. Buyer-facing wording describes assistance and reviewability, never a compliance guarantee or an internal implementation boundary. ## Active commercialization order 1. Reacquire exact-current-head CI/security/SAST/central workflow execution and independent approval; merge PR #1 only through ordinary protection. From f21c65a589c73c9b020696a5eda8240249581f3c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 11:34:53 +0900 Subject: [PATCH 064/282] test: prevent dead pre-release affordances --- src/App.test.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/App.test.tsx b/src/App.test.tsx index d2a9403..9641dff 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -23,6 +23,15 @@ describe('policy editing workflow', () => { expect(container.querySelector('.paper')?.textContent).toContain('https://privacy.example.test') }) + it('아직 제공하지 않는 내보내기와 생성 기능을 클릭 가능한 동작처럼 노출하지 않는다', () => { + const { container } = render() + const exportButton = Array.from(container.querySelectorAll('button')).find((button) => button.textContent?.includes('JSON 내보내기')) + expect(exportButton?.disabled).toBe(true) + expect(exportButton?.textContent).toContain('준비 중') + expect(Array.from(container.querySelectorAll('.preview button')).some((button) => button.textContent?.includes('검토본 생성'))).toBe(false) + expect(container.querySelector('.preview')?.textContent).toContain('입력 내용은 검토본에 즉시 반영됩니다') + }) + it('수집 단계에서는 선택 항목의 수집 경로를 항상 편집하고 처리 목적은 다음 단계에서 편집한다', () => { const { container } = render() expect(container.querySelectorAll('input[placeholder="예: 회원가입 화면"]')).toHaveLength(3) From f00a6ad022c49ab7e125d2186c335462ec9bb2e2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 11:35:43 +0900 Subject: [PATCH 065/282] fix: make unshipped affordances non-deceptive --- src/App.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index cf3b0b6..0e1a1e5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -164,8 +164,7 @@ function DocumentPreview({ items, facts, setCurrent }: { items: PolicyItem[]; fa

제4조 (국외 이전)

{facts.internationalCountry || facts.internationalRecipient ? `${facts.internationalCountry || '국가 확인 필요'} · ${facts.internationalRecipient || '수령자 확인 필요'}` : '국외 이전 여부를 확인하는 단계가 남아 있습니다.'}

개인정보 보호 문의

{facts.privacyOfficerName || '담당자 확인 필요'} · {facts.privacyOfficerEmail || '연락처 확인 필요'}

- - 생성된 문서는 법률 자문이 아닙니다. 공개 전 책임자의 검토가 필요합니다. + 입력 내용은 검토본에 즉시 반영됩니다. 생성된 문서는 법률 자문이 아닙니다. 공개 전 책임자의 검토가 필요합니다.
} @@ -177,7 +176,7 @@ export default function App() { const [message, setMessage] = useState('') function publish() { setMessage(review.blocking.length ? '필수 확인 항목을 먼저 입력하세요.' : '필수 확인이 완료되었습니다. 현재 검토본을 책임자와 검토하고 필요한 사실을 보완하세요.') } return
-
PolicyWeave작성 중버전 0.1.0 (임시저장) 브라우저 작업 중
+
PolicyWeave작성 중버전 0.1.0 (임시저장) 브라우저 작업 중
검토 요약확인을 마친 뒤 공개 준비 상태를 확인하세요.
필수 확인 {review.blocking.length}건
권장 검토 {review.recommended.length}건
{message}
From 8c9eaa384fb14185d620866ab01b6fb261ceca05 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 11:35:56 +0900 Subject: [PATCH 066/282] docs: record non-deceptive pre-release affordances --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3114f19..fe760a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ All notable product changes are recorded here. PolicyWeave is pre-release; entri ### Added - Seven-step authoring workflow with distinct editing surfaces for service information, collection items, processing purposes, retention, third-party transfer, international transfer, and privacy contact. - Warning-to-source navigation for missing processing purposes. -- Regression coverage for all seven step routes, service URL projection into the review draft, warning navigation, clearing the publication blocker after a missing purpose is supplied, whitespace-only purposes, collection-path/purpose step separation, disable/re-enable stale-evidence invalidation, preview/review whitespace consistency, and buyer-facing publication guidance that does not expose implementation boundaries. +- Regression coverage for all seven step routes, service URL projection into the review draft, warning navigation, clearing the publication blocker after a missing purpose is supplied, whitespace-only purposes, collection-path/purpose step separation, disable/re-enable stale-evidence invalidation, preview/review whitespace consistency, buyer-facing publication guidance, and non-deceptive handling of unshipped export/generation affordances. - Product/technical gap ledger, architecture, technical requirements, security baseline, and legal-source traceability. ### Changed @@ -16,6 +16,7 @@ All notable product changes are recorded here. PolicyWeave is pre-release; entri - Disabling a collection item now clears its dependent processing-purpose and collection-path evidence so re-enabling it cannot silently revive stale policy facts. - Review preview now applies the same whitespace-normalized purpose-completeness contract as the blocking review rule. - The publication-area CTA now describes a readiness check and directs the operator to responsible review rather than exposing repository/backend implementation details for an unshipped capability. +- Unshipped JSON export is visibly disabled as `준비 중`, and the redundant no-op `검토본 생성` control was removed because the review draft already updates live. - Review preview now reflects verified authoring facts instead of presenting the collection step as the entire workflow. - Responsive review behavior and mobile publication feedback were repaired during PR review. - Responsive CSS contract tests now use only literal media-query regular expressions, removing the Semgrep dynamic-RegExp SAST finding without suppressing or weakening the scanner gate. From ae947acc93994ade05be86fbbf22746f06bda54f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 11:36:34 +0900 Subject: [PATCH 067/282] docs: reconcile pre-release affordance repair --- docs/product-technical-gap-baseline.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 993e532..7b70627 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -16,7 +16,9 @@ On exact PR head `2d4c956b2b7b4cc3607c2724a4be2073393dc692`, two additional poli A separate buyer-facing workflow review found that the enabled publication-area action exposed an internal implementation boundary (`백엔드`/repository connectivity) even though public URL publication is not shipped. That violated the product-copy boundary and gave the operator no useful next step. A regression was added first at `fcd75aa98242158d0abe5d8beb661dd0ff064011` to require a customer-facing readiness action and reject internal-boundary terms. Production was repaired at `9d86a1316b03455c740c4b70296f604c52d9981f`: the CTA is now `공개 준비 확인`, and the completion message directs the operator to responsible review and fact completion rather than describing backend implementation. `d649604bf41e6618c8deeba5a83d1b8b973355d6` reconciled the CHANGELOG. -The live review corpus also contained a still-valid finding that step 1 captured `serviceUrl` but `DocumentPreview` omitted it, so one of the structured source facts disappeared from the buyer-visible deterministic projection. The regression was added first at `cc9c8a07881d1a06c81a92c82161b469fa52508c`; production was repaired at `815bec382af557e8dc9550329d8c71586eddd297` by rendering the entered URL as plain escaped text in the review draft, avoiding creation of an unvalidated navigation target. `4ceaf822d37d19957637c9679d7884b552e5edb8` reconciled the CHANGELOG. Because every subsequent documentation commit moves the PR head, live Checks/reviews must be reacquired on the resulting exact current head; predecessor-head conclusions are not merge evidence. +The live review corpus also contained a still-valid finding that step 1 captured `serviceUrl` but `DocumentPreview` omitted it, so one of the structured source facts disappeared from the buyer-visible deterministic projection. The regression was added first at `cc9c8a07881d1a06c81a92c82161b469fa52508c`; production was repaired at `815bec382af557e8dc9550329d8c71586eddd297` by rendering the entered URL as plain escaped text in the review draft, avoiding creation of an unvalidated navigation target. `4ceaf822d37d19957637c9679d7884b552e5edb8` reconciled the CHANGELOG. + +A final buyer-workflow pass found two controls that appeared actionable despite having no implementation: the `JSON 내보내기 준비` button and a redundant `검토본 생성` button even though the review draft updates continuously. The regression was added first at `f21c65a589c73c9b020696a5eda8240249581f3c`, requiring the export control to be explicitly disabled/marked `준비 중`, requiring the no-op generation button to be absent, and requiring live-update guidance. Production was repaired at `f00a6ad022c49ab7e125d2186c335462ec9bb2e2`, and `8c9eaa384fb14185d620866ab01b6fb261ceca05` reconciled the CHANGELOG. This removes deceptive pre-release affordances without claiming that export or hosted publication exists. Because every subsequent documentation commit moves the PR head, live Checks/reviews must be reacquired on the resulting exact current head; predecessor-head conclusions are not merge evidence. ### 2026-09-02 failed-Check RCA: responsive CSS test SAST @@ -34,13 +36,13 @@ Required Noema Review run `33555806837`, job `100016218520` was also independent | --- | --- | --- | --- | --- | --- | | Guided authoring | PRD, ADR-0002, seven routed editors in `src/App.tsx`; service URL preserved into review projection; step-2 collection-path/step-3 purpose separation; disabled-item dependent-fact invalidation | Functional foundation | Retention, third-party transfer, international transfer and contact steps currently capture thin facts rather than requirement-complete domain contracts | Policy Fact Authoring: add typed facts and deterministic validations from authoritative requirements | Unit/integration tests for valid, missing, contradictory and not-applicable cases | | Processing-purpose review | `src/policy.ts`, `src/policy.test.ts`, `src/App.test.tsx`; whitespace normalization shared by review and preview | Repaired | Missing and whitespace-only purposes block review consistently, but the rule set covers only purpose completeness | Review & Publication: extend evidence-bound review rules without heuristic scoring | Exact-head tests plus rule/source traceability | -| Review workspace | Live preview, service URL projection, warning-to-source navigation, responsive CSS fixes, buyer-facing readiness guidance | Implemented foundation | No browser screenshot matrix or realistic interaction evidence; some pre-release controls remain thin affordances rather than complete persisted workflows | UX: add Playwright + accessibility harness and remove/complete non-functional affordances as each workflow is implemented | Desktop/tablet/mobile screenshots, keyboard/focus and action-edge verification | +| Review workspace | Live preview, service URL projection, warning-to-source navigation, responsive CSS fixes, buyer-facing readiness guidance; no-op generation control removed and unshipped export explicitly disabled | Implemented foundation | No browser screenshot matrix or realistic interaction evidence | UX: add Playwright + accessibility harness and verify remaining interaction edges | Desktop/tablet/mobile screenshots, keyboard/focus and action-edge verification | | Accessibility | Semantic controls, focus-visible behavior, keyboard-operable step navigation | Partial | No automated WCAG 2.2 evidence or screen-reader contract | UX: add axe/Playwright and manual interaction record | WCAG audit and browser matrix | | Policy model | ADR-0001, ARCHITECTURE, TRD | Designed, browser-memory only | No versioned durable schema, migration, audit history, UPSERT/idempotency or 3NF implementation | Platform: define `policy_revision` schema and item-level conflict contracts before storage | Schema/contract tests and migration round trip | | Publication | CTA truthfully exposes a readiness check rather than pretending to publish; architecture defines immutable `publication_revision` direction | Planned | No authenticated review approval, immutable publication, supersession or public URL lifecycle | Review & Publication: implement only after persistence/security entry criteria | Authorization, replay/digest, supersession and rollback tests | | Legal/rule traceability | `docs/research-traceability.md`; official PIPA/PIPC source register | Source baseline established | No article/section-level requirement mapping or versioned legal-rule engine | Legal Source Registry: map each implemented rule to source/effective date/revision | Fixtures across current and future-effective rule-set snapshots | | Security/privacy | `docs/SECURITY.md`; local-first current runtime | Baseline documented | Hosted tenant model, secrets boundary, encryption/key handling, audit and incident/retention evidence absent | Platform/Security: threat-model hosted boundary before backend | Security tests and exact-head org scans | -| Tests | Vitest workflow, routing/navigation/blocker regressions, service URL projection, whitespace-purpose edge case, collection-path/purpose separation, disable/re-enable stale-evidence invalidation, preview/review whitespace consistency, buyer-copy boundary, responsive CSS regression | Improved | Browser E2E, accessibility and broader domain edge cases remain incomplete; 100% coverage not evidenced | Test Engineering: expand behavior-first tests | Exact-head coverage and browser evidence | +| Tests | Vitest workflow, routing/navigation/blocker regressions, service URL projection, whitespace-purpose edge case, collection-path/purpose separation, disable/re-enable stale-evidence invalidation, preview/review whitespace consistency, buyer-copy boundary, dead-affordance regression, responsive CSS regression | Improved | Browser E2E, accessibility and broader domain edge cases remain incomplete; 100% coverage not evidenced | Test Engineering: expand behavior-first tests | Exact-head coverage and browser evidence | | Performance | Static Vite client | Unevidenced | No page performance or network/load evidence | Operability: baseline browser performance; add k6 only with backend | Realistic measurements before latency claims | | CI/security merge gate | SHA-pinned checkout plus repo and central required workflows | External live gate | Dependency Review can fail closed when GitHub's exact base/head dependency-graph compare is unavailable; review/model providers can also fail closed without publishable evidence | Control plane: repair incidents centrally and never substitute sibling scanners or predecessor evidence | Exact-head assigned jobs must execute; independent approval and threads required | @@ -56,7 +58,7 @@ A publishable release requires complete fact-to-warning navigation, deterministi ## Active commercialization order 1. Reacquire exact-current-head CI/security/SAST/central workflow execution and independent approval; merge PR #1 only through ordinary protection. -2. Add browser-level responsive/accessibility evidence for the repaired seven-step workflow and complete or remove remaining non-functional pre-release affordances. +2. Add browser-level responsive/accessibility evidence for the repaired seven-step workflow and verify all remaining action edges. 3. Convert retention/transfer/contact placeholders into typed domain facts and source-backed deterministic validations. 4. Define and test the versioned `policy_revision` persistence/audit schema and explicit item-level UPSERT contracts. 5. Implement secure review/publication with immutable releases and explicit supersession. From 8233fe09271c3fb0b172892cd0f2fc953a1d84de Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 11:39:17 +0900 Subject: [PATCH 068/282] test: prevent inert document selector affordance --- 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 9641dff..e3bb33d 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -25,9 +25,12 @@ describe('policy editing workflow', () => { it('아직 제공하지 않는 내보내기와 생성 기능을 클릭 가능한 동작처럼 노출하지 않는다', () => { const { container } = render() - const exportButton = Array.from(container.querySelectorAll('button')).find((button) => button.textContent?.includes('JSON 내보내기')) + 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(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) expect(container.querySelector('.preview')?.textContent).toContain('입력 내용은 검토본에 즉시 반영됩니다') }) From c385196a3c9036fa7580ae58ac624b609f96c083 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 11:40:13 +0900 Subject: [PATCH 069/282] fix: render document title as non-interactive status --- src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/App.tsx b/src/App.tsx index 0e1a1e5..a3bfdd3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -176,7 +176,7 @@ export default function App() { const [message, setMessage] = useState('') function publish() { setMessage(review.blocking.length ? '필수 확인 항목을 먼저 입력하세요.' : '필수 확인이 완료되었습니다. 현재 검토본을 책임자와 검토하고 필요한 사실을 보완하세요.') } return
-
PolicyWeave작성 중버전 0.1.0 (임시저장) 브라우저 작업 중
+
PolicyWeave{facts.serviceName || '내 서비스'} 개인정보처리방침작성 중버전 0.1.0 (임시저장) 브라우저 작업 중
검토 요약확인을 마친 뒤 공개 준비 상태를 확인하세요.
필수 확인 {review.blocking.length}건
권장 검토 {review.recommended.length}건
{message}
From 2d4f1def55bc9f367e3fe20eac2b81199c1b6294 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 11:40:28 +0900 Subject: [PATCH 070/282] docs: record document title affordance repair --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe760a0..092e075 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ All notable product changes are recorded here. PolicyWeave is pre-release; entri ### Added - Seven-step authoring workflow with distinct editing surfaces for service information, collection items, processing purposes, retention, third-party transfer, international transfer, and privacy contact. - Warning-to-source navigation for missing processing purposes. -- Regression coverage for all seven step routes, service URL projection into the review draft, warning navigation, clearing the publication blocker after a missing purpose is supplied, whitespace-only purposes, collection-path/purpose step separation, disable/re-enable stale-evidence invalidation, preview/review whitespace consistency, buyer-facing publication guidance, and non-deceptive handling of unshipped export/generation affordances. +- Regression coverage for all seven step routes, service URL projection into the review draft, warning navigation, clearing the publication blocker after a missing purpose is supplied, whitespace-only purposes, collection-path/purpose step separation, disable/re-enable stale-evidence invalidation, preview/review whitespace consistency, buyer-facing publication guidance, and non-deceptive handling of unshipped export/generation/document-selection affordances. - Product/technical gap ledger, architecture, technical requirements, security baseline, and legal-source traceability. ### Changed @@ -16,7 +16,7 @@ All notable product changes are recorded here. PolicyWeave is pre-release; entri - Disabling a collection item now clears its dependent processing-purpose and collection-path evidence so re-enabling it cannot silently revive stale policy facts. - Review preview now applies the same whitespace-normalized purpose-completeness contract as the blocking review rule. - The publication-area CTA now describes a readiness check and directs the operator to responsible review rather than exposing repository/backend implementation details for an unshipped capability. -- Unshipped JSON export is visibly disabled as `준비 중`, and the redundant no-op `검토본 생성` control was removed because the review draft already updates live. +- Unshipped JSON export is visibly disabled as `준비 중`, the redundant no-op `검토본 생성` control was removed because the review draft already updates live, and the document title is rendered as status text rather than an inert dropdown-like button. - Review preview now reflects verified authoring facts instead of presenting the collection step as the entire workflow. - Responsive review behavior and mobile publication feedback were repaired during PR review. - Responsive CSS contract tests now use only literal media-query regular expressions, removing the Semgrep dynamic-RegExp SAST finding without suppressing or weakening the scanner gate. From 8ddb45d5e4868d0b7624d551f6229ec27fafbb5f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 11:42:23 +0900 Subject: [PATCH 071/282] docs: reconcile document-title affordance repair --- docs/product-technical-gap-baseline.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 7b70627..c6521a4 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -18,7 +18,9 @@ A separate buyer-facing workflow review found that the enabled publication-area The live review corpus also contained a still-valid finding that step 1 captured `serviceUrl` but `DocumentPreview` omitted it, so one of the structured source facts disappeared from the buyer-visible deterministic projection. The regression was added first at `cc9c8a07881d1a06c81a92c82161b469fa52508c`; production was repaired at `815bec382af557e8dc9550329d8c71586eddd297` by rendering the entered URL as plain escaped text in the review draft, avoiding creation of an unvalidated navigation target. `4ceaf822d37d19957637c9679d7884b552e5edb8` reconciled the CHANGELOG. -A final buyer-workflow pass found two controls that appeared actionable despite having no implementation: the `JSON 내보내기 준비` button and a redundant `검토본 생성` button even though the review draft updates continuously. The regression was added first at `f21c65a589c73c9b020696a5eda8240249581f3c`, requiring the export control to be explicitly disabled/marked `준비 중`, requiring the no-op generation button to be absent, and requiring live-update guidance. Production was repaired at `f00a6ad022c49ab7e125d2186c335462ec9bb2e2`, and `8c9eaa384fb14185d620866ab01b6fb261ceca05` reconciled the CHANGELOG. This removes deceptive pre-release affordances without claiming that export or hosted publication exists. Because every subsequent documentation commit moves the PR head, live Checks/reviews must be reacquired on the resulting exact current head; predecessor-head conclusions are not merge evidence. +A buyer-workflow pass found two controls that appeared actionable despite having no implementation: the `JSON 내보내기 준비` button and a redundant `검토본 생성` button even though the review draft updates continuously. The regression was added first at `f21c65a589c73c9b020696a5eda8240249581f3c`, requiring the export control to be explicitly disabled/marked `준비 중`, requiring the no-op generation button to be absent, and requiring live-update guidance. Production was repaired at `f00a6ad022c49ab7e125d2186c335462ec9bb2e2`, and `8c9eaa384fb14185d620866ab01b6fb261ceca05` reconciled the CHANGELOG. This removes deceptive pre-release affordances without claiming that export or hosted publication exists. + +The same action-edge sweep found a top-bar document title rendered as a dropdown-like button with a chevron but no click handler, menu, or alternate document workflow. That was another misleading action boundary. A regression was added first at `8233fe09271c3fb0b172892cd0f2fc953a1d84de` to require that the document title is not exposed as a button and is rendered as status text. Production was repaired at `c385196a3c9036fa7580ae58ac624b609f96c083`; the title now remains visible as `.document-name` without implying an unavailable selector. `2d4f1def55bc9f367e3fe20eac2b81199c1b6294` reconciled the CHANGELOG. Because this ledger commit also moves the PR head, live Checks/reviews must be reacquired on the resulting exact current head; predecessor-head conclusions are not merge evidence. ### 2026-09-02 failed-Check RCA: responsive CSS test SAST @@ -36,13 +38,13 @@ Required Noema Review run `33555806837`, job `100016218520` was also independent | --- | --- | --- | --- | --- | --- | | Guided authoring | PRD, ADR-0002, seven routed editors in `src/App.tsx`; service URL preserved into review projection; step-2 collection-path/step-3 purpose separation; disabled-item dependent-fact invalidation | Functional foundation | Retention, third-party transfer, international transfer and contact steps currently capture thin facts rather than requirement-complete domain contracts | Policy Fact Authoring: add typed facts and deterministic validations from authoritative requirements | Unit/integration tests for valid, missing, contradictory and not-applicable cases | | Processing-purpose review | `src/policy.ts`, `src/policy.test.ts`, `src/App.test.tsx`; whitespace normalization shared by review and preview | Repaired | Missing and whitespace-only purposes block review consistently, but the rule set covers only purpose completeness | Review & Publication: extend evidence-bound review rules without heuristic scoring | Exact-head tests plus rule/source traceability | -| Review workspace | Live preview, service URL projection, warning-to-source navigation, responsive CSS fixes, buyer-facing readiness guidance; no-op generation control removed and unshipped export explicitly disabled | Implemented foundation | No browser screenshot matrix or realistic interaction evidence | UX: add Playwright + accessibility harness and verify remaining interaction edges | Desktop/tablet/mobile screenshots, keyboard/focus and action-edge verification | +| Review workspace | Live preview, service URL projection, warning-to-source navigation, responsive CSS fixes, buyer-facing readiness guidance; no-op generation removed, unshipped export explicitly disabled, document title rendered as non-interactive status | Implemented foundation | No browser screenshot matrix or realistic interaction evidence | UX: add Playwright + accessibility harness and verify remaining interaction edges | Desktop/tablet/mobile screenshots, keyboard/focus and action-edge verification | | Accessibility | Semantic controls, focus-visible behavior, keyboard-operable step navigation | Partial | No automated WCAG 2.2 evidence or screen-reader contract | UX: add axe/Playwright and manual interaction record | WCAG audit and browser matrix | | Policy model | ADR-0001, ARCHITECTURE, TRD | Designed, browser-memory only | No versioned durable schema, migration, audit history, UPSERT/idempotency or 3NF implementation | Platform: define `policy_revision` schema and item-level conflict contracts before storage | Schema/contract tests and migration round trip | | Publication | CTA truthfully exposes a readiness check rather than pretending to publish; architecture defines immutable `publication_revision` direction | Planned | No authenticated review approval, immutable publication, supersession or public URL lifecycle | Review & Publication: implement only after persistence/security entry criteria | Authorization, replay/digest, supersession and rollback tests | | Legal/rule traceability | `docs/research-traceability.md`; official PIPA/PIPC source register | Source baseline established | No article/section-level requirement mapping or versioned legal-rule engine | Legal Source Registry: map each implemented rule to source/effective date/revision | Fixtures across current and future-effective rule-set snapshots | | Security/privacy | `docs/SECURITY.md`; local-first current runtime | Baseline documented | Hosted tenant model, secrets boundary, encryption/key handling, audit and incident/retention evidence absent | Platform/Security: threat-model hosted boundary before backend | Security tests and exact-head org scans | -| Tests | Vitest workflow, routing/navigation/blocker regressions, service URL projection, whitespace-purpose edge case, collection-path/purpose separation, disable/re-enable stale-evidence invalidation, preview/review whitespace consistency, buyer-copy boundary, dead-affordance regression, responsive CSS regression | Improved | Browser E2E, accessibility and broader domain edge cases remain incomplete; 100% coverage not evidenced | Test Engineering: expand behavior-first tests | Exact-head coverage and browser evidence | +| Tests | Vitest workflow, routing/navigation/blocker regressions, service URL projection, whitespace-purpose edge case, collection-path/purpose separation, disable/re-enable stale-evidence invalidation, preview/review whitespace consistency, buyer-copy boundary, export/generation/document-title affordance regressions, responsive CSS regression | Improved | Browser E2E, accessibility and broader domain edge cases remain incomplete; 100% coverage not evidenced | Test Engineering: expand behavior-first tests | Exact-head coverage and browser evidence | | Performance | Static Vite client | Unevidenced | No page performance or network/load evidence | Operability: baseline browser performance; add k6 only with backend | Realistic measurements before latency claims | | CI/security merge gate | SHA-pinned checkout plus repo and central required workflows | External live gate | Dependency Review can fail closed when GitHub's exact base/head dependency-graph compare is unavailable; review/model providers can also fail closed without publishable evidence | Control plane: repair incidents centrally and never substitute sibling scanners or predecessor evidence | Exact-head assigned jobs must execute; independent approval and threads required | @@ -58,7 +60,7 @@ A publishable release requires complete fact-to-warning navigation, deterministi ## Active commercialization order 1. Reacquire exact-current-head CI/security/SAST/central workflow execution and independent approval; merge PR #1 only through ordinary protection. -2. Add browser-level responsive/accessibility evidence for the repaired seven-step workflow and verify all remaining action edges. +2. Add browser-level responsive/accessibility evidence for the repaired seven-step workflow and verify all remaining interaction edges. 3. Convert retention/transfer/contact placeholders into typed domain facts and source-backed deterministic validations. 4. Define and test the versioned `policy_revision` persistence/audit schema and explicit item-level UPSERT contracts. 5. Implement secure review/publication with immutable releases and explicit supersession. From 37c5c490e8cc04b3378f828130d9261edb80cfc3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 12:53:21 +0900 Subject: [PATCH 072/282] test: reject inferred customer policy facts --- src/App.test.tsx | 61 +++++++++++++++++++++++++++++++++++++--------- src/policy.test.ts | 37 +++++++++++++++++++--------- 2 files changed, 76 insertions(+), 22 deletions(-) diff --git a/src/App.test.tsx b/src/App.test.tsx index e3bb33d..d049930 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -16,6 +16,15 @@ describe('policy editing workflow', () => { } }) + it('새 작업공간은 운영 사실을 임의 선택하지 않고 수집 여부 확인 전 공개 준비를 차단한다', () => { + const { container } = render() + const selected = Array.from(container.querySelectorAll('.check-label input')).filter((input) => input.checked) + expect(selected).toHaveLength(0) + expect(container.querySelector('.review-stat.blocking b')?.textContent).toBe('1건') + expect(container.querySelector('.publish')?.disabled).toBe(true) + expect(container.querySelector('.document-warning')?.textContent).toContain('수집 항목') + }) + it('서비스 URL을 입력하면 검토본의 적용 서비스 정보에 반영한다', () => { const { container } = render() fireEvent.click(container.querySelectorAll('.rail li button')[0]) @@ -35,26 +44,30 @@ describe('policy editing workflow', () => { expect(container.querySelector('.preview')?.textContent).toContain('입력 내용은 검토본에 즉시 반영됩니다') }) - it('수집 단계에서는 선택 항목의 수집 경로를 항상 편집하고 처리 목적은 다음 단계에서 편집한다', () => { + it('수집 단계에서는 선택 항목의 수집 경로와 수집 구분을 명시적으로 확인하고 처리 목적은 다음 단계에서 편집한다', () => { const { container } = render() - expect(container.querySelectorAll('input[placeholder="예: 회원가입 화면"]')).toHaveLength(3) - expect(container.querySelector('input[placeholder="예: 본인 확인, 알림 발송"]')).toBeNull() + expect(container.querySelectorAll('input[placeholder="예: 회원가입 화면"]')).toHaveLength(0) const phone = container.querySelectorAll('.check-label input')[2] fireEvent.click(phone) - expect(container.querySelectorAll('input[placeholder="예: 회원가입 화면"]')).toHaveLength(4) - expect(container.querySelector('input[placeholder="예: 본인 확인, 알림 발송"]')).toBeNull() + const phoneItem = container.querySelectorAll('.item-list .item')[2] + expect(phoneItem.querySelector('select')?.value).toBe('') + expect(container.querySelectorAll('input[placeholder="예: 회원가입 화면"]')).toHaveLength(1) + + fireEvent.change(phoneItem.querySelector('select')!, { target: { value: '필수' } }) + expect(phoneItem.querySelector('select')?.value).toBe('필수') fireEvent.click(container.querySelectorAll('.rail li button')[2]) expect(container.querySelector('input[name="purpose-phone"]')).not.toBeNull() }) - it('수집 항목을 해제하면 이전 처리 목적과 수집 경로를 폐기해 재활성화 시 재검토한다', () => { + it('수집 항목을 해제하면 이전 처리 목적, 수집 경로, 수집 구분을 폐기해 재활성화 시 재검토한다', () => { const { container } = render() const phoneCheckbox = () => container.querySelectorAll('.check-label input')[2] fireEvent.click(phoneCheckbox()) const phoneItem = container.querySelectorAll('.item-list .item')[2] + fireEvent.change(phoneItem.querySelector('select')!, { target: { value: '필수' } }) fireEvent.change(phoneItem.querySelector('input[placeholder="예: 회원가입 화면"]')!, { target: { value: 'SMS 인증 화면' } }) fireEvent.click(container.querySelectorAll('.rail li button')[2]) @@ -67,7 +80,8 @@ describe('policy editing workflow', () => { const reenabledPhone = container.querySelectorAll('.item-list .item')[2] expect(reenabledPhone.querySelector('input[placeholder="예: 회원가입 화면"]')?.value).toBe('') - expect(container.querySelector('.review-stat.blocking b')?.textContent).toBe('1건') + expect(reenabledPhone.querySelector('select')?.value).toBe('') + expect(container.querySelector('.review-stat.blocking b')?.textContent).toBe('2건') fireEvent.click(container.querySelectorAll('.rail li button')[2]) expect(container.querySelector('input[name="purpose-phone"]')?.value).toBe('') @@ -75,6 +89,10 @@ describe('policy editing workflow', () => { it('공백뿐인 처리 목적은 미리보기에서도 미입력으로 표시한다', () => { const { container } = render() + const nameCheckbox = container.querySelectorAll('.check-label input')[0] + fireEvent.click(nameCheckbox) + const nameItem = container.querySelectorAll('.item-list .item')[0] + fireEvent.change(nameItem.querySelector('select')!, { target: { value: '필수' } }) fireEvent.click(container.querySelectorAll('.rail li button')[2]) const purpose = container.querySelector('input[name="purpose-name"]')! @@ -87,19 +105,40 @@ describe('policy editing workflow', () => { expect(nameRow.cells[2]?.textContent).toBe('확인 필요') }) - it('미리보기 경고에서 처리 목적 단계로 이동한다', () => { + it('미리보기 처리 목적 경고에서 처리 목적 단계로 이동한다', () => { const { container } = render() const phone = container.querySelectorAll('.check-label input')[2] fireEvent.click(phone) - fireEvent.click(container.querySelector('.document-warning button')!) + const warningButton = Array.from(container.querySelectorAll('.document-warning button')).find((button) => button.textContent?.includes('처리 목적'))! + fireEvent.click(warningButton) expect(container.querySelector('.form-panel h1')?.textContent).toBe('3. 처리 목적') }) - it('처리 목적을 보완하면 구현 내부가 아닌 고객의 다음 검토 행동을 안내한다', () => { + it('수집 여부 미확인 경고에서 수집 항목 단계로 이동한다', () => { + const { container } = render() + fireEvent.click(container.querySelectorAll('.rail li button')[0]) + const warningButton = Array.from(container.querySelectorAll('.document-warning button')).find((button) => button.textContent?.includes('수집 항목'))! + fireEvent.click(warningButton) + expect(container.querySelector('.form-panel h1')?.textContent).toBe('2. 수집 항목') + }) + + it('수집 구분 미확인 경고에서 수집 항목 단계로 이동한다', () => { + const { container } = render() + fireEvent.click(container.querySelectorAll('.check-label input')[2]) + fireEvent.click(container.querySelectorAll('.rail li button')[0]) + const warningButton = Array.from(container.querySelectorAll('.document-warning button')).find((button) => button.textContent?.includes('수집 구분'))! + fireEvent.click(warningButton) + expect(container.querySelector('.form-panel h1')?.textContent).toBe('2. 수집 항목') + }) + + it('처리 목적과 수집 구분을 보완하면 구현 내부가 아닌 고객의 다음 검토 행동을 안내한다', () => { const { container } = render() const phone = container.querySelectorAll('.check-label input')[2] fireEvent.click(phone) - fireEvent.click(container.querySelector('.document-warning button')!) + const phoneItem = container.querySelectorAll('.item-list .item')[2] + fireEvent.change(phoneItem.querySelector('select')!, { target: { value: '필수' } }) + const warningButton = Array.from(container.querySelectorAll('.document-warning button')).find((button) => button.textContent?.includes('처리 목적'))! + fireEvent.click(warningButton) const purpose = container.querySelector('input[name="purpose-phone"]')! fireEvent.change(purpose, { target: { value: '본인 확인 및 알림 발송' } }) diff --git a/src/policy.test.ts b/src/policy.test.ts index 9cc1313..aad4af3 100644 --- a/src/policy.test.ts +++ b/src/policy.test.ts @@ -2,22 +2,37 @@ import { describe, expect, it } from 'vitest' import { getReview, initialItems } from './policy' describe('policy review', () => { - it('includes only selected collection items', () => { - expect(getReview(initialItems).enabled.map((item) => item.id)).toEqual(['name', 'email', 'usage']) + it('starts without invented operational selections, collection modes, or processing purposes', () => { + expect(initialItems.filter((item) => item.enabled)).toEqual([]) + expect(initialItems.every((item) => item.mode === '')).toBe(true) + expect(initialItems.every((item) => item.purpose === '')).toBe(true) + const review = getReview(initialItems) + expect(review.selectionMissing).toBe(true) + expect(review.blockingCount).toBe(1) }) - it('blocks publishing when a selected item has no purpose', () => { + it('blocks readiness when a selected item still lacks collection mode and processing purpose', () => { const items = initialItems.map((item) => item.id === 'phone' ? { ...item, enabled: true } : item) - expect(getReview(items).blocking.map((item) => item.id)).toEqual(['phone']) + const review = getReview(items) + expect(review.selectionMissing).toBe(false) + expect(review.modeBlocking.map((item) => item.id)).toEqual(['phone']) + expect(review.blocking.map((item) => item.id)).toEqual(['phone']) + expect(review.blockingCount).toBe(2) }) - it('treats a whitespace-only purpose as missing', () => { - const items = initialItems.map((item) => item.id === 'phone' ? { ...item, enabled: true, purpose: ' \t ' } : item) - expect(getReview(items).blocking.map((item) => item.id)).toEqual(['phone']) + it('treats a whitespace-only purpose as missing after collection mode is confirmed', () => { + const items = initialItems.map((item) => item.id === 'phone' ? { ...item, enabled: true, mode: '필수' as const, purpose: ' \t ' } : item) + const review = getReview(items) + expect(review.modeBlocking).toEqual([]) + expect(review.blocking.map((item) => item.id)).toEqual(['phone']) + expect(review.blockingCount).toBe(1) }) - it('accepts a purpose that contains non-whitespace content', () => { - const items = initialItems.map((item) => item.id === 'phone' ? { ...item, enabled: true, purpose: ' 본인 확인 ' } : item) - expect(getReview(items).blocking.map((item) => item.id)).not.toContain('phone') + it('clears readiness findings once both collection mode and purpose are explicit', () => { + const items = initialItems.map((item) => item.id === 'phone' ? { ...item, enabled: true, mode: '필수' as const, purpose: ' 본인 확인 ' } : item) + const review = getReview(items) + expect(review.blocking.map((item) => item.id)).not.toContain('phone') + expect(review.modeBlocking.map((item) => item.id)).not.toContain('phone') + expect(review.blockingCount).toBe(0) }) -}) +}) \ No newline at end of file From b12052b03646706a89016fbacca52a9d58359022 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 12:54:58 +0900 Subject: [PATCH 073/282] fix: require operator-confirmed policy facts --- src/App.tsx | 20 +++++++++++++++----- src/policy.ts | 28 ++++++++++++++++------------ 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index a3bfdd3..a25b9de 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -33,6 +33,7 @@ const initialFacts: DraftFacts = { privacyOfficerEmail: '', } +/** Renders the seven-step authoring rail and keyboard-targetable preview shortcut. */ function StepRail({ current, setCurrent }: { current: number; setCurrent: (step: number) => void }) { return } +/** Renders previous/next navigation for the active authoring step. */ function StepActions({ current, setCurrent }: { current: number; setCurrent: (step: number) => void }) { return
@@ -54,6 +56,7 @@ function StepActions({ current, setCurrent }: { current: number; setCurrent: (st
} +/** Renders a scalar-fact authoring step backed by the current draft facts. */ function FactStep({ current, title, description, fields, facts, setFacts, setCurrent }: { current: number title: string @@ -73,6 +76,7 @@ function FactStep({ current, title, description, fields, facts, setFacts, setCur } +/** Captures collection facts only after the operator explicitly selects an item. */ function CollectionForm({ items, setItems, setCurrent }: { items: PolicyItem[]; setItems: (items: PolicyItem[]) => void; setCurrent: (step: number) => void }) { const update = (id: string, patch: Partial) => setItems(items.map((item) => item.id === id ? { ...item, ...patch } : item)) return
@@ -84,10 +88,10 @@ function CollectionForm({ items, setItems, setCurrent }: { items: PolicyItem[];
{item.description} - +
{item.enabled &&
} )} @@ -95,6 +99,7 @@ function CollectionForm({ items, setItems, setCurrent }: { items: PolicyItem[];
} +/** Captures processing purposes for collection items explicitly selected by the operator. */ function PurposeForm({ items, setItems, setCurrent }: { items: PolicyItem[]; setItems: (items: PolicyItem[]) => void; setCurrent: (step: number) => void }) { const enabled = items.filter((item) => item.enabled) const updatePurpose = (id: string, purpose: string) => setItems(items.map((item) => item.id === id ? { ...item, purpose } : item)) @@ -107,6 +112,7 @@ function PurposeForm({ items, setItems, setCurrent }: { items: PolicyItem[]; set } +/** Chooses the editing surface that owns the active authoring step. */ function EditingPanel({ current, items, setItems, facts, setFacts, setCurrent }: { current: number items: PolicyItem[] @@ -143,11 +149,12 @@ function EditingPanel({ current, items, setItems, facts, setFacts, setCurrent }: return } +/** Projects verified authoring facts and deterministic readiness findings into the review draft. */ function DocumentPreview({ items, facts, setCurrent }: { items: PolicyItem[]; facts: DraftFacts; setCurrent: (step: number) => void }) { const review = useMemo(() => getReview(items), [items]) return

개인정보처리방침 미리보기

-
근거 법령 개인정보 보호법{review.blocking.length ? `검토 필요 ${review.blocking.length}` : '필수 확인 완료'}버전 0.1.0
+
근거 법령 개인정보 보호법{review.blockingCount ? `검토 필요 ${review.blockingCount}` : '필수 확인 완료'}버전 0.1.0

{facts.serviceName || '개인정보처리방침'} (검토본)

{facts.serviceUrl &&

적용 서비스: {facts.serviceUrl}

} @@ -158,6 +165,8 @@ function DocumentPreview({ items, facts, setCurrent }: { items: PolicyItem[]; fa const hasPurpose = item.purpose.trim().length > 0 return {item.label}{hasPurpose ? item.purpose : '처리 목적 입력 필요'}{hasPurpose ? '입력됨' : '확인 필요'} })} + {review.selectionMissing &&
공개 전 확인실제 수집 항목이 아직 확인되지 않았습니다.
} + {review.modeBlocking.length > 0 &&
공개 전 확인{review.modeBlocking.map((item) => item.label).join(', ')}의 수집 구분을 확인해야 합니다.
} {review.blocking.length > 0 &&
공개 전 확인{review.blocking.map((item) => item.label).join(', ')}의 처리 목적이 입력되지 않았습니다.
}

제2조 (처리 및 보유 기간)

{facts.retentionPeriod || '보유 기간 단계에서 확인한 운영 기준을 입력해야 합니다.'}

제3조 (제3자 제공)

{facts.thirdPartyRecipient ? `${facts.thirdPartyRecipient}에 ${facts.thirdPartyPurpose || '확인 중인 목적'}으로 제공하는 흐름을 검토 중입니다.` : '제3자 제공 여부를 확인하는 단계가 남아 있습니다.'}

@@ -168,16 +177,17 @@ function DocumentPreview({ items, facts, setCurrent }: { items: PolicyItem[]; fa
} +/** Coordinates PolicyWeave browser-only authoring state and readiness feedback. */ export default function App() { const [items, setItems] = useState(initialItems) const [facts, setFacts] = useState(initialFacts) const [current, setCurrent] = useState(2) const review = useMemo(() => getReview(items), [items]) const [message, setMessage] = useState('') - function publish() { setMessage(review.blocking.length ? '필수 확인 항목을 먼저 입력하세요.' : '필수 확인이 완료되었습니다. 현재 검토본을 책임자와 검토하고 필요한 사실을 보완하세요.') } + function publish() { setMessage(review.blockingCount ? '필수 확인 항목을 먼저 입력하세요.' : '필수 확인이 완료되었습니다. 현재 검토본을 책임자와 검토하고 필요한 사실을 보완하세요.') } return
PolicyWeave{facts.serviceName || '내 서비스'} 개인정보처리방침작성 중버전 0.1.0 (임시저장) 브라우저 작업 중
-
검토 요약확인을 마친 뒤 공개 준비 상태를 확인하세요.
필수 확인 {review.blocking.length}건
권장 검토 {review.recommended.length}건
{message}
+
검토 요약확인을 마친 뒤 공개 준비 상태를 확인하세요.
필수 확인 {review.blockingCount}건
권장 검토 {review.recommended.length}건
{message}
} \ No newline at end of file diff --git a/src/policy.ts b/src/policy.ts index 4f39d95..df4d598 100644 --- a/src/policy.ts +++ b/src/policy.ts @@ -1,23 +1,27 @@ -export type CollectionMode = '필수' | '선택' +export type CollectionMode = '' | '필수' | '선택' export type PolicyItem = { id: string; label: string; description: string; purpose: string; enabled: boolean; mode: CollectionMode; detail?: string } export const initialItems: PolicyItem[] = [ - { id: 'name', label: '이름', description: '서비스 이용자 식별', purpose: '서비스 이용자 식별', enabled: true, mode: '필수' }, - { id: 'email', label: '이메일 주소', description: '계정 식별, 로그인, 중요 고지 수신', purpose: '계정 식별 및 중요 고지', enabled: true, mode: '필수' }, - { id: 'phone', label: '휴대전화 번호', description: '본인 확인, 알림 발송', purpose: '', enabled: false, mode: '선택' }, - { id: 'usage', label: '서비스 이용 기록', description: '접속 로그, 이용 내역, 클릭 기록 등', purpose: '서비스 개선 및 이용 통계 분석', enabled: true, mode: '필수' }, - { id: 'ip', label: '접속 IP 주소', description: '보안, 부정 이용 방지', purpose: '보안 및 부정 이용 방지', enabled: false, mode: '선택' }, - { id: 'cookie', label: '쿠키 및 유사 기술', description: '설정 유지, 통계 분석', purpose: '', enabled: false, mode: '선택' }, - { id: 'address', label: '주소', description: '배송, 청구서 발송', purpose: '', enabled: false, mode: '선택' }, - { id: 'payment', label: '결제 정보', description: '결제 처리, 환불 처리', purpose: '', enabled: false, mode: '선택' }, - { id: 'content', label: '게시물 및 문의 내용', description: '게시 기능, 고객 문의 처리', purpose: '', enabled: false, mode: '선택' }, + { id: 'name', label: '이름', description: '서비스 이용자 식별', purpose: '', enabled: false, mode: '' }, + { id: 'email', label: '이메일 주소', description: '계정 식별, 로그인, 중요 고지 수신', purpose: '', enabled: false, mode: '' }, + { id: 'phone', label: '휴대전화 번호', description: '본인 확인, 알림 발송', purpose: '', enabled: false, mode: '' }, + { id: 'usage', label: '서비스 이용 기록', description: '접속 로그, 이용 내역, 클릭 기록 등', purpose: '', enabled: false, mode: '' }, + { id: 'ip', label: '접속 IP 주소', description: '보안, 부정 이용 방지', purpose: '', enabled: false, mode: '' }, + { id: 'cookie', label: '쿠키 및 유사 기술', description: '설정 유지, 통계 분석', purpose: '', enabled: false, mode: '' }, + { id: 'address', label: '주소', description: '배송, 청구서 발송', purpose: '', enabled: false, mode: '' }, + { id: 'payment', label: '결제 정보', description: '결제 처리, 환불 처리', purpose: '', enabled: false, mode: '' }, + { id: 'content', label: '게시물 및 문의 내용', description: '게시 기능, 고객 문의 처리', purpose: '', enabled: false, mode: '' }, ] export const steps = ['서비스 정보', '수집 항목', '처리 목적', '보유 기간', '제3자 제공', '국외 이전', '개인정보 보호 담당자'] +/** Derives deterministic readiness findings from operator-confirmed collection facts. */ export function getReview(items: PolicyItem[]) { const enabled = items.filter((item) => item.enabled) const blocking = enabled.filter((item) => !item.purpose.trim()) + const modeBlocking = enabled.filter((item) => !item.mode) + const selectionMissing = enabled.length === 0 + const blockingCount = blocking.length + modeBlocking.length + (selectionMissing ? 1 : 0) const recommended = enabled.filter((item) => item.mode === '선택' && item.id !== 'usage') - return { enabled, blocking, recommended } -} + return { enabled, blocking, modeBlocking, selectionMissing, blockingCount, recommended } +} \ No newline at end of file From 8e242842049c245c593b625f058468518318bdea Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 12:57:14 +0900 Subject: [PATCH 074/282] docs: align fact-authority commercialization contract --- ARCHITECTURE.md | 17 ++++--- CHANGELOG.md | 10 ++-- .../ADR-0002-seven-step-authoring-workflow.md | 8 ++- docs/PRD.md | 5 +- docs/TRD.md | 19 ++++--- docs/product-technical-gap-baseline.md | 49 ++++++++----------- 6 files changed, 56 insertions(+), 52 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 189b970..bc6f4c5 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -18,15 +18,16 @@ The core subdomain is **Policy Fact Authoring**. Supporting subdomains are **Rev A future `policy_revision` is the minimal aggregate root and transaction boundary. `collection_item`, `processing_purpose`, `retention_rule`, `third_party_transfer`, `international_transfer`, and `privacy_contact` are revision-owned facts/value objects unless later evidence requires independent lifecycles. `review_finding` is derived evidence. `publication_revision` is an immutable release receipt, not a mutable policy row. Rendered prose is a projection and never the source of truth. Core invariants: -1. A selected collection item cannot pass review without a non-empty processing purpose. -2. Disabling a collection item invalidates its dependent processing-purpose and collection-path evidence; re-enabling it requires those facts to be captured and reviewed again rather than silently reviving stale evidence. -3. Review findings navigate to the fact that caused them. -4. Publication must never upgrade an unreviewed or incomplete draft to a reviewed/authoritative state. -5. A published revision remains reproducible from its policy facts plus rule/template/source versions. -6. External legal-source updates produce explicit re-evaluation, not silent rewriting. +1. A fresh workspace contains no inferred customer operational facts: collection selection, collection mode, and processing purpose remain unresolved until the operator establishes them. +2. Selecting a collection item without confirming its collection mode or processing purpose creates blocking review findings. +3. Disabling a collection item invalidates its dependent collection-mode, processing-purpose, and collection-path evidence; re-enabling it requires those facts to be captured and reviewed again rather than silently reviving stale evidence. +4. Review findings navigate to the fact that caused them. +5. Publication must never upgrade an unreviewed or incomplete draft to a reviewed/authoritative state. +6. A published revision remains reproducible from its policy facts plus rule/template/source versions. +7. 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. `src/policy.ts` owns the current collection-item review rule; `src/App.tsx` owns browser orchestration and deterministic preview rendering. +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: every item begins unselected, with no inferred collection mode or processing purpose. `src/policy.ts` owns deterministic selection-established, collection-mode, and processing-purpose readiness rules; `src/App.tsx` owns browser orchestration, warning-to-source navigation, and deterministic preview rendering. ## Persistence boundary (planned, not implemented) Use relational 3NF by default. Named database/schema/persistence objects use at least two semantic words and `snake_case`, for example `policy_revision`, `collection_item`, `processing_purpose`, `review_finding`, `publication_revision`, and `legal_source_revision`. Item-level UPSERTs must declare their natural/idempotency key and conflict behavior. Publication is append-only/immutable with explicit supersession; writes across unrelated aggregates must not share a transaction merely for convenience. @@ -34,4 +35,4 @@ Use relational 3NF by default. Named database/schema/persistence objects use at Separate write-side draft commands from read-side rendered/review projections once hosted traffic justifies it. Account for revision hot spots and optimistic/constrained writes before adding collaborative editing. Keep source/customer integrations behind ACLs; do not form a shared kernel with unrelated ContextualWisdomLab products without demonstrated reuse. ## Deployment and operability direction -The browser-only MVP needs no service mesh. A hosted backend should be compose-deployable across Docker/Podman/Colima before Kubernetes migration, expose asynchronous/non-blocking request handling, and add realistic k6 evidence for network surfaces before latency claims. No code may depend on an optional `close_connection` instance attribute existing unless the adapter contract guarantees it. +The browser-only MVP needs no service mesh. A hosted backend should be compose-deployable across Docker/Podman/Colima before Kubernetes migration, expose asynchronous/non-blocking request handling, and add realistic k6 evidence for network surfaces before latency claims. No code may depend on an optional `close_connection` instance attribute existing unless the adapter contract guarantees it. \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 092e075..688231c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,14 +6,15 @@ All notable product changes are recorded here. PolicyWeave is pre-release; entri ### Added - Seven-step authoring workflow with distinct editing surfaces for service information, collection items, processing purposes, retention, third-party transfer, international transfer, and privacy contact. -- Warning-to-source navigation for missing processing purposes. -- Regression coverage for all seven step routes, service URL projection into the review draft, warning navigation, clearing the publication blocker after a missing purpose is supplied, whitespace-only purposes, collection-path/purpose step separation, disable/re-enable stale-evidence invalidation, preview/review whitespace consistency, buyer-facing publication guidance, and non-deceptive handling of unshipped export/generation/document-selection affordances. +- Warning-to-source navigation for missing collection selection, collection mode, and processing purposes. +- Regression coverage for all seven step routes, zero-inferred startup facts, collection-mode confirmation, selection/mode warning navigation, service URL projection into the review draft, warning navigation, clearing publication blockers, whitespace-only purposes, collection-path/purpose step separation, disable/re-enable stale-evidence invalidation, preview/review whitespace consistency, buyer-facing publication guidance, and non-deceptive handling of unshipped export/generation/document-selection affordances. - Product/technical gap ledger, architecture, technical requirements, security baseline, and legal-source traceability. ### Changed +- Fresh workspaces no longer preselect collection items or prefill collection modes/processing purposes; readiness now fails closed until the operator establishes actual collection facts. +- Disabling a collection item now clears its collection mode, processing purpose, and collection-path evidence so re-enabling it cannot silently revive stale customer facts. - Service URL entered in the service-information step is now projected into the review draft instead of being silently omitted. - Collection-path editing now remains available for every selected item in the collection step, while processing-purpose editing stays in its dedicated purpose step. -- Disabling a collection item now clears its dependent processing-purpose and collection-path evidence so re-enabling it cannot silently revive stale policy facts. - Review preview now applies the same whitespace-normalized purpose-completeness contract as the blocking review rule. - The publication-area CTA now describes a readiness check and directs the operator to responsible review rather than exposing repository/backend implementation details for an unshipped capability. - Unshipped JSON export is visibly disabled as `준비 중`, the redundant no-op `검토본 생성` control was removed because the review draft already updates live, and the document title is rendered as status text rather than an inert dropdown-like button. @@ -23,7 +24,8 @@ All notable product changes are recorded here. PolicyWeave is pre-release; entri - GitHub Actions checkout dependencies are SHA pinned and credentials are not persisted in the working tree. ### Not yet shipped +- Requirement-complete typed validation for service identity, retention, transfers, privacy contact, and collection-path evidence. - Durable versioned policy persistence and audit history. - Authenticated immutable publication revisions and public URL lifecycle. - Browser-matrix/accessibility evidence and hosted endpoint load testing. -- Legal/rule engine backed by versioned authoritative requirements. +- Legal/rule engine backed by versioned authoritative requirements. \ No newline at end of file diff --git a/docs/ADR-0002-seven-step-authoring-workflow.md b/docs/ADR-0002-seven-step-authoring-workflow.md index 8c18d8d..2e8773d 100644 --- a/docs/ADR-0002-seven-step-authoring-workflow.md +++ b/docs/ADR-0002-seven-step-authoring-workflow.md @@ -6,13 +6,19 @@ Date: 2026-09-01 ## Context The PRD defines seven authoring stages, but an early implementation changed only the step rail while always rendering the collection-item editor. This made the buyer-facing workflow appear complete while most facts were not editable and review findings could not lead the operator to the responsible input. +A later commercialization pass found a second workflow-integrity defect: the fresh workspace preselected collection items and prefilled service-specific collection modes and processing purposes. Those values looked like customer facts even though the operator had never established them, contradicting PolicyWeave's fact-authoring boundary and its prohibition on synthetic production data. + ## Decision Each PRD stage is an explicit editable state of the Policy Fact Authoring context: service information, collection items, processing purposes, retention, third-party transfer, international transfer, and privacy contact. The UI router must render a corresponding editor for the selected stage. Review findings carry enough domain context to navigate to the responsible stage. +The collection catalog is metadata, not an assertion about a customer's service. A fresh workspace therefore starts with every collection item unselected and with collection mode and processing purpose unresolved. Selecting an item requires the operator to establish its collection mode and purpose before readiness can clear. An empty selection is treated as unresolved, not as an inferred assertion that the service collects no personal data. Disabling an item invalidates its mode, purpose, and collection-path evidence so re-enabling cannot silently restore stale facts. + The workflow is not a legal state machine. Completion of a UI step does not mean legal sufficiency. Legal/rule completeness is determined separately by deterministic review rules bound to versioned authoritative source evidence. ## Consequences - Navigation and review-to-source behavior are regression-tested. +- Production startup state contains taxonomy metadata only, not inferred customer operational facts. +- Selection-not-established, collection-mode, and processing-purpose findings fail closed and navigate to their owning step. - The preview remains a projection over structured facts and cannot become an independent source of truth. - Future persistence stores facts and revision/review state rather than serialized page prose. -- Additional legal validations can grow without redefining the seven buyer-facing authoring responsibilities. +- Additional legal validations can grow without redefining the seven buyer-facing authoring responsibilities. \ No newline at end of file diff --git a/docs/PRD.md b/docs/PRD.md index ef6bb4e..f1115cc 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -6,7 +6,7 @@ ## 제품 원칙 -PolicyWeave는 법률 문장을 임의로 창작하는 도구가 아니다. 운영자가 입력한 사실을 구조화하고, 적용 근거와 확인이 필요한 부분을 분리해 검토본을 만든다. 자동 생성 결과는 변호사·개인정보보호책임자의 검토를 대체하지 않는다. +PolicyWeave는 법률 문장을 임의로 창작하는 도구가 아니다. 운영자가 입력한 사실을 구조화하고, 적용 근거와 확인이 필요한 부분을 분리해 검토본을 만든다. 자동 생성 결과는 변호사·개인정보보호책임자의 검토를 대체하지 않는다. 새 작업공간은 고객의 실제 수집 여부, 수집 구분 또는 처리 목적을 추정하거나 미리 선택하지 않는다. ## MVP @@ -27,6 +27,7 @@ PolicyWeave는 법률 문장을 임의로 창작하는 도구가 아니다. 운 ## 성공 기준 - 사용자는 누락 없이 검토본을 생성할 수 있다. +- 새 작업공간은 고객 운영 사실을 미리 채우지 않으며, 실제 수집 항목과 각 항목의 수집 구분·처리 목적이 확인되기 전에는 공개 준비 상태를 통과하지 않는다. - 모든 경고는 해당 입력 단계로 이동할 수 있다. - 모바일에서도 작성과 미리보기를 전환할 수 있다. -- 키보드만으로 모든 입력과 주요 동작을 수행할 수 있다. +- 키보드만으로 모든 입력과 주요 동작을 수행할 수 있다. \ No newline at end of file diff --git a/docs/TRD.md b/docs/TRD.md index e4bbecc..73b6f99 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -6,21 +6,24 @@ 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. -- `src/policy.ts` contains deterministic review logic for selected collection items and missing processing purposes. +- `src/policy.ts` contains deterministic review logic for whether collection selection has been established, whether selected items have an explicit collection mode, and whether selected items have a processing purpose. - `src/App.tsx` provides the seven-step authoring flow, review navigation, and deterministic preview projection. - The current CI contract is lint, Vitest, and TypeScript/Vite build plus live organization-required security/review workflows. ## Functional contracts 1. Every PRD step must route to an editable surface; selecting a step cannot change only the rail indicator. -2. Selected collection items expose their collection mode and processing-purpose contract. -3. A selected item with blank/whitespace purpose produces a blocking review finding, and the preview must apply the same blank/whitespace normalization when it renders purpose completeness. -4. Disabling a collection item invalidates dependent processing-purpose and collection-path evidence; re-enabling the item requires those facts to be captured and reviewed again. -5. A blocking finding links to the responsible editing step. -6. Preview text is derived from current structured facts; it does not become an independently editable authority. -7. UI copy distinguishes a review draft from legal advice, certification, or a compliance guarantee. +2. A fresh workspace contains no inferred customer operational facts: collection selection, collection mode, and processing purpose start unresolved. +3. Until at least one actual collection item is selected, readiness produces a blocking selection-not-established finding that navigates to the collection step. +4. Every selected collection item requires an explicit collection mode; a missing mode is blocking and navigates to the collection step. +5. A selected item with blank/whitespace purpose produces a blocking review finding, and the preview must apply the same blank/whitespace normalization when it renders purpose completeness. +6. Disabling a collection item invalidates dependent collection-mode, processing-purpose, and collection-path evidence; re-enabling the item requires those facts to be captured and reviewed again. +7. Every blocking finding links to the responsible editing step. +8. Preview text is derived from current structured facts; it does not become an independently editable authority. +9. UI copy distinguishes a review draft from legal advice, certification, or a compliance guarantee. ## Quality contracts - Touched production behavior requires regression and edge-case tests first. +- Touched production functions carry descriptive JSDoc rather than relying on implicit behavior. - Do not suppress deprecation warnings to pass CI. - Browser/accessibility validation must eventually include keyboard/focus order, WCAG 2.2 automated checks, responsive desktop/tablet/mobile interactions, and screenshot evidence. - 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. @@ -35,4 +38,4 @@ A publication command must operate on a specific reviewed `policy_revision` and Legal/rule data enters through a Legal Source Registry ACL with source identity, effective date, retrieval/version evidence, and an implementation mapping. Customer/application integrations require their own explicit adapters. If future LLM assistance is justified for drafting/explanation, calls must route through `ContextualWisdomLab/contextual-orchestrator`; deterministic validation and publication authority remain outside the model. ## Verification state -Only exact-current-head checks count. Queued, cancelled, skipped-required, or predecessor-head runs are not evidence of passing. Normal integration requires the live ruleset's independent approval and thread-resolution requirements; no administrative bypass is part of this TRD. +Only exact-current-head checks count. Queued, cancelled, skipped-required, or predecessor-head runs are not evidence of passing. Normal integration requires the live ruleset's independent approval and thread-resolution requirements; no administrative bypass is part of this TRD. \ No newline at end of file diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index c6521a4..2de9514 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -10,43 +10,34 @@ PolicyWeave is a local-first policy-fact workspace that helps a service operator ## Exact-head implementation evidence The seven-step routing defect and warning-navigation defect were reproduced on predecessor head `62b6f5ef9056a111c93a09ba40d205c98f42baef`: `App.tsx` always rendered the collection editor while regression tests expected all seven editors and a warning-navigation action. Tests were strengthened first at `4795f99beb51a95cbc7393a2bb0b9600389776d8`, then production routing/review behavior was repaired at `61f5bac24d0271f382668872140a7ef4a3463db4`. -A later exact-head review found that collection-path editing was incorrectly coupled to an empty processing purpose: enabled items with an existing purpose lost the collection-path field, while entering a purpose made both fields disappear. The regression was added first at `b35e18f3831ea5a257b0a9397c08aa73af8c3f52`; `CollectionForm` was then repaired at `87d37b9a219ff50013f9ac46c3b1f11b52a94467` so collection-path editing remains in step 2 and purpose editing remains in step 3. `ec1aead4484018d52ce8bd5bbab04c517d42870d` also locks the existing whitespace-only-purpose blocking contract. The review thread was resolved only after the repaired code was re-fetched at `87d37b9a219ff50013f9ac46c3b1f11b52a94467`. +A later exact-head review found that collection-path editing was incorrectly coupled to an empty processing purpose: enabled items with an existing purpose lost the collection-path field, while entering a purpose made both fields disappear. The regression was added first at `b35e18f3831ea5a257b0a9397c08aa73af8c3f52`; `CollectionForm` was then repaired at `87d37b9a219ff50013f9ac46c3b1f11b52a94467` so collection-path editing remains in step 2 and purpose editing remains in step 3. `ec1aead4484018d52ce8bd5bbab04c517d42870d` also locks the existing whitespace-only-purpose blocking contract. -On exact PR head `2d4c956b2b7b4cc3607c2724a4be2073393dc692`, two additional policy-fact integrity gaps were reproduced from the current implementation. First, disabling an enabled collection item preserved its processing purpose and collection path, so later re-enabling silently revived stale evidence without renewed review. Second, the review engine correctly treated a whitespace-only purpose as missing while the preview used string truthiness and displayed the same whitespace value as entered. Regression tests were added before production changes at `10ac51201afb59d677ed546dd7e32ebc7373031f` and `12f4cc8bae8aa1a324ba5238d593acb53e59dd8c`. Production was then repaired at `798ac3672812c5263a44de09a610e0b5cb9a7efc`: disabling an item now invalidates its dependent purpose/path facts, and preview status uses the same trimmed-purpose completeness contract as review. Architecture, TRD, ADR-0001 and CHANGELOG were reconciled afterward. +On exact PR head `2d4c956b2b7b4cc3607c2724a4be2073393dc692`, disabling an enabled collection item preserved its processing purpose and collection path, so later re-enabling silently revived stale evidence without renewed review. The preview also used string truthiness while the review engine trimmed whitespace. Regression tests were added before production changes at `10ac51201afb59d677ed546dd7e32ebc7373031f` and `12f4cc8bae8aa1a324ba5238d593acb53e59dd8c`; production was repaired at `798ac3672812c5263a44de09a610e0b5cb9a7efc`. -A separate buyer-facing workflow review found that the enabled publication-area action exposed an internal implementation boundary (`백엔드`/repository connectivity) even though public URL publication is not shipped. That violated the product-copy boundary and gave the operator no useful next step. A regression was added first at `fcd75aa98242158d0abe5d8beb661dd0ff064011` to require a customer-facing readiness action and reject internal-boundary terms. Production was repaired at `9d86a1316b03455c740c4b70296f604c52d9981f`: the CTA is now `공개 준비 확인`, and the completion message directs the operator to responsible review and fact completion rather than describing backend implementation. `d649604bf41e6618c8deeba5a83d1b8b973355d6` reconciled the CHANGELOG. +Buyer-facing workflow passes then repaired internal-boundary publication copy (`fcd75aa98242158d0abe5d8beb661dd0ff064011` -> `9d86a1316b03455c740c4b70296f604c52d9981f`), missing `serviceUrl` projection (`cc9c8a07881d1a06c81a92c82161b469fa52508c` -> `815bec382af557e8dc9550329d8c71586eddd297`), unshipped/no-op action affordances (`f21c65a589c73c9b020696a5eda8240249581f3c` -> `f00a6ad022c49ab7e125d2186c335462ec9bb2e2`), and an inert dropdown-like document-title control (`8233fe09271c3fb0b172892cd0f2fc953a1d84de` -> `c385196a3c9036fa7580ae58ac624b609f96c083`). -The live review corpus also contained a still-valid finding that step 1 captured `serviceUrl` but `DocumentPreview` omitted it, so one of the structured source facts disappeared from the buyer-visible deterministic projection. The regression was added first at `cc9c8a07881d1a06c81a92c82161b469fa52508c`; production was repaired at `815bec382af557e8dc9550329d8c71586eddd297` by rendering the entered URL as plain escaped text in the review draft, avoiding creation of an unvalidated navigation target. `4ceaf822d37d19957637c9679d7884b552e5edb8` reconciled the CHANGELOG. +The next exact-head commercialization pass found a more fundamental fact-authority violation. Production `initialItems` preselected `name`, `email`, and `usage` and prefilled their collection modes and processing purposes even though no operator had established those service-specific facts. This contradicted the PRD/AGENTS/TRD boundary that PolicyWeave structures customer-supplied facts and that production must not consume synthetic demo facts. Regression tests were committed first at `37c5c490e8cc04b3378f828130d9261edb80cfc3`, requiring zero inferred selection/mode/purpose facts, fail-closed readiness when collection selection is unestablished, explicit collection-mode confirmation, warning-to-owner navigation, and invalidation of stale mode evidence. Production was repaired at `b12052b03646706a89016fbacca52a9d58359022`: all taxonomy items now start unselected with unresolved mode/purpose, `getReview` derives selection/mode/purpose blocking findings and a single blocking count, the preview navigates each finding to its owner step, and disabling an item clears its mode/purpose/path evidence. Touched production functions also received descriptive JSDoc. This repair changes no legal rule and introduces no heuristic weighting. -A buyer-workflow pass found two controls that appeared actionable despite having no implementation: the `JSON 내보내기 준비` button and a redundant `검토본 생성` button even though the review draft updates continuously. The regression was added first at `f21c65a589c73c9b020696a5eda8240249581f3c`, requiring the export control to be explicitly disabled/marked `준비 중`, requiring the no-op generation button to be absent, and requiring live-update guidance. Production was repaired at `f00a6ad022c49ab7e125d2186c335462ec9bb2e2`, and `8c9eaa384fb14185d620866ab01b6fb261ceca05` reconciled the CHANGELOG. This removes deceptive pre-release affordances without claiming that export or hosted publication exists. - -The same action-edge sweep found a top-bar document title rendered as a dropdown-like button with a chevron but no click handler, menu, or alternate document workflow. That was another misleading action boundary. A regression was added first at `8233fe09271c3fb0b172892cd0f2fc953a1d84de` to require that the document title is not exposed as a button and is rendered as status text. Production was repaired at `c385196a3c9036fa7580ae58ac624b609f96c083`; the title now remains visible as `.document-name` without implying an unavailable selector. `2d4f1def55bc9f367e3fe20eac2b81199c1b6294` reconciled the CHANGELOG. Because this ledger commit also moves the PR head, live Checks/reviews must be reacquired on the resulting exact current head; predecessor-head conclusions are not merge evidence. - -### 2026-09-02 failed-Check RCA: responsive CSS test SAST - -On exact PR head `272c0aa7502c8768159ec7ba6188348e88d86b4f`, SAST run `33555808672`, job `100016223694` completed exact-head checkout and Semgrep analysis, then failed its Medium+ enforcement gate on one unsuppressed finding: `javascript.lang.security.audit.detect-non-literal-regexp.detect-non-literal-regexp` at `src/styles.test.ts:7`. The PR-introduced test helper constructed its media-query matcher with `new RegExp(...)` from a function argument. The only callers were fixed widths (`1300` and `720`), so this was not an attacker-controlled product path, but it was a real repository-owned static-analysis defect and was not suppressed or downgraded. - -The smallest repair replaces the dynamic constructor with a closed map of literal regular expressions for the two supported widths while preserving the existing responsive assertions and missing-block failure. The failing SAST result is the regression evidence; exact-head Vitest and Semgrep must both be reacquired after the repair. No production CSS behavior, warning severity, scanner configuration, review rule, or coverage gate is weakened. - -The same predecessor head also had an independent Security Scan failure in run `33555808490`, dependency-review job `100016223043`. Exact checkout succeeded and the dependency-graph compare request for base `52f4fd6bb68f870d0519cf11dd471573a2f197c0` to head `272c0aa7502c8768159ec7ba6188348e88d86b4f` completed transport successfully (`curl_exit=0`) but GitHub returned HTTP `403`. OSV, Trivy, and Scorecard sibling jobs succeeded. This matches the organization-level Dependency Review availability/authorization incident tracked by `ContextualWisdomLab/.github#810`; the central workflow correctly failed closed, so no PolicyWeave dependency or security gate is changed to work around it. - -Required Noema Review run `33555806837`, job `100016218520` was also independent of the product code. It used trusted central workflow source `ContextualWisdomLab/.github@30f55d10e200673493f824886aa891cb3b099b7e`, successfully minted the repository-scoped reviewer token and started `contextual-orchestrator` with the `orchestrator/free` pool. Five provider routes passed runtime preflight, but the returned verdict lacked the required two concrete adversarial probes and the bounded schema/evidence repair then exceeded its 900-second absolute deadline. That is a fail-closed model/control-plane evidence failure, not permission failure or a PolicyWeave code verdict. A new exact head must exercise the then-current trusted central workflow rather than carrying this predecessor result forward. +### 2026-09-02 predecessor Check RCA retained for traceability +On predecessor head `272c0aa7502c8768159ec7ba6188348e88d86b4f`, SAST run `33555808672` failed on a dynamic `RegExp` in `src/styles.test.ts`; the repair replaced it with literal regexes without suppressing the scanner. The same predecessor had an independent Dependency Review HTTP 403 availability/authorization failure tracked by `ContextualWisdomLab/.github#810`, while sibling OSV/Trivy/Scorecard jobs succeeded. Required Noema Review also failed closed because the returned verdict lacked required adversarial probes and bounded repair exceeded its absolute deadline. None of those predecessor results are valid evidence for a later head; exact-current-head workflows must be reacquired. ## Current baseline | Area | Evidence | Status | Commercialization gap | Owner/action | Next verification | | --- | --- | --- | --- | --- | --- | -| Guided authoring | PRD, ADR-0002, seven routed editors in `src/App.tsx`; service URL preserved into review projection; step-2 collection-path/step-3 purpose separation; disabled-item dependent-fact invalidation | Functional foundation | Retention, third-party transfer, international transfer and contact steps currently capture thin facts rather than requirement-complete domain contracts | Policy Fact Authoring: add typed facts and deterministic validations from authoritative requirements | Unit/integration tests for valid, missing, contradictory and not-applicable cases | -| Processing-purpose review | `src/policy.ts`, `src/policy.test.ts`, `src/App.test.tsx`; whitespace normalization shared by review and preview | Repaired | Missing and whitespace-only purposes block review consistently, but the rule set covers only purpose completeness | Review & Publication: extend evidence-bound review rules without heuristic scoring | Exact-head tests plus rule/source traceability | -| Review workspace | Live preview, service URL projection, warning-to-source navigation, responsive CSS fixes, buyer-facing readiness guidance; no-op generation removed, unshipped export explicitly disabled, document title rendered as non-interactive status | Implemented foundation | No browser screenshot matrix or realistic interaction evidence | UX: add Playwright + accessibility harness and verify remaining interaction edges | Desktop/tablet/mobile screenshots, keyboard/focus and action-edge verification | +| Guided authoring | PRD, ADR-0002, seven routed editors; zero-inferred startup facts; service URL projection; step-2 collection-path/mode and step-3 purpose separation; stale dependent-fact invalidation | Repaired foundation | Service identity, retention, transfers, contact, and collection-path evidence remain thin rather than requirement-complete typed contracts | Policy Fact Authoring: add typed facts and deterministic validations from authoritative requirements | Unit/integration tests for valid, missing, contradictory and not-applicable cases | +| Collection fact authority | `src/policy.ts`, `src/policy.test.ts`, `src/App.test.tsx`; test-first `37c5c490` -> production `b12052b0` | Repaired | No explicit first-class attestation yet for a service that truly collects no personal data; empty selection intentionally remains unresolved | Policy Fact Authoring: model evidence-backed no-collection/not-applicable state only if product requirements justify it | Contract tests that distinguish unresolved from explicit not-applicable | +| Processing-purpose review | Whitespace normalization plus selection/mode/purpose blocking in `getReview` and preview | Repaired foundation | Rule set still covers only a subset of authoring completeness and no versioned legal-rule snapshots | Review & Publication: extend evidence-bound rules without heuristic scoring | Exact-head tests plus rule/source traceability | +| Review workspace | Live preview, warning-to-source navigation, responsive fixes, buyer-facing readiness guidance; no-op/unshipped affordances removed or disabled | Implemented foundation | No browser screenshot matrix or realistic interaction evidence | UX: add Playwright + accessibility harness and verify remaining action edges | Desktop/tablet/mobile screenshots, keyboard/focus and action-edge verification | | Accessibility | Semantic controls, focus-visible behavior, keyboard-operable step navigation | Partial | No automated WCAG 2.2 evidence or screen-reader contract | UX: add axe/Playwright and manual interaction record | WCAG audit and browser matrix | -| Policy model | ADR-0001, ARCHITECTURE, TRD | Designed, browser-memory only | No versioned durable schema, migration, audit history, UPSERT/idempotency or 3NF implementation | Platform: define `policy_revision` schema and item-level conflict contracts before storage | Schema/contract tests and migration round trip | -| Publication | CTA truthfully exposes a readiness check rather than pretending to publish; architecture defines immutable `publication_revision` direction | Planned | No authenticated review approval, immutable publication, supersession or public URL lifecycle | Review & Publication: implement only after persistence/security entry criteria | Authorization, replay/digest, supersession and rollback tests | -| Legal/rule traceability | `docs/research-traceability.md`; official PIPA/PIPC source register | Source baseline established | No article/section-level requirement mapping or versioned legal-rule engine | Legal Source Registry: map each implemented rule to source/effective date/revision | Fixtures across current and future-effective rule-set snapshots | -| Security/privacy | `docs/SECURITY.md`; local-first current runtime | Baseline documented | Hosted tenant model, secrets boundary, encryption/key handling, audit and incident/retention evidence absent | Platform/Security: threat-model hosted boundary before backend | Security tests and exact-head org scans | -| Tests | Vitest workflow, routing/navigation/blocker regressions, service URL projection, whitespace-purpose edge case, collection-path/purpose separation, disable/re-enable stale-evidence invalidation, preview/review whitespace consistency, buyer-copy boundary, export/generation/document-title affordance regressions, responsive CSS regression | Improved | Browser E2E, accessibility and broader domain edge cases remain incomplete; 100% coverage not evidenced | Test Engineering: expand behavior-first tests | Exact-head coverage and browser evidence | +| Policy model | ADR-0001/0002, ARCHITECTURE, TRD | Designed, browser-memory only | No versioned durable schema, migration, audit history, UPSERT/idempotency or 3NF implementation | Platform: define `policy_revision` schema and item-level conflict contracts before storage | Schema/contract tests and migration round trip | +| Publication | CTA truthfully exposes readiness rather than pretending to publish; architecture defines immutable `publication_revision` | Planned | No authenticated approval, immutable publication, supersession or public URL lifecycle | Review & Publication: implement only after persistence/security entry criteria | Authorization, replay/digest, supersession and rollback tests | +| Legal/rule traceability | `docs/research-traceability.md`; official PIPA/PIPC source register | Source baseline established | No article/section-level requirement mapping or versioned rule engine | Legal Source Registry: map each implemented legal rule to source/effective date/revision | Fixtures across current and future-effective snapshots | +| Security/privacy | `docs/SECURITY.md`; local-first runtime | Baseline documented | Hosted tenant model, secrets boundary, encryption/key handling, audit and incident/retention evidence absent | Platform/Security: threat-model hosted boundary before backend | Security tests and exact-head org scans | +| Tests | Vitest routing/navigation/readiness regressions, zero-inferred-facts and stale-mode tests, responsive CSS regression | Improved | Browser E2E, accessibility and broader domain edge cases remain incomplete; 100% execution coverage not yet evidenced | Test Engineering: expand behavior-first tests and measure touched coverage | Exact-head coverage and browser evidence | +| Documentation fitness | Production functions touched by the fact-authority repair now carry JSDoc; PRD/TRD/ADR/architecture/changelog/baseline align on the contract | Improved | Repository-wide 100% docstring coverage has not yet been independently measured | Test Engineering: add a deterministic documentation-coverage check if compatible with TypeScript tooling | Exact-head documented-function inventory | | Performance | Static Vite client | Unevidenced | No page performance or network/load evidence | Operability: baseline browser performance; add k6 only with backend | Realistic measurements before latency claims | -| CI/security merge gate | SHA-pinned checkout plus repo and central required workflows | External live gate | Dependency Review can fail closed when GitHub's exact base/head dependency-graph compare is unavailable; review/model providers can also fail closed without publishable evidence | Control plane: repair incidents centrally and never substitute sibling scanners or predecessor evidence | Exact-head assigned jobs must execute; independent approval and threads required | +| CI/security merge gate | SHA-pinned checkout plus repo and central required workflows | External live gate | Dependency Review/review providers can fail closed independently of product code | Control plane: repair incidents centrally; never substitute predecessor evidence | Exact-head assigned jobs must execute; independent approval and threads required | ## DDD/context map Core subdomain: **Policy Fact Authoring**. Supporting: **Review & Publication**, **Legal Source Registry**. Generic infrastructure remains outside domain authority. `Policy Fact Authoring -> Review & Publication` supplies a versioned fact contract. `Legal Source Registry -> Review & Publication` supplies versioned source/rule evidence through an ACL. Customer/source-system integrations remain adapters and may not mutate foreign systems through hidden coupling. @@ -56,12 +47,12 @@ Ubiquitous language: `policy_revision`, `service_profile`, `collection_item`, `p The future aggregate root is the minimal `policy_revision` boundary. Rendered prose is a read projection. `publication_revision` is immutable and explicitly superseded rather than updated. Relational persistence is 3NF by default; named persistence objects use at least two semantic words and `snake_case`, and item-level UPSERT/idempotency semantics must be explicit. ## Buyer-visible release gates -A publishable release requires complete fact-to-warning navigation, deterministic rendering of captured source facts, explicit unresolved-review blocking, immutable reviewed publication revisions, accessible responsive interaction, authoritative source/version traceability, hosted security/privacy evidence, and exact-head CI/security/independent review. Buyer-facing wording describes assistance and reviewability, never a compliance guarantee or an internal implementation boundary. +A publishable release requires complete fact-to-warning navigation, deterministic rendering of captured source facts, no inferred customer operational facts, explicit unresolved-review blocking, immutable reviewed publication revisions, accessible responsive interaction, authoritative source/version traceability, hosted security/privacy evidence, and exact-head CI/security/independent review. Buyer-facing wording describes assistance and reviewability, never a compliance guarantee or an internal implementation boundary. ## Active commercialization order 1. Reacquire exact-current-head CI/security/SAST/central workflow execution and independent approval; merge PR #1 only through ordinary protection. 2. Add browser-level responsive/accessibility evidence for the repaired seven-step workflow and verify all remaining interaction edges. -3. Convert retention/transfer/contact placeholders into typed domain facts and source-backed deterministic validations. +3. Convert service identity, retention, transfer, contact, and collection-path placeholders into typed domain facts and source-backed deterministic validations. 4. Define and test the versioned `policy_revision` persistence/audit schema and explicit item-level UPSERT contracts. 5. Implement secure review/publication with immutable releases and explicit supersession. -6. Add hosted tenant isolation, audit/incident evidence, compose deployment, and realistic performance/load tests when network services exist. +6. Add hosted tenant isolation, audit/incident evidence, compose deployment, and realistic performance/load tests when network services exist. \ No newline at end of file From 1e77d4424f4b028e56f0453afe17f78dd5cebeca Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 13:01:10 +0900 Subject: [PATCH 075/282] test: require all seven authoring facts --- src/App.test.tsx | 75 +++++++++++++++++++++++++++++++++++++++------- src/policy.test.ts | 71 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 134 insertions(+), 12 deletions(-) diff --git a/src/App.test.tsx b/src/App.test.tsx index d049930..8c8c29c 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -5,6 +5,27 @@ import App from './App' afterEach(cleanup) +function completeNonCollectionFacts(container: HTMLElement) { + const stepButtons = container.querySelectorAll('.rail li button') + + fireEvent.click(stepButtons[0]) + fireEvent.change(container.querySelector('input[name="serviceName"]')!, { target: { value: '예시 서비스' } }) + fireEvent.change(container.querySelector('input[name="serviceUrl"]')!, { target: { value: 'https://example.test' } }) + + fireEvent.click(stepButtons[3]) + fireEvent.change(container.querySelector('input[name="retentionPeriod"]')!, { target: { value: '회원 탈퇴 시까지' } }) + + fireEvent.click(stepButtons[4]) + fireEvent.change(container.querySelector('select[name="thirdPartyStatus"]')!, { target: { value: 'no' } }) + + fireEvent.click(stepButtons[5]) + fireEvent.change(container.querySelector('select[name="internationalStatus"]')!, { target: { value: 'no' } }) + + fireEvent.click(stepButtons[6]) + fireEvent.change(container.querySelector('input[name="privacyOfficerName"]')!, { target: { value: '개인정보보호 담당' } }) + fireEvent.change(container.querySelector('input[name="privacyOfficerEmail"]')!, { target: { value: 'privacy@example.test' } }) +} + describe('policy editing workflow', () => { it('일곱 단계의 입력 화면을 전환한다', () => { const { container } = render() @@ -16,11 +37,11 @@ describe('policy editing workflow', () => { } }) - it('새 작업공간은 운영 사실을 임의 선택하지 않고 수집 여부 확인 전 공개 준비를 차단한다', () => { + it('새 작업공간은 운영 사실을 임의 선택하지 않고 일곱 단계 미확인 사실을 공개 준비 차단에 포함한다', () => { const { container } = render() const selected = Array.from(container.querySelectorAll('.check-label input')).filter((input) => input.checked) expect(selected).toHaveLength(0) - expect(container.querySelector('.review-stat.blocking b')?.textContent).toBe('1건') + expect(container.querySelector('.review-stat.blocking b')?.textContent).toBe('8건') expect(container.querySelector('.publish')?.disabled).toBe(true) expect(container.querySelector('.document-warning')?.textContent).toContain('수집 항목') }) @@ -72,7 +93,7 @@ describe('policy editing workflow', () => { fireEvent.click(container.querySelectorAll('.rail li button')[2]) fireEvent.change(container.querySelector('input[name="purpose-phone"]')!, { target: { value: '본인 확인 및 알림 발송' } }) - expect(container.querySelector('.review-stat.blocking b')?.textContent).toBe('0건') + expect(container.querySelector('.review-stat.blocking b')?.textContent).toBe('7건') fireEvent.click(container.querySelectorAll('.rail li button')[1]) fireEvent.click(phoneCheckbox()) @@ -81,7 +102,7 @@ describe('policy editing workflow', () => { const reenabledPhone = container.querySelectorAll('.item-list .item')[2] expect(reenabledPhone.querySelector('input[placeholder="예: 회원가입 화면"]')?.value).toBe('') expect(reenabledPhone.querySelector('select')?.value).toBe('') - expect(container.querySelector('.review-stat.blocking b')?.textContent).toBe('2건') + expect(container.querySelector('.review-stat.blocking b')?.textContent).toBe('9건') fireEvent.click(container.querySelectorAll('.rail li button')[2]) expect(container.querySelector('input[name="purpose-phone"]')?.value).toBe('') @@ -98,7 +119,7 @@ describe('policy editing workflow', () => { const purpose = container.querySelector('input[name="purpose-name"]')! fireEvent.change(purpose, { target: { value: ' ' } }) - expect(container.querySelector('.review-stat.blocking b')?.textContent).toBe('1건') + expect(container.querySelector('.review-stat.blocking b')?.textContent).toBe('8건') const nameRow = Array.from(container.querySelectorAll('.paper tbody tr')).find((row) => row.cells[0]?.textContent === '이름')! expect(nameRow.cells[1]?.textContent).toBe('처리 목적 입력 필요') expect(nameRow.cells[1]?.classList.contains('missing')).toBe(true) @@ -131,17 +152,51 @@ describe('policy editing workflow', () => { expect(container.querySelector('.form-panel h1')?.textContent).toBe('2. 수집 항목') }) - it('처리 목적과 수집 구분을 보완하면 구현 내부가 아닌 고객의 다음 검토 행동을 안내한다', () => { + it('제3자 제공과 국외 이전은 확인 전 상태를 별도로 표현하고 없음 확인 시 종속 사실을 요구하지 않는다', () => { + const { container } = render() + const buttons = container.querySelectorAll('.rail li button') + + fireEvent.click(buttons[4]) + const thirdParty = container.querySelector('select[name="thirdPartyStatus"]')! + expect(thirdParty.value).toBe('') + expect(container.querySelector('input[name="thirdPartyRecipient"]')).toBeNull() + fireEvent.change(thirdParty, { target: { value: 'no' } }) + expect(container.querySelector('input[name="thirdPartyRecipient"]')).toBeNull() + + fireEvent.click(buttons[5]) + const international = container.querySelector('select[name="internationalStatus"]')! + expect(international.value).toBe('') + fireEvent.change(international, { target: { value: 'no' } }) + expect(container.querySelector('input[name="internationalCountry"]')).toBeNull() + }) + + it('제공 있음에서 없음으로 바꾸면 이전 수령자 사실을 폐기해 재활성화 시 재검토한다', () => { + const { container } = render() + const buttons = container.querySelectorAll('.rail li button') + fireEvent.click(buttons[4]) + const status = container.querySelector('select[name="thirdPartyStatus"]')! + fireEvent.change(status, { target: { value: 'yes' } }) + fireEvent.change(container.querySelector('input[name="thirdPartyRecipient"]')!, { target: { value: '외부 처리자' } }) + fireEvent.change(container.querySelector('input[name="thirdPartyPurpose"]')!, { target: { value: '업무 처리' } }) + fireEvent.change(status, { target: { value: 'no' } }) + fireEvent.change(status, { target: { value: 'yes' } }) + expect(container.querySelector('input[name="thirdPartyRecipient"]')?.value).toBe('') + expect(container.querySelector('input[name="thirdPartyPurpose"]')?.value).toBe('') + }) + + it('모든 제품 정의 필수 사실을 확인한 뒤에만 공개 준비 확인을 허용한다', () => { const { container } = render() const phone = container.querySelectorAll('.check-label input')[2] fireEvent.click(phone) const phoneItem = container.querySelectorAll('.item-list .item')[2] fireEvent.change(phoneItem.querySelector('select')!, { target: { value: '필수' } }) - const warningButton = Array.from(container.querySelectorAll('.document-warning button')).find((button) => button.textContent?.includes('처리 목적'))! - fireEvent.click(warningButton) + fireEvent.click(container.querySelectorAll('.rail li button')[2]) + fireEvent.change(container.querySelector('input[name="purpose-phone"]')!, { target: { value: '본인 확인 및 알림 발송' } }) + + expect(container.querySelector('.review-stat.blocking b')?.textContent).toBe('7건') + expect(container.querySelector('.publish')?.disabled).toBe(true) - const purpose = container.querySelector('input[name="purpose-phone"]')! - fireEvent.change(purpose, { target: { value: '본인 확인 및 알림 발송' } }) + completeNonCollectionFacts(container) expect(container.querySelector('.review-stat.blocking b')?.textContent).toBe('0건') const publish = container.querySelector('.publish')! diff --git a/src/policy.test.ts b/src/policy.test.ts index aad4af3..5dfc9bc 100644 --- a/src/policy.test.ts +++ b/src/policy.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest' -import { getReview, initialItems } from './policy' +import { getDraftReview, getReview, initialFacts, initialItems } from './policy' -describe('policy review', () => { +describe('collection policy review', () => { it('starts without invented operational selections, collection modes, or processing purposes', () => { expect(initialItems.filter((item) => item.enabled)).toEqual([]) expect(initialItems.every((item) => item.mode === '')).toBe(true) @@ -35,4 +35,71 @@ describe('policy review', () => { expect(review.modeBlocking.map((item) => item.id)).not.toContain('phone') expect(review.blockingCount).toBe(0) }) +}) + +describe('seven-step draft readiness', () => { + it('starts with every unresolved non-collection authoring responsibility blocked', () => { + expect(getDraftReview(initialFacts).map((finding) => finding.code)).toEqual([ + 'service_name', + 'service_url', + 'retention_period', + 'third_party_status', + 'international_status', + 'privacy_contact_name', + 'privacy_contact_email', + ]) + }) + + it('treats explicit no-transfer attestations as complete without inventing recipients', () => { + const facts = { + ...initialFacts, + serviceName: '예시 서비스', + serviceUrl: 'https://example.test', + retentionPeriod: '회원 탈퇴 시까지', + thirdPartyStatus: 'no' as const, + internationalStatus: 'no' as const, + privacyOfficerName: '개인정보보호 담당', + privacyOfficerEmail: 'privacy@example.test', + } + expect(getDraftReview(facts)).toEqual([]) + }) + + it('requires dependent transfer facts only when the operator confirms a transfer exists', () => { + const facts = { + ...initialFacts, + serviceName: '예시 서비스', + serviceUrl: 'https://example.test', + retentionPeriod: '회원 탈퇴 시까지', + thirdPartyStatus: 'yes' as const, + internationalStatus: 'yes' as const, + privacyOfficerName: '개인정보보호 담당', + privacyOfficerEmail: 'privacy@example.test', + } + expect(getDraftReview(facts).map((finding) => finding.code)).toEqual([ + 'third_party_recipient', + 'third_party_purpose', + 'international_country', + 'international_recipient', + ]) + }) + + it('normalizes whitespace-only authoring facts as unresolved', () => { + const facts = { + ...initialFacts, + serviceName: ' ', + serviceUrl: '\t', + retentionPeriod: '\n', + thirdPartyStatus: 'no' as const, + internationalStatus: 'no' as const, + privacyOfficerName: ' ', + privacyOfficerEmail: ' ', + } + expect(getDraftReview(facts).map((finding) => finding.code)).toEqual([ + 'service_name', + 'service_url', + 'retention_period', + 'privacy_contact_name', + 'privacy_contact_email', + ]) + }) }) \ No newline at end of file From cbe3a743bd8ba09b3c63d4b79507975a6a77547a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 13:04:11 +0900 Subject: [PATCH 076/282] fix: require complete seven-step authoring facts --- src/App.tsx | 89 ++++++++++++++++++++++++++------------------------- src/policy.ts | 77 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 122 insertions(+), 44 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index a25b9de..18563b5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,36 +1,14 @@ import { useMemo, useState } from 'react' import { AlertTriangle, Check, ChevronDown, ExternalLink, FileText, Link, Save } from 'lucide-react' -import { getReview, initialItems, PolicyItem, steps } from './policy' - -type DraftFacts = { - serviceName: string - serviceUrl: string - retentionPeriod: string - thirdPartyRecipient: string - thirdPartyPurpose: string - internationalCountry: string - internationalRecipient: string - privacyOfficerName: string - privacyOfficerEmail: string -} +import { DraftFacts, getDraftReview, getReview, initialFacts, initialItems, PolicyItem, steps } from './policy' type FactField = { key: keyof DraftFacts label: string - placeholder: string - type?: 'text' | 'email' | 'url' -} - -const initialFacts: DraftFacts = { - serviceName: '', - serviceUrl: '', - retentionPeriod: '', - thirdPartyRecipient: '', - thirdPartyPurpose: '', - internationalCountry: '', - internationalRecipient: '', - privacyOfficerName: '', - privacyOfficerEmail: '', + placeholder?: string + type?: 'text' | 'email' | 'url' | 'select' + options?: Array<{ value: string; label: string }> + visibleWhen?: { key: keyof DraftFacts; equals: string } } /** Renders the seven-step authoring rail and keyboard-targetable preview shortcut. */ @@ -66,12 +44,25 @@ function FactStep({ current, title, description, fields, facts, setFacts, setCur setFacts: (facts: DraftFacts) => void setCurrent: (step: number) => void }) { - const update = (key: keyof DraftFacts, value: string) => setFacts({ ...facts, [key]: value }) + const update = (key: keyof DraftFacts, value: string) => { + const next = { ...facts, [key]: value } as DraftFacts + if (key === 'thirdPartyStatus' && value !== 'yes') { + next.thirdPartyRecipient = '' + next.thirdPartyPurpose = '' + } + if (key === 'internationalStatus' && value !== 'yes') { + next.internationalCountry = '' + next.internationalRecipient = '' + } + setFacts(next) + } return

{current}. {title}

{description}

사실 기반 입력운영 중인 서비스와 계약·처리 흐름에서 확인한 사실만 입력하세요. 확인되지 않은 내용은 비워 두고 검토 대상으로 남깁니다.

확인 정보

-
{fields.map((field) => )}
+
{fields.filter((field) => !field.visibleWhen || facts[field.visibleWhen.key] === field.visibleWhen.equals).map((field) => )}
} @@ -124,21 +115,28 @@ function EditingPanel({ current, items, setItems, facts, setFacts, setCurrent }: if (current === 2) return if (current === 3) return + const yesNoOptions = [ + { value: '', label: '확인 필요' }, + { value: 'yes', label: '있음' }, + { value: 'no', label: '없음' }, + ] const stepConfig: Record = { 1: { title: '서비스 정보', description: '개인정보처리방침이 적용되는 서비스와 공개 위치를 확인합니다.', fields: [ - { key: 'serviceName', label: '서비스 이름', placeholder: '예: PolicyWeave' }, + { key: 'serviceName', label: '서비스 이름', placeholder: '예: 서비스 이름' }, { key: 'serviceUrl', label: '서비스 URL', placeholder: 'https://example.com', type: 'url' }, ] }, 4: { title: '보유 기간', description: '수집한 개인정보를 언제까지 보유하는지 운영 사실과 근거에 맞춰 기록합니다.', fields: [ - { key: 'retentionPeriod', label: '대표 보유 기간 또는 종료 조건', placeholder: '예: 회원 탈퇴 시까지, 법정 보존 항목은 별도 기간' }, + { key: 'retentionPeriod', label: '대표 보유 기간 또는 종료 조건', placeholder: '예: 회원 탈퇴 시까지, 별도 보존 근거가 있는 항목은 해당 기간' }, ] }, - 5: { title: '제3자 제공', description: '제3자에게 개인정보를 제공하는 경우 제공받는 자와 목적을 확인합니다.', fields: [ - { key: 'thirdPartyRecipient', label: '제공받는 자', placeholder: '없으면 비워 두고 검토에서 확인' }, - { key: 'thirdPartyPurpose', label: '제공 목적', placeholder: '실제 제공 목적' }, + 5: { title: '제3자 제공', description: '제3자 제공 여부를 먼저 확인하고, 실제 제공이 있는 경우 제공받는 자와 목적을 기록합니다.', fields: [ + { key: 'thirdPartyStatus', label: '제3자 제공 여부', type: 'select', options: yesNoOptions }, + { key: 'thirdPartyRecipient', label: '제공받는 자', placeholder: '실제 제공받는 자', visibleWhen: { key: 'thirdPartyStatus', equals: 'yes' } }, + { key: 'thirdPartyPurpose', label: '제공 목적', placeholder: '실제 제공 목적', visibleWhen: { key: 'thirdPartyStatus', equals: 'yes' } }, ] }, - 6: { title: '국외 이전', description: '개인정보의 국외 이전이 있는 경우 국가와 수령자를 확인합니다.', fields: [ - { key: 'internationalCountry', label: '이전 국가', placeholder: '예: 일본' }, - { key: 'internationalRecipient', label: '국외 수령자', placeholder: '실제 수령 법인 또는 서비스' }, + 6: { title: '국외 이전', description: '국외 이전 여부를 먼저 확인하고, 실제 이전이 있는 경우 국가와 수령자를 기록합니다.', fields: [ + { key: 'internationalStatus', label: '국외 이전 여부', type: 'select', options: yesNoOptions }, + { key: 'internationalCountry', label: '이전 국가', placeholder: '실제 이전 국가', visibleWhen: { key: 'internationalStatus', equals: 'yes' } }, + { key: 'internationalRecipient', label: '국외 수령자', placeholder: '실제 수령 법인 또는 서비스', visibleWhen: { key: 'internationalStatus', equals: 'yes' } }, ] }, 7: { title: '개인정보 보호 담당자', description: '개인정보 관련 문의를 받을 책임자와 연락 채널을 기록합니다.', fields: [ { key: 'privacyOfficerName', label: '담당자 또는 담당 부서', placeholder: '예: 개인정보보호 담당' }, @@ -152,9 +150,11 @@ function EditingPanel({ current, items, setItems, facts, setFacts, setCurrent }: /** Projects verified authoring facts and deterministic readiness findings into the review draft. */ function DocumentPreview({ items, facts, setCurrent }: { items: PolicyItem[]; facts: DraftFacts; setCurrent: (step: number) => void }) { const review = useMemo(() => getReview(items), [items]) + const draftFindings = useMemo(() => getDraftReview(facts), [facts]) + const blockingCount = review.blockingCount + draftFindings.length return

개인정보처리방침 미리보기

-
근거 법령 개인정보 보호법{review.blockingCount ? `검토 필요 ${review.blockingCount}` : '필수 확인 완료'}버전 0.1.0
+
근거 법령 개인정보 보호법{blockingCount ? `검토 필요 ${blockingCount}` : '필수 확인 완료'}버전 0.1.0

{facts.serviceName || '개인정보처리방침'} (검토본)

{facts.serviceUrl &&

적용 서비스: {facts.serviceUrl}

} @@ -168,9 +168,10 @@ function DocumentPreview({ items, facts, setCurrent }: { items: PolicyItem[]; fa {review.selectionMissing &&
공개 전 확인실제 수집 항목이 아직 확인되지 않았습니다.
} {review.modeBlocking.length > 0 &&
공개 전 확인{review.modeBlocking.map((item) => item.label).join(', ')}의 수집 구분을 확인해야 합니다.
} {review.blocking.length > 0 &&
공개 전 확인{review.blocking.map((item) => item.label).join(', ')}의 처리 목적이 입력되지 않았습니다.
} + {draftFindings.map((finding) =>
공개 전 확인{finding.label} 확인이 필요합니다.
)}

제2조 (처리 및 보유 기간)

{facts.retentionPeriod || '보유 기간 단계에서 확인한 운영 기준을 입력해야 합니다.'}

-

제3조 (제3자 제공)

{facts.thirdPartyRecipient ? `${facts.thirdPartyRecipient}에 ${facts.thirdPartyPurpose || '확인 중인 목적'}으로 제공하는 흐름을 검토 중입니다.` : '제3자 제공 여부를 확인하는 단계가 남아 있습니다.'}

-

제4조 (국외 이전)

{facts.internationalCountry || facts.internationalRecipient ? `${facts.internationalCountry || '국가 확인 필요'} · ${facts.internationalRecipient || '수령자 확인 필요'}` : '국외 이전 여부를 확인하는 단계가 남아 있습니다.'}

+

제3조 (제3자 제공)

{facts.thirdPartyStatus === 'no' ? '제3자 제공 없음으로 확인되었습니다.' : facts.thirdPartyStatus === 'yes' ? `${facts.thirdPartyRecipient || '제공받는 자 확인 필요'}에 ${facts.thirdPartyPurpose || '제공 목적 확인 필요'}으로 제공하는 흐름을 검토 중입니다.` : '제3자 제공 여부를 확인하는 단계가 남아 있습니다.'}

+

제4조 (국외 이전)

{facts.internationalStatus === 'no' ? '국외 이전 없음으로 확인되었습니다.' : facts.internationalStatus === 'yes' ? `${facts.internationalCountry || '국가 확인 필요'} · ${facts.internationalRecipient || '수령자 확인 필요'}` : '국외 이전 여부를 확인하는 단계가 남아 있습니다.'}

개인정보 보호 문의

{facts.privacyOfficerName || '담당자 확인 필요'} · {facts.privacyOfficerEmail || '연락처 확인 필요'}

입력 내용은 검토본에 즉시 반영됩니다. 생성된 문서는 법률 자문이 아닙니다. 공개 전 책임자의 검토가 필요합니다. @@ -182,12 +183,14 @@ export default function App() { const [items, setItems] = useState(initialItems) const [facts, setFacts] = useState(initialFacts) const [current, setCurrent] = useState(2) - const review = useMemo(() => getReview(items), [items]) + const collectionReview = useMemo(() => getReview(items), [items]) + const draftFindings = useMemo(() => getDraftReview(facts), [facts]) + const blockingCount = collectionReview.blockingCount + draftFindings.length const [message, setMessage] = useState('') - function publish() { setMessage(review.blockingCount ? '필수 확인 항목을 먼저 입력하세요.' : '필수 확인이 완료되었습니다. 현재 검토본을 책임자와 검토하고 필요한 사실을 보완하세요.') } + function publish() { setMessage(blockingCount ? '필수 확인 항목을 먼저 입력하세요.' : '필수 확인이 완료되었습니다. 현재 검토본을 책임자와 검토하고 필요한 사실을 보완하세요.') } return
PolicyWeave{facts.serviceName || '내 서비스'} 개인정보처리방침작성 중버전 0.1.0 (임시저장) 브라우저 작업 중
-
검토 요약확인을 마친 뒤 공개 준비 상태를 확인하세요.
필수 확인 {review.blockingCount}건
권장 검토 {review.recommended.length}건
{message}
+
검토 요약확인을 마친 뒤 공개 준비 상태를 확인하세요.
필수 확인 {blockingCount}건
권장 검토 {collectionReview.recommended.length}건
{message}
} \ No newline at end of file diff --git a/src/policy.ts b/src/policy.ts index df4d598..0fbc903 100644 --- a/src/policy.ts +++ b/src/policy.ts @@ -1,5 +1,35 @@ export type CollectionMode = '' | '필수' | '선택' -export type PolicyItem = { id: string; label: string; description: string; purpose: string; enabled: boolean; mode: CollectionMode; detail?: string } +export type DisclosureStatus = '' | 'yes' | 'no' + +export type PolicyItem = { + id: string + label: string + description: string + purpose: string + enabled: boolean + mode: CollectionMode + detail?: string +} + +export type DraftFacts = { + serviceName: string + serviceUrl: string + retentionPeriod: string + thirdPartyStatus: DisclosureStatus + thirdPartyRecipient: string + thirdPartyPurpose: string + internationalStatus: DisclosureStatus + internationalCountry: string + internationalRecipient: string + privacyOfficerName: string + privacyOfficerEmail: string +} + +export type DraftFinding = { + code: string + step: number + label: string +} export const initialItems: PolicyItem[] = [ { id: 'name', label: '이름', description: '서비스 이용자 식별', purpose: '', enabled: false, mode: '' }, @@ -13,6 +43,20 @@ export const initialItems: PolicyItem[] = [ { id: 'content', label: '게시물 및 문의 내용', description: '게시 기능, 고객 문의 처리', purpose: '', enabled: false, mode: '' }, ] +export const initialFacts: DraftFacts = { + serviceName: '', + serviceUrl: '', + retentionPeriod: '', + thirdPartyStatus: '', + thirdPartyRecipient: '', + thirdPartyPurpose: '', + internationalStatus: '', + internationalCountry: '', + internationalRecipient: '', + privacyOfficerName: '', + privacyOfficerEmail: '', +} + export const steps = ['서비스 정보', '수집 항목', '처리 목적', '보유 기간', '제3자 제공', '국외 이전', '개인정보 보호 담당자'] /** Derives deterministic readiness findings from operator-confirmed collection facts. */ @@ -24,4 +68,35 @@ export function getReview(items: PolicyItem[]) { const blockingCount = blocking.length + modeBlocking.length + (selectionMissing ? 1 : 0) const recommended = enabled.filter((item) => item.mode === '선택' && item.id !== 'usage') return { enabled, blocking, modeBlocking, selectionMissing, blockingCount, recommended } +} + +/** Derives authoring-completeness findings for the non-collection responsibilities in the seven-step workflow. */ +export function getDraftReview(facts: DraftFacts): DraftFinding[] { + const findings: DraftFinding[] = [] + const addWhenBlank = (value: string, code: string, step: number, label: string) => { + if (!value.trim()) findings.push({ code, step, label }) + } + + addWhenBlank(facts.serviceName, 'service_name', 1, '서비스 이름') + addWhenBlank(facts.serviceUrl, 'service_url', 1, '서비스 URL') + addWhenBlank(facts.retentionPeriod, 'retention_period', 4, '보유 기간') + + if (!facts.thirdPartyStatus) { + findings.push({ code: 'third_party_status', step: 5, label: '제3자 제공 여부' }) + } else if (facts.thirdPartyStatus === 'yes') { + addWhenBlank(facts.thirdPartyRecipient, 'third_party_recipient', 5, '제3자 제공받는 자') + addWhenBlank(facts.thirdPartyPurpose, 'third_party_purpose', 5, '제3자 제공 목적') + } + + if (!facts.internationalStatus) { + findings.push({ code: 'international_status', step: 6, label: '국외 이전 여부' }) + } else if (facts.internationalStatus === 'yes') { + addWhenBlank(facts.internationalCountry, 'international_country', 6, '이전 국가') + addWhenBlank(facts.internationalRecipient, 'international_recipient', 6, '국외 수령자') + } + + addWhenBlank(facts.privacyOfficerName, 'privacy_contact_name', 7, '개인정보 보호 담당자') + addWhenBlank(facts.privacyOfficerEmail, 'privacy_contact_email', 7, '개인정보 보호 연락 이메일') + + return findings } \ No newline at end of file From 36b162388cca6b6d4c111e158bd5b9a69f5577ce Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 13:06:23 +0900 Subject: [PATCH 077/282] docs: align seven-step readiness contract --- ARCHITECTURE.md | 22 +++++---- CHANGELOG.md | 31 ++++++------ .../ADR-0002-seven-step-authoring-workflow.md | 12 +++-- docs/PRD.md | 17 ++++--- docs/TRD.md | 26 +++++----- docs/product-technical-gap-baseline.md | 49 ++++++++----------- 6 files changed, 85 insertions(+), 72 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index bc6f4c5..1e8ecbd 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -15,19 +15,23 @@ The core subdomain is **Policy Fact Authoring**. Supporting subdomains are **Rev `Policy Fact Authoring -> Review & Publication` is a customer/supplier relationship through a versioned policy-fact contract. `Legal Source Registry -> Review & Publication` supplies versioned rule evidence; source updates cannot silently mutate historical policy revisions. External customer systems remain behind adapters and must not be queried or mutated through hidden coupling. ## Ubiquitous language and model -A future `policy_revision` is the minimal aggregate root and transaction boundary. `collection_item`, `processing_purpose`, `retention_rule`, `third_party_transfer`, `international_transfer`, and `privacy_contact` are revision-owned facts/value objects unless later evidence requires independent lifecycles. `review_finding` is derived evidence. `publication_revision` is an immutable release receipt, not a mutable policy row. Rendered prose is a projection and never the source of truth. +A future `policy_revision` is the minimal aggregate root and transaction boundary. `service_profile`, `collection_item`, `processing_purpose`, `retention_rule`, `third_party_transfer`, `international_transfer`, and `privacy_contact` are revision-owned facts/value objects unless later evidence requires independent lifecycles. `review_finding` is derived evidence. `publication_revision` is an immutable release receipt, not a mutable policy row. Rendered prose is a projection and never the source of truth. Core invariants: -1. A fresh workspace contains no inferred customer operational facts: collection selection, collection mode, and processing purpose remain unresolved until the operator establishes them. +1. A fresh workspace contains no inferred customer operational facts; blank means unresolved rather than `none`. 2. Selecting a collection item without confirming its collection mode or processing purpose creates blocking review findings. -3. Disabling a collection item invalidates its dependent collection-mode, processing-purpose, and collection-path evidence; re-enabling it requires those facts to be captured and reviewed again rather than silently reviving stale evidence. -4. Review findings navigate to the fact that caused them. -5. Publication must never upgrade an unreviewed or incomplete draft to a reviewed/authoritative state. -6. A published revision remains reproducible from its policy facts plus rule/template/source versions. -7. External legal-source updates produce explicit re-evaluation, not silent rewriting. +3. Service identity, retention, third-party provision status, international-transfer status, and privacy contact remain blocking authoring responsibilities until explicitly established. +4. Third-party provision and international transfer distinguish unresolved, yes, and no. A `yes` status requires dependent facts; a transition away from yes invalidates dependent details so stale facts cannot revive silently. +5. Disabling a collection item invalidates its dependent collection-mode, processing-purpose, and collection-path evidence; re-enabling requires renewed confirmation. +6. Review findings navigate to the fact that caused them. +7. Publication must never upgrade an unreviewed or incomplete draft to a reviewed/authoritative state. +8. A published revision remains reproducible from its policy facts plus rule/template/source versions. +9. 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: every item begins unselected, with no inferred collection mode or processing purpose. `src/policy.ts` owns deterministic selection-established, collection-mode, and processing-purpose readiness rules; `src/App.tsx` owns browser orchestration, warning-to-source navigation, and deterministic preview rendering. +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 selection/mode/purpose and non-collection authoring-completeness findings; `src/App.tsx` owns browser orchestration, explicit transfer-status capture, warning-to-source navigation, stale dependent-fact invalidation, and deterministic preview rendering. + +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. ## Persistence boundary (planned, not implemented) Use relational 3NF by default. Named database/schema/persistence objects use at least two semantic words and `snake_case`, for example `policy_revision`, `collection_item`, `processing_purpose`, `review_finding`, `publication_revision`, and `legal_source_revision`. Item-level UPSERTs must declare their natural/idempotency key and conflict behavior. Publication is append-only/immutable with explicit supersession; writes across unrelated aggregates must not share a transaction merely for convenience. @@ -35,4 +39,4 @@ Use relational 3NF by default. Named database/schema/persistence objects use at Separate write-side draft commands from read-side rendered/review projections once hosted traffic justifies it. Account for revision hot spots and optimistic/constrained writes before adding collaborative editing. Keep source/customer integrations behind ACLs; do not form a shared kernel with unrelated ContextualWisdomLab products without demonstrated reuse. ## Deployment and operability direction -The browser-only MVP needs no service mesh. A hosted backend should be compose-deployable across Docker/Podman/Colima before Kubernetes migration, expose asynchronous/non-blocking request handling, and add realistic k6 evidence for network surfaces before latency claims. No code may depend on an optional `close_connection` instance attribute existing unless the adapter contract guarantees it. \ No newline at end of file +The browser-only MVP needs no service mesh. A hosted backend should be compose-deployable across Docker/Podman/Colima before Kubernetes migration, expose asynchronous/non-blocking request handling, and add realistic k6 evidence for network surfaces before latency claims. No code may depend on an optional `close_connection` instance attribute existing unless the adapter contract guarantees it. diff --git a/CHANGELOG.md b/CHANGELOG.md index 688231c..bd545e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,26 +6,29 @@ All notable product changes are recorded here. PolicyWeave is pre-release; entri ### Added - Seven-step authoring workflow with distinct editing surfaces for service information, collection items, processing purposes, retention, third-party transfer, international transfer, and privacy contact. -- Warning-to-source navigation for missing collection selection, collection mode, and processing purposes. -- Regression coverage for all seven step routes, zero-inferred startup facts, collection-mode confirmation, selection/mode warning navigation, service URL projection into the review draft, warning navigation, clearing publication blockers, whitespace-only purposes, collection-path/purpose step separation, disable/re-enable stale-evidence invalidation, preview/review whitespace consistency, buyer-facing publication guidance, and non-deceptive handling of unshipped export/generation/document-selection affordances. +- Warning-to-source navigation for missing collection selection, collection mode, processing purposes, service identity, retention, transfer statuses/details, and privacy contact. +- 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, collection-mode 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, and non-deceptive handling of unshipped affordances. - Product/technical gap ledger, architecture, technical requirements, security baseline, and legal-source traceability. ### Changed -- Fresh workspaces no longer preselect collection items or prefill collection modes/processing purposes; readiness now fails closed until the operator establishes actual collection facts. -- Disabling a collection item now clears its collection mode, processing purpose, and collection-path evidence so re-enabling it cannot silently revive stale customer facts. -- Service URL entered in the service-information step is now projected into the review draft instead of being silently omitted. -- Collection-path editing now remains available for every selected item in the collection step, while processing-purpose editing stays in its dedicated purpose step. -- Review preview now applies the same whitespace-normalized purpose-completeness contract as the blocking review rule. -- The publication-area CTA now describes a readiness check and directs the operator to responsible review rather than exposing repository/backend implementation details for an unshipped capability. -- Unshipped JSON export is visibly disabled as `준비 중`, the redundant no-op `검토본 생성` control was removed because the review draft already updates live, and the document title is rendered as status text rather than an inert dropdown-like button. -- Review preview now reflects verified authoring facts instead of presenting the collection step as the entire workflow. +- Fresh workspaces no longer preselect collection items or prefill collection modes/processing purposes; readiness fails closed until the operator establishes actual collection facts. +- Public-readiness now includes product-defined service name/URL, retention, transfer-status/detail, and privacy-contact completeness rather than allowing the CTA to enable while those steps remain visibly unresolved. +- 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. +- Service URL entered in the service-information step is projected into the review draft. +- Collection-path editing remains in the collection step while processing-purpose editing stays in its dedicated purpose step. +- Review preview applies the same whitespace-normalized purpose-completeness contract as the blocking review rule. +- 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. - Responsive review behavior and mobile publication feedback were repaired during PR review. -- Responsive CSS contract tests now use only literal media-query regular expressions, removing the Semgrep dynamic-RegExp SAST finding without suppressing or weakening the scanner gate. +- Responsive CSS contract tests use literal media-query regular expressions, removing the Semgrep dynamic-RegExp finding without suppressing or weakening the scanner gate. - GitHub Actions checkout dependencies are SHA pinned and credentials are not persisted in the working tree. ### Not yet shipped -- Requirement-complete typed validation for service identity, retention, transfers, privacy contact, and collection-path evidence. +- Authoritative legal-rule snapshots that can determine legal sufficiency beyond product-defined fact completeness. +- Browser-matrix/accessibility evidence and screenshot verification. - Durable versioned policy persistence and audit history. - Authenticated immutable publication revisions and public URL lifecycle. -- Browser-matrix/accessibility evidence and hosted endpoint load testing. -- Legal/rule engine backed by versioned authoritative requirements. \ No newline at end of file +- Hosted tenant/security/operability evidence and endpoint load testing. diff --git a/docs/ADR-0002-seven-step-authoring-workflow.md b/docs/ADR-0002-seven-step-authoring-workflow.md index 2e8773d..91350b3 100644 --- a/docs/ADR-0002-seven-step-authoring-workflow.md +++ b/docs/ADR-0002-seven-step-authoring-workflow.md @@ -8,17 +8,23 @@ The PRD defines seven authoring stages, but an early implementation changed only A later commercialization pass found a second workflow-integrity defect: the fresh workspace preselected collection items and prefilled service-specific collection modes and processing purposes. Those values looked like customer facts even though the operator had never established them, contradicting PolicyWeave's fact-authoring boundary and its prohibition on synthetic production data. +A subsequent exact-head pass found that even after collection facts were repaired, the readiness CTA could enable while service identity, retention, transfer statuses, and privacy contact were still visibly unresolved in the review draft. Blank transfer fields were also ambiguous between `none` and `not yet checked`. + ## Decision Each PRD stage is an explicit editable state of the Policy Fact Authoring context: service information, collection items, processing purposes, retention, third-party transfer, international transfer, and privacy contact. The UI router must render a corresponding editor for the selected stage. Review findings carry enough domain context to navigate to the responsible stage. The collection catalog is metadata, not an assertion about a customer's service. A fresh workspace therefore starts with every collection item unselected and with collection mode and processing purpose unresolved. Selecting an item requires the operator to establish its collection mode and purpose before readiness can clear. An empty selection is treated as unresolved, not as an inferred assertion that the service collects no personal data. Disabling an item invalidates its mode, purpose, and collection-path evidence so re-enabling cannot silently restore stale facts. -The workflow is not a legal state machine. Completion of a UI step does not mean legal sufficiency. Legal/rule completeness is determined separately by deterministic review rules bound to versioned authoritative source evidence. +Readiness also requires the product-defined facts owned by the other authoring stages: service name and URL, retention period, third-party provision status, international-transfer status, and privacy-contact owner/email. Third-party provision and international transfer use explicit unresolved/yes/no states. `no` is an operator attestation, not an inference from a blank field. `yes` requires its dependent recipient/purpose or country/recipient facts. A transition away from `yes` clears those dependent values so stale operational facts do not silently revive. + +These readiness rules are authoring-completeness rules, not a legal state machine. Completion of a UI step or zero product-defined blockers does not mean legal sufficiency. Legal/rule completeness is determined separately by deterministic rules bound to versioned authoritative source evidence. ## Consequences - Navigation and review-to-source behavior are regression-tested. - Production startup state contains taxonomy metadata only, not inferred customer operational facts. -- Selection-not-established, collection-mode, and processing-purpose findings fail closed and navigate to their owning step. +- Selection, collection mode, processing purpose, service identity, retention, transfer status/detail, and privacy contact findings fail closed and navigate to their owning step. +- Blank transfer state can no longer masquerade as an explicit `none` attestation. +- Stale collection and transfer-dependent facts are invalidated when their owning status changes. - The preview remains a projection over structured facts and cannot become an independent source of truth. - Future persistence stores facts and revision/review state rather than serialized page prose. -- Additional legal validations can grow without redefining the seven buyer-facing authoring responsibilities. \ No newline at end of file +- Additional legal validations can grow without redefining the seven buyer-facing authoring responsibilities. diff --git a/docs/PRD.md b/docs/PRD.md index f1115cc..ff5cc90 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -6,14 +6,15 @@ ## 제품 원칙 -PolicyWeave는 법률 문장을 임의로 창작하는 도구가 아니다. 운영자가 입력한 사실을 구조화하고, 적용 근거와 확인이 필요한 부분을 분리해 검토본을 만든다. 자동 생성 결과는 변호사·개인정보보호책임자의 검토를 대체하지 않는다. 새 작업공간은 고객의 실제 수집 여부, 수집 구분 또는 처리 목적을 추정하거나 미리 선택하지 않는다. +PolicyWeave는 법률 문장을 임의로 창작하는 도구가 아니다. 운영자가 입력한 사실을 구조화하고, 적용 근거와 확인이 필요한 부분을 분리해 검토본을 만든다. 자동 생성 결과는 변호사·개인정보보호책임자의 검토를 대체하지 않는다. 새 작업공간은 고객의 실제 수집 여부, 수집 구분, 처리 목적, 제3자 제공 여부 또는 국외 이전 여부를 추정하지 않는다. 비어 있는 값은 `없음`이 아니라 `미확인`이다. ## MVP - 7단계 질문 흐름: 서비스 정보, 수집 항목, 처리 목적, 보유 기간, 제3자 제공, 국외 이전, 담당자 - 입력과 동시에 갱신되는 개인정보처리방침 검토본 -- 필수 입력 누락과 권장 검토 항목의 구분 -- 처리 항목별 목적·근거 연결 +- 제품 정의 필수 사실 누락과 권장 검토 항목의 구분 +- 처리 항목별 수집 구분·목적·수집 경로 연결 +- 제3자 제공과 국외 이전의 명시적 `있음`/`없음` 확인; `있음`일 때만 종속 상세 사실 요구 - 공개 전 검토 요약과 버전 정보 - 정적 공개 URL 발행 계약(후속 백엔드에서 구현) - JSON 내보내기 가능한 정책 데이터 모델 @@ -23,11 +24,15 @@ PolicyWeave는 법률 문장을 임의로 창작하는 도구가 아니다. 운 - 법률 자문 또는 준법 보장 - 사용자의 실제 서비스·SDK를 자동 추측 - 동의가 필요 없는 처리에 무조건 동의를 요구하는 방식 +- 빈 입력을 자동으로 `없음` 또는 적법 상태로 해석하는 방식 ## 성공 기준 -- 사용자는 누락 없이 검토본을 생성할 수 있다. -- 새 작업공간은 고객 운영 사실을 미리 채우지 않으며, 실제 수집 항목과 각 항목의 수집 구분·처리 목적이 확인되기 전에는 공개 준비 상태를 통과하지 않는다. +- 사용자는 일곱 작성 책임의 제품 정의 필수 사실을 누락 없이 검토할 수 있다. +- 새 작업공간은 고객 운영 사실을 미리 채우지 않는다. +- 실제 수집 항목과 각 항목의 수집 구분·처리 목적, 서비스 이름·URL, 보유 기간, 제3자 제공 여부, 국외 이전 여부, 개인정보 보호 담당자·연락 이메일이 확인되기 전에는 공개 준비 상태를 통과하지 않는다. +- 제3자 제공 또는 국외 이전을 `있음`으로 확인한 경우 해당 수령자/목적 또는 국가/수령자 사실까지 확인되어야 한다. +- `없음` 확인은 명시적 운영자 사실이며, 이전에 입력한 종속 제공·이전 상세 사실은 상태 변경 시 폐기된다. - 모든 경고는 해당 입력 단계로 이동할 수 있다. - 모바일에서도 작성과 미리보기를 전환할 수 있다. -- 키보드만으로 모든 입력과 주요 동작을 수행할 수 있다. \ No newline at end of file +- 키보드만으로 모든 입력과 주요 동작을 수행할 수 있다. diff --git a/docs/TRD.md b/docs/TRD.md index 73b6f99..ccb843a 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -6,26 +6,30 @@ 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. -- `src/policy.ts` contains deterministic review logic for whether collection selection has been established, whether selected items have an explicit collection mode, and whether selected items have a processing purpose. -- `src/App.tsx` provides the seven-step authoring flow, review navigation, and deterministic preview projection. +- `src/policy.ts` owns deterministic review logic for collection selection/mode/purpose and the non-collection authoring-completeness findings for service identity, retention, transfer statuses/details, and privacy contact. +- `src/App.tsx` provides the seven-step authoring flow, review navigation, conditional transfer editors, and deterministic preview projection. - The current CI contract is lint, Vitest, and TypeScript/Vite build plus live organization-required security/review workflows. ## Functional contracts 1. Every PRD step must route to an editable surface; selecting a step cannot change only the rail indicator. -2. A fresh workspace contains no inferred customer operational facts: collection selection, collection mode, and processing purpose start unresolved. +2. A fresh workspace contains no inferred customer operational facts. Blank authoring values mean unresolved, not `none`. 3. Until at least one actual collection item is selected, readiness produces a blocking selection-not-established finding that navigates to the collection step. -4. Every selected collection item requires an explicit collection mode; a missing mode is blocking and navigates to the collection step. -5. A selected item with blank/whitespace purpose produces a blocking review finding, and the preview must apply the same blank/whitespace normalization when it renders purpose completeness. -6. Disabling a collection item invalidates dependent collection-mode, processing-purpose, and collection-path evidence; re-enabling the item requires those facts to be captured and reviewed again. -7. Every blocking finding links to the responsible editing step. -8. Preview text is derived from current structured facts; it does not become an independently editable authority. -9. UI copy distinguishes a review draft from legal advice, certification, or a compliance guarantee. +4. Every selected collection item requires an explicit collection mode and nonblank processing purpose. +5. Service name, service URL, retention period, third-party provision status, international-transfer status, privacy-contact owner, and privacy-contact email are product-defined readiness facts and block readiness while unresolved. +6. 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. +7. Blank/whitespace authoring facts are normalized as unresolved. +8. Disabling a collection item invalidates dependent collection-mode, processing-purpose, and collection-path evidence; re-enabling requires renewed confirmation. +9. Every blocking finding links to the responsible editing step. +10. Preview text is derived from current structured facts; it does not become an independently editable authority. +11. UI copy distinguishes a review draft from legal advice, certification, or a compliance guarantee. + +These are authoring-completeness contracts, not claims that the resulting policy is legally sufficient. Legal sufficiency remains a separate versioned-rule responsibility backed by authoritative source evidence. ## Quality contracts - Touched production behavior requires regression and edge-case tests first. - Touched production functions carry descriptive JSDoc rather than relying on implicit behavior. - Do not suppress deprecation warnings to pass CI. -- Browser/accessibility validation must eventually include keyboard/focus order, WCAG 2.2 automated checks, responsive desktop/tablet/mobile interactions, and screenshot evidence. +- Browser/accessibility validation must include keyboard/focus order, WCAG 2.2 automated checks, responsive desktop/tablet/mobile interactions, and screenshot evidence before a buyer-facing accessibility claim. - 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. @@ -38,4 +42,4 @@ A publication command must operate on a specific reviewed `policy_revision` and Legal/rule data enters through a Legal Source Registry ACL with source identity, effective date, retrieval/version evidence, and an implementation mapping. Customer/application integrations require their own explicit adapters. If future LLM assistance is justified for drafting/explanation, calls must route through `ContextualWisdomLab/contextual-orchestrator`; deterministic validation and publication authority remain outside the model. ## Verification state -Only exact-current-head checks count. Queued, cancelled, skipped-required, or predecessor-head runs are not evidence of passing. Normal integration requires the live ruleset's independent approval and thread-resolution requirements; no administrative bypass is part of this TRD. \ No newline at end of file +Only exact-current-head checks count. Queued, cancelled, skipped-required, or predecessor-head runs are not evidence of passing. Normal integration requires the live ruleset's independent approval and thread-resolution requirements; no administrative bypass is part of this TRD. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 2de9514..b9f1639 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -2,42 +2,33 @@ Last reconciled: 2026-09-02 -This ledger records PolicyWeave's commercialization gap from PRD/ADR/TRD/architecture, implementation, authoritative legal-source evidence, review findings, and live GitHub state. Live Check conclusions are deliberately not committed because they change outside the repository; merge decisions must re-fetch the exact current PR head and live ruleset. +This ledger records PolicyWeave's commercialization gap from PRD/ADR/TRD/architecture, implementation, authoritative legal-source evidence, review findings, and live GitHub state. Live Check conclusions are not committed because they change outside the repository; merge decisions must re-fetch the exact current PR head and live ruleset. ## Product responsibility PolicyWeave is a local-first policy-fact workspace that helps a service operator structure actual personal-data processing facts, detect omissions or contradictions, and render a reviewable privacy-policy draft. It is not legal advice and does not claim compliance or certification. Structured facts are the source of truth; rendered prose is a deterministic projection over facts plus future versioned rule/template evidence. ## Exact-head implementation evidence -The seven-step routing defect and warning-navigation defect were reproduced on predecessor head `62b6f5ef9056a111c93a09ba40d205c98f42baef`: `App.tsx` always rendered the collection editor while regression tests expected all seven editors and a warning-navigation action. Tests were strengthened first at `4795f99beb51a95cbc7393a2bb0b9600389776d8`, then production routing/review behavior was repaired at `61f5bac24d0271f382668872140a7ef4a3463db4`. +Earlier PR review repaired seven-step routing/warning navigation, collection-path/purpose coupling, stale collection evidence, whitespace normalization, service-URL projection, misleading publication copy, unshipped/no-op action affordances, responsive preview behavior, checkout credential persistence, legal-source traceability, and the test-only dynamic-RegExp SAST finding. Those predecessor repairs remain covered by the regression suite but predecessor workflow conclusions are never merge evidence for a later head. -A later exact-head review found that collection-path editing was incorrectly coupled to an empty processing purpose: enabled items with an existing purpose lost the collection-path field, while entering a purpose made both fields disappear. The regression was added first at `b35e18f3831ea5a257b0a9397c08aa73af8c3f52`; `CollectionForm` was then repaired at `87d37b9a219ff50013f9ac46c3b1f11b52a94467` so collection-path editing remains in step 2 and purpose editing remains in step 3. `ec1aead4484018d52ce8bd5bbab04c517d42870d` also locks the existing whitespace-only-purpose blocking contract. +A 2026-09-02 exact-head commercialization pass found that `initialItems` preselected `name`, `email`, and `usage` and prefilled their collection modes and purposes even though the operator had never established those service-specific facts. Regression tests were committed first at `37c5c490e8cc04b3378f828130d9261edb80cfc3`; production was repaired at `b12052b03646706a89016fbacca52a9d58359022`, then PRD/ADR/TRD/architecture/changelog/baseline were reconciled at `8e242842049c245c593b625f058468518318bdea`. The fresh workspace now has zero inferred collection selection/mode/purpose facts, selected items fail closed on missing mode/purpose, and disabling an item invalidates its mode/purpose/path evidence. -On exact PR head `2d4c956b2b7b4cc3607c2724a4be2073393dc692`, disabling an enabled collection item preserved its processing purpose and collection path, so later re-enabling silently revived stale evidence without renewed review. The preview also used string truthiness while the review engine trimmed whitespace. Regression tests were added before production changes at `10ac51201afb59d677ed546dd7e32ebc7373031f` and `12f4cc8bae8aa1a324ba5238d593acb53e59dd8c`; production was repaired at `798ac3672812c5263a44de09a610e0b5cb9a7efc`. - -Buyer-facing workflow passes then repaired internal-boundary publication copy (`fcd75aa98242158d0abe5d8beb661dd0ff064011` -> `9d86a1316b03455c740c4b70296f604c52d9981f`), missing `serviceUrl` projection (`cc9c8a07881d1a06c81a92c82161b469fa52508c` -> `815bec382af557e8dc9550329d8c71586eddd297`), unshipped/no-op action affordances (`f21c65a589c73c9b020696a5eda8240249581f3c` -> `f00a6ad022c49ab7e125d2186c335462ec9bb2e2`), and an inert dropdown-like document-title control (`8233fe09271c3fb0b172892cd0f2fc953a1d84de` -> `c385196a3c9036fa7580ae58ac624b609f96c083`). - -The next exact-head commercialization pass found a more fundamental fact-authority violation. Production `initialItems` preselected `name`, `email`, and `usage` and prefilled their collection modes and processing purposes even though no operator had established those service-specific facts. This contradicted the PRD/AGENTS/TRD boundary that PolicyWeave structures customer-supplied facts and that production must not consume synthetic demo facts. Regression tests were committed first at `37c5c490e8cc04b3378f828130d9261edb80cfc3`, requiring zero inferred selection/mode/purpose facts, fail-closed readiness when collection selection is unestablished, explicit collection-mode confirmation, warning-to-owner navigation, and invalidation of stale mode evidence. Production was repaired at `b12052b03646706a89016fbacca52a9d58359022`: all taxonomy items now start unselected with unresolved mode/purpose, `getReview` derives selection/mode/purpose blocking findings and a single blocking count, the preview navigates each finding to its owner step, and disabling an item clears its mode/purpose/path evidence. Touched production functions also received descriptive JSDoc. This repair changes no legal rule and introduces no heuristic weighting. - -### 2026-09-02 predecessor Check RCA retained for traceability -On predecessor head `272c0aa7502c8768159ec7ba6188348e88d86b4f`, SAST run `33555808672` failed on a dynamic `RegExp` in `src/styles.test.ts`; the repair replaced it with literal regexes without suppressing the scanner. The same predecessor had an independent Dependency Review HTTP 403 availability/authorization failure tracked by `ContextualWisdomLab/.github#810`, while sibling OSV/Trivy/Scorecard jobs succeeded. Required Noema Review also failed closed because the returned verdict lacked required adversarial probes and bounded repair exceeded its absolute deadline. None of those predecessor results are valid evidence for a later head; exact-current-head workflows must be reacquired. +The next exact-head pass found a second buyer-facing authority defect: after completing one selected collection item, the readiness CTA could enable while service identity, retention, third-party provision, international transfer, and privacy contact remained visibly unresolved in the review draft. Blank transfer fields also conflated `not checked` with `none`. Regression/edge-case tests were committed first at `1e77d4424f4b028e56f0453afe17f78dd5cebeca`. Production was repaired at `cbe3a743bd8ba09b3c63d4b79507975a6a77547a`: `getDraftReview` now derives product-defined findings for all non-collection authoring responsibilities; third-party provision and international transfer use explicit unresolved/yes/no states; confirmed `yes` requires dependent details; changing away from `yes` invalidates stale dependent details; total readiness and warning navigation include these findings. These rules establish authoring completeness only and introduce no new legal conclusion, heuristic weighting, or synthetic production facts. ## Current baseline | Area | Evidence | Status | Commercialization gap | Owner/action | Next verification | | --- | --- | --- | --- | --- | --- | -| Guided authoring | PRD, ADR-0002, seven routed editors; zero-inferred startup facts; service URL projection; step-2 collection-path/mode and step-3 purpose separation; stale dependent-fact invalidation | Repaired foundation | Service identity, retention, transfers, contact, and collection-path evidence remain thin rather than requirement-complete typed contracts | Policy Fact Authoring: add typed facts and deterministic validations from authoritative requirements | Unit/integration tests for valid, missing, contradictory and not-applicable cases | -| Collection fact authority | `src/policy.ts`, `src/policy.test.ts`, `src/App.test.tsx`; test-first `37c5c490` -> production `b12052b0` | Repaired | No explicit first-class attestation yet for a service that truly collects no personal data; empty selection intentionally remains unresolved | Policy Fact Authoring: model evidence-backed no-collection/not-applicable state only if product requirements justify it | Contract tests that distinguish unresolved from explicit not-applicable | -| Processing-purpose review | Whitespace normalization plus selection/mode/purpose blocking in `getReview` and preview | Repaired foundation | Rule set still covers only a subset of authoring completeness and no versioned legal-rule snapshots | Review & Publication: extend evidence-bound rules without heuristic scoring | Exact-head tests plus rule/source traceability | -| Review workspace | Live preview, warning-to-source navigation, responsive fixes, buyer-facing readiness guidance; no-op/unshipped affordances removed or disabled | Implemented foundation | No browser screenshot matrix or realistic interaction evidence | UX: add Playwright + accessibility harness and verify remaining action edges | Desktop/tablet/mobile screenshots, keyboard/focus and action-edge verification | -| Accessibility | Semantic controls, focus-visible behavior, keyboard-operable step navigation | Partial | No automated WCAG 2.2 evidence or screen-reader contract | UX: add axe/Playwright and manual interaction record | WCAG audit and browser matrix | -| Policy model | ADR-0001/0002, ARCHITECTURE, TRD | Designed, browser-memory only | No versioned durable schema, migration, audit history, UPSERT/idempotency or 3NF implementation | Platform: define `policy_revision` schema and item-level conflict contracts before storage | Schema/contract tests and migration round trip | -| Publication | CTA truthfully exposes readiness rather than pretending to publish; architecture defines immutable `publication_revision` | Planned | No authenticated approval, immutable publication, supersession or public URL lifecycle | Review & Publication: implement only after persistence/security entry criteria | Authorization, replay/digest, supersession and rollback tests | -| Legal/rule traceability | `docs/research-traceability.md`; official PIPA/PIPC source register | Source baseline established | No article/section-level requirement mapping or versioned rule engine | Legal Source Registry: map each implemented legal rule to source/effective date/revision | Fixtures across current and future-effective snapshots | -| Security/privacy | `docs/SECURITY.md`; local-first runtime | Baseline documented | Hosted tenant model, secrets boundary, encryption/key handling, audit and incident/retention evidence absent | Platform/Security: threat-model hosted boundary before backend | Security tests and exact-head org scans | -| Tests | Vitest routing/navigation/readiness regressions, zero-inferred-facts and stale-mode tests, responsive CSS regression | Improved | Browser E2E, accessibility and broader domain edge cases remain incomplete; 100% execution coverage not yet evidenced | Test Engineering: expand behavior-first tests and measure touched coverage | Exact-head coverage and browser evidence | -| Documentation fitness | Production functions touched by the fact-authority repair now carry JSDoc; PRD/TRD/ADR/architecture/changelog/baseline align on the contract | Improved | Repository-wide 100% docstring coverage has not yet been independently measured | Test Engineering: add a deterministic documentation-coverage check if compatible with TypeScript tooling | Exact-head documented-function inventory | -| Performance | Static Vite client | Unevidenced | No page performance or network/load evidence | Operability: baseline browser performance; add k6 only with backend | Realistic measurements before latency claims | -| CI/security merge gate | SHA-pinned checkout plus repo and central required workflows | External live gate | Dependency Review/review providers can fail closed independently of product code | Control plane: repair incidents centrally; never substitute predecessor evidence | Exact-head assigned jobs must execute; independent approval and threads required | +| Guided authoring | PRD, ADR-0002, seven routed editors, `getDraftReview`, explicit transfer statuses | Repaired foundation | Product-defined seven-step completeness now fails closed, but collection-path evidence and field syntax/semantic validity remain thin | Policy Fact Authoring: strengthen typed field contracts without conflating product completeness with legal sufficiency | Exact-head unit/UI edge tests, then browser E2E | +| Customer-fact authority | Zero-inferred startup facts; explicit collection/transfer states; stale dependent-fact invalidation | Repaired | No first-class evidence-backed state yet for a service that explicitly collects no personal data; empty collection selection intentionally remains unresolved | Policy Fact Authoring: add explicit no-collection attestation only when product/source requirements justify it | Contract tests distinguishing unresolved from explicit none | +| Review workspace | Live preview, total blocker count, warning-to-owner navigation, buyer-facing readiness guidance | Implemented foundation | No browser screenshot matrix or realistic interaction evidence | UX: add Playwright + automated accessibility harness and verify action edges | Desktop/tablet/mobile screenshots, keyboard/focus and accessibility checks | +| Accessibility | Semantic controls and focus-visible behavior | Partial | No automated WCAG 2.2 evidence or screen-reader contract | UX/Test Engineering: add axe/Playwright and manual interaction record | Exact-head WCAG audit and browser matrix | +| Legal/rule traceability | `docs/research-traceability.md`; source/effective-date model | Source baseline established | Current readiness rules are product completeness only; no versioned article/section-level legal-rule engine | Legal Source Registry: map future legal validations to source/effective date/revision | Snapshot fixtures across effective dates | +| Policy model | ADRs, ARCHITECTURE, TRD | Designed, browser-memory only | No versioned durable schema, migrations, audit history, 3NF implementation, or explicit UPSERT/idempotency | Platform: define `policy_revision` schema and item-level conflict contracts before storage | Schema tests and migration round trip | +| 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 | Test-first startup authority and seven-step completeness regressions plus earlier routing/responsive/SAST regressions | Improved | Browser E2E/accessibility and repository-wide 100% execution/docstring coverage are not yet evidenced | Test Engineering: measure coverage and add browser-level behavior tests | Exact-head coverage + browser 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 | Live external gate | Every branch movement invalidates predecessor evidence and stale approval; dependency/reviewer control-plane failures can fail closed independently | Re-fetch exact-head workflows/reviews; repair causal owner if failure is real | Terminal exact-head checks + independent approval + resolved threads | ## DDD/context map Core subdomain: **Policy Fact Authoring**. Supporting: **Review & Publication**, **Legal Source Registry**. Generic infrastructure remains outside domain authority. `Policy Fact Authoring -> Review & Publication` supplies a versioned fact contract. `Legal Source Registry -> Review & Publication` supplies versioned source/rule evidence through an ACL. Customer/source-system integrations remain adapters and may not mutate foreign systems through hidden coupling. @@ -47,12 +38,12 @@ Ubiquitous language: `policy_revision`, `service_profile`, `collection_item`, `p The future aggregate root is the minimal `policy_revision` boundary. Rendered prose is a read projection. `publication_revision` is immutable and explicitly superseded rather than updated. Relational persistence is 3NF by default; named persistence objects use at least two semantic words and `snake_case`, and item-level UPSERT/idempotency semantics must be explicit. ## Buyer-visible release gates -A publishable release requires complete fact-to-warning navigation, deterministic rendering of captured source facts, no inferred customer operational facts, explicit unresolved-review blocking, immutable reviewed publication revisions, accessible responsive interaction, authoritative source/version traceability, hosted security/privacy evidence, and exact-head CI/security/independent review. Buyer-facing wording describes assistance and reviewability, never a compliance guarantee or an internal implementation boundary. +A publishable release requires complete fact-to-warning navigation, deterministic rendering of captured source facts, no inferred customer operational facts, explicit unresolved-review blocking across the seven authoring responsibilities, immutable reviewed publication revisions, accessible responsive interaction, authoritative source/version traceability for legal rules, hosted security/privacy evidence, and exact-head CI/security/independent review. Buyer-facing wording describes assistance and reviewability, never a compliance guarantee or an internal implementation boundary. ## Active commercialization order 1. Reacquire exact-current-head CI/security/SAST/central workflow execution and independent approval; merge PR #1 only through ordinary protection. -2. Add browser-level responsive/accessibility evidence for the repaired seven-step workflow and verify all remaining interaction edges. -3. Convert service identity, retention, transfer, contact, and collection-path placeholders into typed domain facts and source-backed deterministic validations. -4. Define and test the versioned `policy_revision` persistence/audit schema and explicit item-level UPSERT contracts. +2. Add browser-level responsive/accessibility evidence for the now fail-closed seven-step workflow and verify remaining interaction edges. +3. Strengthen typed validation for service URL/email and collection-path evidence without inventing legal rules. +4. Define and test versioned `policy_revision` 3NF persistence/audit and explicit item-level UPSERT contracts. 5. Implement secure review/publication with immutable releases and explicit supersession. -6. Add hosted tenant isolation, audit/incident evidence, compose deployment, and realistic performance/load tests when network services exist. \ No newline at end of file +6. Add hosted tenant isolation, audit/incident evidence, compose deployment, and realistic performance/load tests when network services exist. From 61220e847158c107df60b5c144ce5906e0297279 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 13:10:14 +0900 Subject: [PATCH 078/282] test: reject malformed policy contact facts --- src/policy.test.ts | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/policy.test.ts b/src/policy.test.ts index 5dfc9bc..25239e5 100644 --- a/src/policy.test.ts +++ b/src/policy.test.ts @@ -102,4 +102,33 @@ describe('seven-step draft readiness', () => { 'privacy_contact_email', ]) }) + + it('blocks malformed or non-web service URLs after presence is established', () => { + const base = { + ...initialFacts, + serviceName: '예시 서비스', + retentionPeriod: '회원 탈퇴 시까지', + thirdPartyStatus: 'no' as const, + internationalStatus: 'no' as const, + privacyOfficerName: '개인정보보호 담당', + privacyOfficerEmail: 'privacy@example.test', + } + expect(getDraftReview({ ...base, serviceUrl: 'not a url' }).map((finding) => finding.code)).toEqual(['service_url_format']) + expect(getDraftReview({ ...base, serviceUrl: 'javascript:alert(1)' }).map((finding) => finding.code)).toEqual(['service_url_format']) + expect(getDraftReview({ ...base, serviceUrl: 'https://example.test/path' })).toEqual([]) + }) + + it('blocks malformed privacy contact email after presence is established', () => { + const base = { + ...initialFacts, + serviceName: '예시 서비스', + serviceUrl: 'https://example.test', + retentionPeriod: '회원 탈퇴 시까지', + thirdPartyStatus: 'no' as const, + internationalStatus: 'no' as const, + privacyOfficerName: '개인정보보호 담당', + } + expect(getDraftReview({ ...base, privacyOfficerEmail: 'not-an-email' }).map((finding) => finding.code)).toEqual(['privacy_contact_email_format']) + expect(getDraftReview({ ...base, privacyOfficerEmail: 'privacy@example.test' })).toEqual([]) + }) }) \ No newline at end of file From bcabe96c42b981627a09496afb24c3e545c9e4b4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 13:11:26 +0900 Subject: [PATCH 079/282] fix: validate policy contact fact syntax --- src/policy.ts | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/policy.ts b/src/policy.ts index 0fbc903..325b0ff 100644 --- a/src/policy.ts +++ b/src/policy.ts @@ -59,6 +59,21 @@ 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. */ +function isWebServiceUrl(value: string) { + try { + const url = new URL(value) + return (url.protocol === 'https:' || url.protocol === 'http:') && Boolean(url.hostname) + } catch { + return false + } +} + +/** 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) +} + /** Derives deterministic readiness findings from operator-confirmed collection facts. */ export function getReview(items: PolicyItem[]) { const enabled = items.filter((item) => item.enabled) @@ -78,7 +93,10 @@ export function getDraftReview(facts: DraftFacts): DraftFinding[] { } addWhenBlank(facts.serviceName, 'service_name', 1, '서비스 이름') - addWhenBlank(facts.serviceUrl, 'service_url', 1, '서비스 URL') + const serviceUrl = facts.serviceUrl.trim() + if (!serviceUrl) findings.push({ code: 'service_url', step: 1, label: '서비스 URL' }) + else if (!isWebServiceUrl(serviceUrl)) findings.push({ code: 'service_url_format', step: 1, label: '서비스 URL 형식' }) + addWhenBlank(facts.retentionPeriod, 'retention_period', 4, '보유 기간') if (!facts.thirdPartyStatus) { @@ -96,7 +114,9 @@ export function getDraftReview(facts: DraftFacts): DraftFinding[] { } addWhenBlank(facts.privacyOfficerName, 'privacy_contact_name', 7, '개인정보 보호 담당자') - addWhenBlank(facts.privacyOfficerEmail, 'privacy_contact_email', 7, '개인정보 보호 연락 이메일') + const privacyOfficerEmail = facts.privacyOfficerEmail.trim() + if (!privacyOfficerEmail) findings.push({ code: 'privacy_contact_email', step: 7, label: '개인정보 보호 연락 이메일' }) + else if (!isContactEmail(privacyOfficerEmail)) findings.push({ code: 'privacy_contact_email_format', step: 7, label: '개인정보 보호 연락 이메일 형식' }) return findings } \ No newline at end of file From 0b2c8a5b13851fa20c61d17e3758892f67858c10 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 13:21:37 +0900 Subject: [PATCH 080/282] test: require collection path evidence --- src/policy.test.ts | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/policy.test.ts b/src/policy.test.ts index 25239e5..9cbc3b6 100644 --- a/src/policy.test.ts +++ b/src/policy.test.ts @@ -11,28 +11,40 @@ describe('collection policy review', () => { expect(review.blockingCount).toBe(1) }) - it('blocks readiness when a selected item still lacks collection mode and processing purpose', () => { + it('blocks readiness when a selected item still lacks collection mode, processing purpose, and collection path evidence', () => { const items = initialItems.map((item) => item.id === 'phone' ? { ...item, enabled: true } : item) const review = getReview(items) expect(review.selectionMissing).toBe(false) expect(review.modeBlocking.map((item) => item.id)).toEqual(['phone']) expect(review.blocking.map((item) => item.id)).toEqual(['phone']) - expect(review.blockingCount).toBe(2) + expect(review.pathBlocking.map((item) => item.id)).toEqual(['phone']) + expect(review.blockingCount).toBe(3) }) - it('treats a whitespace-only purpose as missing after collection mode is confirmed', () => { - const items = initialItems.map((item) => item.id === 'phone' ? { ...item, enabled: true, mode: '필수' as const, purpose: ' \t ' } : item) + it('treats a whitespace-only collection path as missing evidence', () => { + const items = initialItems.map((item) => item.id === 'phone' ? { ...item, enabled: true, mode: '필수' as const, purpose: '본인 확인', detail: ' \t ' } : item) const review = getReview(items) expect(review.modeBlocking).toEqual([]) + expect(review.blocking).toEqual([]) + expect(review.pathBlocking.map((item) => item.id)).toEqual(['phone']) + expect(review.blockingCount).toBe(1) + }) + + it('treats a whitespace-only purpose as missing after collection mode and path are confirmed', () => { + const items = initialItems.map((item) => item.id === 'phone' ? { ...item, enabled: true, mode: '필수' as const, purpose: ' \t ', detail: '회원가입 화면' } : item) + const review = getReview(items) + expect(review.modeBlocking).toEqual([]) + expect(review.pathBlocking).toEqual([]) expect(review.blocking.map((item) => item.id)).toEqual(['phone']) expect(review.blockingCount).toBe(1) }) - it('clears readiness findings once both collection mode and purpose are explicit', () => { - const items = initialItems.map((item) => item.id === 'phone' ? { ...item, enabled: true, mode: '필수' as const, purpose: ' 본인 확인 ' } : item) + it('clears readiness findings once collection mode, purpose, and path evidence are explicit', () => { + const items = initialItems.map((item) => item.id === 'phone' ? { ...item, enabled: true, mode: '필수' as const, purpose: ' 본인 확인 ', detail: ' 회원가입 화면 ' } : item) const review = getReview(items) expect(review.blocking.map((item) => item.id)).not.toContain('phone') expect(review.modeBlocking.map((item) => item.id)).not.toContain('phone') + expect(review.pathBlocking.map((item) => item.id)).not.toContain('phone') expect(review.blockingCount).toBe(0) }) }) From 7b843ef8fdc049e7f24513557cd86c7c4bbbcca6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 13:37:12 +0900 Subject: [PATCH 081/282] feat: require collection path evidence for readiness --- src/App.test.tsx | 17 ++++++++++++++++- src/App.tsx | 1 + src/policy.ts | 5 +++-- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/App.test.tsx b/src/App.test.tsx index 8c8c29c..b901320 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -102,7 +102,7 @@ describe('policy editing workflow', () => { const reenabledPhone = container.querySelectorAll('.item-list .item')[2] expect(reenabledPhone.querySelector('input[placeholder="예: 회원가입 화면"]')?.value).toBe('') expect(reenabledPhone.querySelector('select')?.value).toBe('') - expect(container.querySelector('.review-stat.blocking b')?.textContent).toBe('9건') + expect(container.querySelector('.review-stat.blocking b')?.textContent).toBe('10건') fireEvent.click(container.querySelectorAll('.rail li button')[2]) expect(container.querySelector('input[name="purpose-phone"]')?.value).toBe('') @@ -114,6 +114,7 @@ describe('policy editing workflow', () => { fireEvent.click(nameCheckbox) const nameItem = container.querySelectorAll('.item-list .item')[0] fireEvent.change(nameItem.querySelector('select')!, { target: { value: '필수' } }) + fireEvent.change(nameItem.querySelector('input[placeholder="예: 회원가입 화면"]')!, { target: { value: '회원가입 화면' } }) fireEvent.click(container.querySelectorAll('.rail li button')[2]) const purpose = container.querySelector('input[name="purpose-name"]')! @@ -152,6 +153,19 @@ describe('policy editing workflow', () => { expect(container.querySelector('.form-panel h1')?.textContent).toBe('2. 수집 항목') }) + it('수집 경로 미확인 경고에서 수집 항목 단계로 이동한다', () => { + const { container } = render() + fireEvent.click(container.querySelectorAll('.check-label input')[2]) + const phoneItem = container.querySelectorAll('.item-list .item')[2] + fireEvent.change(phoneItem.querySelector('select')!, { target: { value: '필수' } }) + fireEvent.click(container.querySelectorAll('.rail li button')[2]) + fireEvent.change(container.querySelector('input[name="purpose-phone"]')!, { target: { value: '본인 확인' } }) + fireEvent.click(container.querySelectorAll('.rail li button')[0]) + const warningButton = Array.from(container.querySelectorAll('.document-warning button')).find((button) => button.textContent?.includes('수집 경로'))! + fireEvent.click(warningButton) + expect(container.querySelector('.form-panel h1')?.textContent).toBe('2. 수집 항목') + }) + it('제3자 제공과 국외 이전은 확인 전 상태를 별도로 표현하고 없음 확인 시 종속 사실을 요구하지 않는다', () => { const { container } = render() const buttons = container.querySelectorAll('.rail li button') @@ -190,6 +204,7 @@ describe('policy editing workflow', () => { fireEvent.click(phone) const phoneItem = container.querySelectorAll('.item-list .item')[2] fireEvent.change(phoneItem.querySelector('select')!, { target: { value: '필수' } }) + fireEvent.change(phoneItem.querySelector('input[placeholder="예: 회원가입 화면"]')!, { target: { value: '회원가입 화면' } }) fireEvent.click(container.querySelectorAll('.rail li button')[2]) fireEvent.change(container.querySelector('input[name="purpose-phone"]')!, { target: { value: '본인 확인 및 알림 발송' } }) diff --git a/src/App.tsx b/src/App.tsx index 18563b5..928db5d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -167,6 +167,7 @@ function DocumentPreview({ items, facts, setCurrent }: { items: PolicyItem[]; fa })} {review.selectionMissing &&
공개 전 확인실제 수집 항목이 아직 확인되지 않았습니다.
} {review.modeBlocking.length > 0 &&
공개 전 확인{review.modeBlocking.map((item) => item.label).join(', ')}의 수집 구분을 확인해야 합니다.
} + {review.pathBlocking.length > 0 &&
공개 전 확인{review.pathBlocking.map((item) => item.label).join(', ')}의 수집 경로를 확인해야 합니다.
} {review.blocking.length > 0 &&
공개 전 확인{review.blocking.map((item) => item.label).join(', ')}의 처리 목적이 입력되지 않았습니다.
} {draftFindings.map((finding) =>
공개 전 확인{finding.label} 확인이 필요합니다.
)}

제2조 (처리 및 보유 기간)

{facts.retentionPeriod || '보유 기간 단계에서 확인한 운영 기준을 입력해야 합니다.'}

diff --git a/src/policy.ts b/src/policy.ts index 325b0ff..13095dc 100644 --- a/src/policy.ts +++ b/src/policy.ts @@ -79,10 +79,11 @@ export function getReview(items: PolicyItem[]) { const enabled = items.filter((item) => item.enabled) const blocking = enabled.filter((item) => !item.purpose.trim()) const modeBlocking = enabled.filter((item) => !item.mode) + const pathBlocking = enabled.filter((item) => !item.detail?.trim()) const selectionMissing = enabled.length === 0 - const blockingCount = blocking.length + modeBlocking.length + (selectionMissing ? 1 : 0) + const blockingCount = blocking.length + modeBlocking.length + pathBlocking.length + (selectionMissing ? 1 : 0) const recommended = enabled.filter((item) => item.mode === '선택' && item.id !== 'usage') - return { enabled, blocking, modeBlocking, selectionMissing, blockingCount, recommended } + return { enabled, blocking, modeBlocking, pathBlocking, selectionMissing, blockingCount, recommended } } /** Derives authoring-completeness findings for the non-collection responsibilities in the seven-step workflow. */ From aa3be87a37fa78d55627c9e65464e5cda8fbff60 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 13:41:56 +0900 Subject: [PATCH 082/282] docs: reconcile collection path readiness contract --- ARCHITECTURE.md | 6 +++--- CHANGELOG.md | 6 ++++-- docs/PRD.md | 5 +++-- docs/TRD.md | 6 +++--- docs/product-technical-gap-baseline.md | 13 +++++++------ 5 files changed, 20 insertions(+), 16 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 1e8ecbd..bdf349b 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -19,8 +19,8 @@ A future `policy_revision` is the minimal aggregate root and transaction boundar Core invariants: 1. A fresh workspace contains no inferred customer operational facts; blank means unresolved rather than `none`. -2. Selecting a collection item without confirming its collection mode or processing purpose creates blocking review findings. -3. Service identity, retention, third-party provision status, international-transfer status, and privacy contact remain blocking authoring responsibilities until explicitly established. +2. Selecting a collection item without confirming its collection mode, processing purpose, or collection-path evidence creates independent blocking review findings. +3. Service identity, retention, third-party provision status, international-transfer status, and privacy contact remain blocking authoring responsibilities until explicitly established; service URL and contact-email syntax checks prove only usable field shape, not endpoint/mailbox ownership. 4. Third-party provision and international transfer distinguish unresolved, yes, and no. A `yes` status requires dependent facts; a transition away from yes invalidates dependent details so stale facts cannot revive silently. 5. Disabling a collection item invalidates its dependent collection-mode, processing-purpose, and collection-path evidence; re-enabling requires renewed confirmation. 6. Review findings navigate to the fact that caused them. @@ -29,7 +29,7 @@ Core invariants: 9. 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 selection/mode/purpose and non-collection authoring-completeness findings; `src/App.tsx` owns browser orchestration, explicit transfer-status capture, warning-to-source navigation, stale dependent-fact invalidation, and deterministic preview rendering. +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 selection/mode/purpose/path and non-collection authoring-completeness findings; `src/App.tsx` owns browser orchestration, explicit transfer-status capture, warning-to-source navigation, stale dependent-fact invalidation, and deterministic preview rendering. 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 bd545e6..1e3f974 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,14 +6,16 @@ All notable product changes are recorded here. PolicyWeave is pre-release; entri ### Added - Seven-step authoring workflow with distinct editing surfaces for service information, collection items, processing purposes, retention, third-party transfer, international transfer, and privacy contact. -- Warning-to-source navigation for missing collection selection, collection mode, processing purposes, service identity, retention, transfer statuses/details, and privacy contact. +- Warning-to-source navigation for missing collection selection, collection mode, collection-path evidence, processing purposes, service identity, retention, transfer statuses/details, and privacy contact. - 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, collection-mode 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, and non-deceptive handling of unshipped affordances. +- Regression coverage for all seven step routes, zero-inferred startup facts, 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, and non-deceptive handling of unshipped affordances. - Product/technical gap ledger, architecture, technical requirements, security baseline, and legal-source traceability. ### Changed - Fresh workspaces no longer preselect collection items or prefill collection modes/processing purposes; readiness fails closed until the operator establishes actual collection facts. +- Every selected collection item now requires explicit collection mode, processing purpose, and nonblank collection-path evidence before readiness can pass; collection-path findings navigate back to the collection step. - Public-readiness now includes product-defined service name/URL, retention, transfer-status/detail, and privacy-contact completeness rather than allowing the CTA to enable while those steps remain visibly unresolved. +- Service URL and privacy-contact email are shape-validated as usability contracts without claiming endpoint reachability or mailbox ownership. - 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/PRD.md b/docs/PRD.md index ff5cc90..0a1d100 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -6,7 +6,7 @@ ## 제품 원칙 -PolicyWeave는 법률 문장을 임의로 창작하는 도구가 아니다. 운영자가 입력한 사실을 구조화하고, 적용 근거와 확인이 필요한 부분을 분리해 검토본을 만든다. 자동 생성 결과는 변호사·개인정보보호책임자의 검토를 대체하지 않는다. 새 작업공간은 고객의 실제 수집 여부, 수집 구분, 처리 목적, 제3자 제공 여부 또는 국외 이전 여부를 추정하지 않는다. 비어 있는 값은 `없음`이 아니라 `미확인`이다. +PolicyWeave는 법률 문장을 임의로 창작하는 도구가 아니다. 운영자가 입력한 사실을 구조화하고, 적용 근거와 확인이 필요한 부분을 분리해 검토본을 만든다. 자동 생성 결과는 변호사·개인정보보호책임자의 검토를 대체하지 않는다. 새 작업공간은 고객의 실제 수집 여부, 수집 구분, 처리 목적, 수집 경로, 제3자 제공 여부 또는 국외 이전 여부를 추정하지 않는다. 비어 있는 값은 `없음`이 아니라 `미확인`이다. ## MVP @@ -30,7 +30,8 @@ PolicyWeave는 법률 문장을 임의로 창작하는 도구가 아니다. 운 - 사용자는 일곱 작성 책임의 제품 정의 필수 사실을 누락 없이 검토할 수 있다. - 새 작업공간은 고객 운영 사실을 미리 채우지 않는다. -- 실제 수집 항목과 각 항목의 수집 구분·처리 목적, 서비스 이름·URL, 보유 기간, 제3자 제공 여부, 국외 이전 여부, 개인정보 보호 담당자·연락 이메일이 확인되기 전에는 공개 준비 상태를 통과하지 않는다. +- 실제 수집 항목과 각 항목의 수집 구분·처리 목적·수집 경로, 서비스 이름·URL, 보유 기간, 제3자 제공 여부, 국외 이전 여부, 개인정보 보호 담당자·연락 이메일이 확인되기 전에는 공개 준비 상태를 통과하지 않는다. +- 서비스 URL은 절대 HTTP(S) 주소 형식을 만족해야 하고, 개인정보 보호 연락 이메일은 최소 주소 형식 검사를 통과해야 한다. 이 검사는 서비스 존재나 메일함 소유권을 주장하지 않는다. - 제3자 제공 또는 국외 이전을 `있음`으로 확인한 경우 해당 수령자/목적 또는 국가/수령자 사실까지 확인되어야 한다. - `없음` 확인은 명시적 운영자 사실이며, 이전에 입력한 종속 제공·이전 상세 사실은 상태 변경 시 폐기된다. - 모든 경고는 해당 입력 단계로 이동할 수 있다. diff --git a/docs/TRD.md b/docs/TRD.md index ccb843a..a929345 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. -- `src/policy.ts` owns deterministic review logic for collection selection/mode/purpose and the non-collection authoring-completeness findings for service identity, retention, transfer statuses/details, and privacy contact. +- `src/policy.ts` owns deterministic review logic for collection selection/mode/purpose/path and the non-collection authoring-completeness findings for service identity, retention, transfer statuses/details, and privacy contact. - `src/App.tsx` provides the seven-step authoring flow, review navigation, conditional transfer editors, and deterministic preview projection. - The current CI contract is lint, Vitest, and TypeScript/Vite build plus live organization-required security/review workflows. @@ -14,8 +14,8 @@ This TRD covers the pre-release PolicyWeave browser workspace and the contracts 1. Every PRD step must route to an editable surface; selecting a step cannot change only the rail indicator. 2. A fresh workspace contains no inferred customer operational facts. Blank authoring values mean unresolved, not `none`. 3. Until at least one actual collection item is selected, readiness produces a blocking selection-not-established finding that navigates to the collection step. -4. Every selected collection item requires an explicit collection mode and nonblank processing purpose. -5. Service name, service URL, retention period, third-party provision status, international-transfer status, privacy-contact owner, and privacy-contact email are product-defined readiness facts and block readiness while unresolved. +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. Service name, service URL, retention period, 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. 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. 7. Blank/whitespace authoring facts are normalized as unresolved. 8. 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 b9f1639..ef7d8ad 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -14,11 +14,13 @@ A 2026-09-02 exact-head commercialization pass found that `initialItems` presele The next exact-head pass found a second buyer-facing authority defect: after completing one selected collection item, the readiness CTA could enable while service identity, retention, third-party provision, international transfer, and privacy contact remained visibly unresolved in the review draft. Blank transfer fields also conflated `not checked` with `none`. Regression/edge-case tests were committed first at `1e77d4424f4b028e56f0453afe17f78dd5cebeca`. Production was repaired at `cbe3a743bd8ba09b3c63d4b79507975a6a77547a`: `getDraftReview` now derives product-defined findings for all non-collection authoring responsibilities; third-party provision and international transfer use explicit unresolved/yes/no states; confirmed `yes` requires dependent details; changing away from `yes` invalidates stale dependent details; total readiness and warning navigation include these findings. These rules establish authoring completeness only and introduce no new legal conclusion, heuristic weighting, or synthetic production facts. +The current pass found that collection-path evidence was editable and stale-path invalidation already existed, but readiness could still pass a selected item whose collection path was blank. A concurrent writer established the failing contract first at `0b2c8a5b13851fa20c61d17e3758892f67858c10`. Production was repaired at `7b843ef8fdc049e7f24513557cd86c7c4bbbcca6`: `getReview` now derives independent whitespace-normalized path blockers, the buyer-facing preview explains which item still needs collection-path evidence and navigates to the owning collection step, and UI regressions were reconciled so the readiness count remains internally consistent. Existing service-URL and privacy-contact-email shape validation was also revalidated on that exact implementation rather than left as a stale planned gap. + ## Current baseline | Area | Evidence | Status | Commercialization gap | Owner/action | Next verification | | --- | --- | --- | --- | --- | --- | -| Guided authoring | PRD, ADR-0002, seven routed editors, `getDraftReview`, explicit transfer statuses | Repaired foundation | Product-defined seven-step completeness now fails closed, but collection-path evidence and field syntax/semantic validity remain thin | Policy Fact Authoring: strengthen typed field contracts without conflating product completeness with legal sufficiency | Exact-head unit/UI edge tests, then browser E2E | +| Guided authoring | PRD, ADR-0002, seven routed editors, `getReview`, `getDraftReview`, explicit transfer statuses | Repaired foundation | Selection/mode/purpose/path and non-collection authoring responsibilities fail closed; URL/email shape checks exist, but collection-path evidence remains unstructured free text and legal sufficiency is deliberately separate | Policy Fact Authoring: preserve deterministic completeness; only add structured path-evidence types when a real integration/use case proves the need | Exact-head unit/UI edge tests, then browser E2E | | Customer-fact authority | Zero-inferred startup facts; explicit collection/transfer states; stale dependent-fact invalidation | Repaired | No first-class evidence-backed state yet for a service that explicitly collects no personal data; empty collection selection intentionally remains unresolved | Policy Fact Authoring: add explicit no-collection attestation only when product/source requirements justify it | Contract tests distinguishing unresolved from explicit none | | Review workspace | Live preview, total blocker count, warning-to-owner navigation, buyer-facing readiness guidance | Implemented foundation | No browser screenshot matrix or realistic interaction evidence | UX: add Playwright + automated accessibility harness and verify action edges | Desktop/tablet/mobile screenshots, keyboard/focus and accessibility checks | | Accessibility | Semantic controls and focus-visible behavior | Partial | No automated WCAG 2.2 evidence or screen-reader contract | UX/Test Engineering: add axe/Playwright and manual interaction record | Exact-head WCAG audit and browser matrix | @@ -26,7 +28,7 @@ The next exact-head pass found a second buyer-facing authority defect: after com | Policy model | ADRs, ARCHITECTURE, TRD | Designed, browser-memory only | No versioned durable schema, migrations, audit history, 3NF implementation, or explicit UPSERT/idempotency | Platform: define `policy_revision` schema and item-level conflict contracts before storage | Schema tests and migration round trip | | 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 | Test-first startup authority and seven-step completeness regressions plus earlier routing/responsive/SAST regressions | Improved | Browser E2E/accessibility and repository-wide 100% execution/docstring coverage are not yet evidenced | Test Engineering: measure coverage and add browser-level behavior tests | Exact-head coverage + browser evidence | +| Tests | Test-first startup authority, seven-step completeness, and collection-path regressions plus earlier routing/responsive/SAST regressions | Improved | Browser E2E/accessibility and repository-wide 100% execution/docstring coverage are not yet evidenced | Test Engineering: measure coverage and add browser-level behavior tests | Exact-head coverage + browser 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 | Live external gate | Every branch movement invalidates predecessor evidence and stale approval; dependency/reviewer control-plane failures can fail closed independently | Re-fetch exact-head workflows/reviews; repair causal owner if failure is real | Terminal exact-head checks + independent approval + resolved threads | @@ -43,7 +45,6 @@ A publishable release requires complete fact-to-warning navigation, deterministi ## Active commercialization order 1. Reacquire exact-current-head CI/security/SAST/central workflow execution and independent approval; merge PR #1 only through ordinary protection. 2. Add browser-level responsive/accessibility evidence for the now fail-closed seven-step workflow and verify remaining interaction edges. -3. Strengthen typed validation for service URL/email and collection-path evidence without inventing legal rules. -4. Define and test versioned `policy_revision` 3NF persistence/audit and explicit item-level UPSERT contracts. -5. Implement secure review/publication with immutable releases and explicit supersession. -6. Add hosted tenant isolation, audit/incident evidence, compose deployment, and realistic performance/load tests when network services exist. +3. Define and test versioned `policy_revision` 3NF persistence/audit and explicit item-level UPSERT contracts. +4. Implement secure review/publication with immutable releases and explicit supersession. +5. Add hosted tenant isolation, audit/incident evidence, compose deployment, and realistic performance/load tests when network services exist. From 97063fc34fbc0e6db2652922f78bc74c00e1ef93 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 13:45:01 +0900 Subject: [PATCH 083/282] test: specify explicit no-collection attestation --- src/no-collection.test.tsx | 53 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 src/no-collection.test.tsx diff --git a/src/no-collection.test.tsx b/src/no-collection.test.tsx new file mode 100644 index 0000000..138cee2 --- /dev/null +++ b/src/no-collection.test.tsx @@ -0,0 +1,53 @@ +// @vitest-environment jsdom +import { cleanup, fireEvent, render } from '@testing-library/react' +import { afterEach, describe, expect, it } from 'vitest' +import App from './App' +import { getReview, initialItems } from './policy' + +afterEach(cleanup) + +describe('explicit no-collection attestation', () => { + it('clears the unresolved selection blocker without inventing collection items', () => { + const review = getReview(initialItems, true) + expect(review.enabled).toEqual([]) + expect(review.selectionMissing).toBe(false) + expect(review.collectionContradiction).toBe(false) + expect(review.blockingCount).toBe(0) + }) + + it('fails closed when a no-collection attestation coexists with a selected item', () => { + const items = initialItems.map((item) => item.id === 'phone' ? { ...item, enabled: true, mode: '필수' as const, purpose: '본인 확인', detail: '회원가입 화면' } : item) + const review = getReview(items, true) + expect(review.collectionContradiction).toBe(true) + expect(review.blockingCount).toBe(1) + }) + + it('invalidates stale item facts when the operator attests no collection and does not revive them when the attestation is removed', () => { + const { container } = render() + const phone = container.querySelectorAll('.check-label input')[2] + fireEvent.click(phone) + const phoneItem = container.querySelectorAll('.item-list .item')[2] + fireEvent.change(phoneItem.querySelector('select')!, { target: { value: '필수' } }) + fireEvent.change(phoneItem.querySelector('input[placeholder="예: 회원가입 화면"]')!, { target: { value: '회원가입 화면' } }) + + fireEvent.click(container.querySelectorAll('.rail li button')[2]) + fireEvent.change(container.querySelector('input[name="purpose-phone"]')!, { target: { value: '본인 확인' } }) + fireEvent.click(container.querySelectorAll('.rail li button')[1]) + + const noCollection = container.querySelector('input[name="noCollectionAttested"]')! + expect(noCollection).not.toBeNull() + fireEvent.click(noCollection) + expect(container.querySelectorAll('.check-label input')[2].checked).toBe(false) + expect(container.querySelector('.review-stat.blocking b')?.textContent).toBe('7건') + + fireEvent.click(noCollection) + expect(container.querySelector('.review-stat.blocking b')?.textContent).toBe('8건') + fireEvent.click(container.querySelectorAll('.check-label input')[2]) + const reenabledPhone = container.querySelectorAll('.item-list .item')[2] + expect(reenabledPhone.querySelector('select')?.value).toBe('') + expect(reenabledPhone.querySelector('input[placeholder="예: 회원가입 화면"]')?.value).toBe('') + + fireEvent.click(container.querySelectorAll('.rail li button')[2]) + expect(container.querySelector('input[name="purpose-phone"]')?.value).toBe('') + }) +}) From e73eb83a913597fb05ddea97519d549d58007faf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 13:46:42 +0900 Subject: [PATCH 084/282] feat: support explicit no-collection attestation --- src/App.tsx | 48 ++++++++++++++++++++++++++++++++---------------- src/policy.ts | 11 ++++++----- 2 files changed, 38 insertions(+), 21 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 928db5d..56a8ab2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -67,53 +67,67 @@ function FactStep({ current, title, description, fields, facts, setFacts, setCur } -/** Captures collection facts only after the operator explicitly selects an item. */ -function CollectionForm({ items, setItems, setCurrent }: { items: PolicyItem[]; setItems: (items: PolicyItem[]) => void; setCurrent: (step: number) => void }) { +/** Captures collection facts only after the operator explicitly selects an item or attests that none are collected. */ +function CollectionForm({ items, setItems, noCollectionAttested, setNoCollectionAttested, setCurrent }: { + items: PolicyItem[] + setItems: (items: PolicyItem[]) => void + noCollectionAttested: boolean + setNoCollectionAttested: (attested: boolean) => void + setCurrent: (step: number) => void +}) { const update = (id: string, patch: Partial) => setItems(items.map((item) => item.id === id ? { ...item, ...patch } : item)) + const setNoCollection = (attested: boolean) => { + if (attested) setItems(items.map((item) => ({ ...item, enabled: false, purpose: '', detail: '', mode: '' }))) + setNoCollectionAttested(attested) + } return

2. 수집 항목

서비스에서 실제로 수집하는 개인정보만 선택하세요. 선택한 항목에 따라 다음 단계가 달라집니다.

입력 원칙서비스 코드와 운영 절차에서 확인한 항목만 반영하세요. 추정으로 선택하지 않습니다.
+

수집 여부

+

기본 정보

수집 항목설명수집 여부
{items.map((item) =>
-
- {item.enabled &&
} + {item.enabled && !noCollectionAttested &&
}
)}
} /** Captures processing purposes for collection items explicitly selected by the operator. */ -function PurposeForm({ items, setItems, setCurrent }: { items: PolicyItem[]; setItems: (items: PolicyItem[]) => void; setCurrent: (step: number) => void }) { +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) const updatePurpose = (id: string, purpose: string) => setItems(items.map((item) => item.id === id ? { ...item, purpose } : item)) return

3. 처리 목적

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

검토 원칙포괄적인 문구를 새로 만들기보다 실제 기능·업무 목적과 연결하세요.

항목별 처리 목적

- {enabled.length === 0 ?

수집 항목 단계에서 실제 수집 항목을 먼저 선택하세요.

:
{enabled.map((item) =>
)}
} + {noCollectionAttested ?

개인정보를 수집하지 않음으로 확인되었습니다. 수집 항목을 추가하려면 수집 항목 단계에서 이 확인을 해제하세요.

: enabled.length === 0 ?

수집 항목 단계에서 실제 수집 항목을 먼저 선택하세요.

:
{enabled.map((item) =>
)}
}
} /** Chooses the editing surface that owns the active authoring step. */ -function EditingPanel({ current, items, setItems, facts, setFacts, setCurrent }: { +function EditingPanel({ current, items, setItems, noCollectionAttested, setNoCollectionAttested, facts, setFacts, setCurrent }: { current: number items: PolicyItem[] setItems: (items: PolicyItem[]) => void + noCollectionAttested: boolean + setNoCollectionAttested: (attested: boolean) => void facts: DraftFacts setFacts: (facts: DraftFacts) => void setCurrent: (step: number) => void }) { - if (current === 2) return - if (current === 3) return + if (current === 2) return + if (current === 3) return const yesNoOptions = [ { value: '', label: '확인 필요' }, @@ -148,8 +162,8 @@ function EditingPanel({ current, items, setItems, facts, setFacts, setCurrent }: } /** Projects verified authoring facts and deterministic readiness findings into the review draft. */ -function DocumentPreview({ items, facts, setCurrent }: { items: PolicyItem[]; facts: DraftFacts; setCurrent: (step: number) => void }) { - const review = useMemo(() => getReview(items), [items]) +function DocumentPreview({ items, noCollectionAttested, facts, setCurrent }: { items: PolicyItem[]; noCollectionAttested: boolean; facts: DraftFacts; setCurrent: (step: number) => void }) { + const review = useMemo(() => getReview(items, noCollectionAttested), [items, noCollectionAttested]) const draftFindings = useMemo(() => getDraftReview(facts), [facts]) const blockingCount = review.blockingCount + draftFindings.length return
@@ -160,12 +174,13 @@ function DocumentPreview({ items, facts, setCurrent }: { items: PolicyItem[]; fa {facts.serviceUrl &&

적용 서비스: {facts.serviceUrl}

}

{facts.serviceName || '서비스 운영자'}는 이용자의 개인정보를 중요하게 여기며, 확인된 실제 처리 사실을 바탕으로 다음 사항을 검토합니다.

제1조 (개인정보의 처리 목적)

-

아래 목적은 작성자가 확인한 운영 사실을 기준으로 표시됩니다.

+ {noCollectionAttested ?

운영자는 현재 서비스에서 개인정보를 수집하지 않음으로 확인했습니다.

:

아래 목적은 작성자가 확인한 운영 사실을 기준으로 표시됩니다.

} {review.enabled.map((item) => { const hasPurpose = item.purpose.trim().length > 0 return })}
수집 항목처리 목적검토 상태
{item.label}{hasPurpose ? item.purpose : '처리 목적 입력 필요'}{hasPurpose ? '입력됨' : '확인 필요'}
- {review.selectionMissing &&
공개 전 확인실제 수집 항목이 아직 확인되지 않았습니다.
} + {review.selectionMissing &&
공개 전 확인실제 수집 항목 또는 개인정보를 수집하지 않는다는 운영 사실이 아직 확인되지 않았습니다.
} + {review.collectionContradiction &&
공개 전 확인수집하지 않음 확인과 선택된 수집 항목이 동시에 존재합니다.
} {review.modeBlocking.length > 0 &&
공개 전 확인{review.modeBlocking.map((item) => item.label).join(', ')}의 수집 구분을 확인해야 합니다.
} {review.pathBlocking.length > 0 &&
공개 전 확인{review.pathBlocking.map((item) => item.label).join(', ')}의 수집 경로를 확인해야 합니다.
} {review.blocking.length > 0 &&
공개 전 확인{review.blocking.map((item) => item.label).join(', ')}의 처리 목적이 입력되지 않았습니다.
} @@ -182,16 +197,17 @@ function DocumentPreview({ items, facts, setCurrent }: { items: PolicyItem[]; fa /** Coordinates PolicyWeave browser-only authoring state and readiness feedback. */ export default function App() { const [items, setItems] = useState(initialItems) + const [noCollectionAttested, setNoCollectionAttested] = useState(false) const [facts, setFacts] = useState(initialFacts) const [current, setCurrent] = useState(2) - const collectionReview = useMemo(() => getReview(items), [items]) + const collectionReview = useMemo(() => getReview(items, noCollectionAttested), [items, noCollectionAttested]) const draftFindings = useMemo(() => getDraftReview(facts), [facts]) const blockingCount = collectionReview.blockingCount + draftFindings.length const [message, setMessage] = useState('') function publish() { setMessage(blockingCount ? '필수 확인 항목을 먼저 입력하세요.' : '필수 확인이 완료되었습니다. 현재 검토본을 책임자와 검토하고 필요한 사실을 보완하세요.') } return
PolicyWeave{facts.serviceName || '내 서비스'} 개인정보처리방침작성 중버전 0.1.0 (임시저장) 브라우저 작업 중
-
+
검토 요약확인을 마친 뒤 공개 준비 상태를 확인하세요.
필수 확인 {blockingCount}건
권장 검토 {collectionReview.recommended.length}건
{message}
} \ No newline at end of file diff --git a/src/policy.ts b/src/policy.ts index 13095dc..d143750 100644 --- a/src/policy.ts +++ b/src/policy.ts @@ -74,16 +74,17 @@ function isContactEmail(value: string) { return /^[^\s@]+@[^\s@]+$/.test(value) } -/** Derives deterministic readiness findings from operator-confirmed collection facts. */ -export function getReview(items: PolicyItem[]) { +/** Derives deterministic readiness findings from operator-confirmed collection facts and an explicit no-collection attestation. */ +export function getReview(items: PolicyItem[], noCollectionAttested = false) { const enabled = items.filter((item) => item.enabled) const blocking = enabled.filter((item) => !item.purpose.trim()) const modeBlocking = enabled.filter((item) => !item.mode) const pathBlocking = enabled.filter((item) => !item.detail?.trim()) - const selectionMissing = enabled.length === 0 - const blockingCount = blocking.length + modeBlocking.length + pathBlocking.length + (selectionMissing ? 1 : 0) + const selectionMissing = enabled.length === 0 && !noCollectionAttested + const collectionContradiction = noCollectionAttested && enabled.length > 0 + const blockingCount = blocking.length + modeBlocking.length + pathBlocking.length + (selectionMissing ? 1 : 0) + (collectionContradiction ? 1 : 0) const recommended = enabled.filter((item) => item.mode === '선택' && item.id !== 'usage') - return { enabled, blocking, modeBlocking, pathBlocking, selectionMissing, blockingCount, recommended } + return { enabled, blocking, modeBlocking, pathBlocking, selectionMissing, collectionContradiction, noCollectionAttested, blockingCount, recommended } } /** Derives authoring-completeness findings for the non-collection responsibilities in the seven-step workflow. */ From 6a071de4bd1cb55335032dc2464a9a404f692446 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 13:49:10 +0900 Subject: [PATCH 085/282] docs: model explicit no-collection authority --- ARCHITECTURE.md | 26 ++++++++++--------- CHANGELOG.md | 13 ++++++---- .../ADR-0002-seven-step-authoring-workflow.md | 10 +++++-- docs/PRD.md | 7 +++-- docs/TRD.md | 8 +++--- docs/product-technical-gap-baseline.md | 24 +++++++++-------- 6 files changed, 52 insertions(+), 36 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index bdf349b..9eaf9ef 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -7,7 +7,7 @@ PolicyWeave owns the authoring and review of structured privacy-processing facts The core subdomain is **Policy Fact Authoring**. Supporting subdomains are **Review & Publication** and **Legal Source Registry**. Browser/storage frameworks, authentication infrastructure, observability, and deployment are generic subdomains. ### Bounded contexts -- **Policy Fact Authoring**: captures `service_profile`, `collection_item`, `processing_purpose`, `retention_rule`, `third_party_transfer`, `international_transfer`, and `privacy_contact` facts. +- **Policy Fact Authoring**: captures `service_profile`, collection presence/no-collection attestation, `collection_item`, `processing_purpose`, `retention_rule`, `third_party_transfer`, `international_transfer`, and `privacy_contact` facts. - **Review & Publication**: derives `review_finding`, controls `policy_revision` review state, and will create immutable `publication_revision` artifacts only after explicit authorization. - **Legal Source Registry**: versions authoritative sources, effective dates, rule/template revisions, and citations. It is an anti-corruption layer between changing external law/guidance and already-published revisions. @@ -15,26 +15,28 @@ The core subdomain is **Policy Fact Authoring**. Supporting subdomains are **Rev `Policy Fact Authoring -> Review & Publication` is a customer/supplier relationship through a versioned policy-fact contract. `Legal Source Registry -> Review & Publication` supplies versioned rule evidence; source updates cannot silently mutate historical policy revisions. External customer systems remain behind adapters and must not be queried or mutated through hidden coupling. ## Ubiquitous language and model -A future `policy_revision` is the minimal aggregate root and transaction boundary. `service_profile`, `collection_item`, `processing_purpose`, `retention_rule`, `third_party_transfer`, `international_transfer`, and `privacy_contact` are revision-owned facts/value objects unless later evidence requires independent lifecycles. `review_finding` is derived evidence. `publication_revision` is an immutable release receipt, not a mutable policy row. Rendered prose is a projection and never the source of truth. +A future `policy_revision` is the minimal aggregate root and transaction boundary. `service_profile`, collection presence/no-collection attestation, `collection_item`, `processing_purpose`, `retention_rule`, `third_party_transfer`, `international_transfer`, and `privacy_contact` are revision-owned facts/value objects unless later evidence requires independent lifecycles. `review_finding` is derived evidence. `publication_revision` is an immutable release receipt, not a mutable policy row. Rendered prose is a projection and never the source of truth. Core invariants: 1. A fresh workspace contains no inferred customer operational facts; blank means unresolved rather than `none`. -2. Selecting a collection item without confirming its collection mode, processing purpose, or collection-path evidence creates independent blocking review findings. -3. Service identity, retention, third-party provision status, international-transfer status, and privacy contact remain blocking authoring responsibilities until explicitly established; service URL and contact-email syntax checks prove only usable field shape, not endpoint/mailbox ownership. -4. Third-party provision and international transfer distinguish unresolved, yes, and no. A `yes` status requires dependent facts; a transition away from yes invalidates dependent details so stale facts cannot revive silently. -5. Disabling a collection item invalidates its dependent collection-mode, processing-purpose, and collection-path evidence; re-enabling requires renewed confirmation. -6. Review findings navigate to the fact that caused them. -7. Publication must never upgrade an unreviewed or incomplete draft to a reviewed/authoritative state. -8. A published revision remains reproducible from its policy facts plus rule/template/source versions. -9. External legal-source updates produce explicit re-evaluation, not silent rewriting. +2. Collection presence is established by either at least one selected collection item or an explicit no-collection attestation. Empty selection alone remains unresolved. The no-collection attestation and selected items are mutually exclusive; contradictory state fails closed. +3. Turning on the no-collection attestation invalidates all selected-item collection mode, processing purpose, and collection-path evidence. Turning the attestation off cannot revive those stale facts. +4. Selecting a collection item without confirming its collection mode, processing purpose, or collection-path evidence creates independent blocking review findings. +5. Service identity, retention, third-party provision status, international-transfer status, and privacy contact remain blocking authoring responsibilities until explicitly established; service URL and contact-email syntax checks prove only usable field shape, not endpoint/mailbox ownership. +6. Third-party provision and international transfer distinguish unresolved, yes, and no. A `yes` status requires dependent facts; a transition away from yes invalidates dependent details so stale facts cannot revive silently. +7. Disabling a collection item invalidates its dependent collection-mode, processing-purpose, and collection-path evidence; re-enabling requires renewed confirmation. +8. Review findings navigate to the fact that caused them. +9. Publication must never upgrade an unreviewed or incomplete draft to a reviewed/authoritative state. +10. A published revision remains reproducible from its policy facts plus rule/template/source versions. +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 selection/mode/purpose/path and non-collection authoring-completeness findings; `src/App.tsx` owns browser orchestration, explicit transfer-status capture, warning-to-source navigation, stale dependent-fact invalidation, and deterministic preview rendering. +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. 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. ## Persistence boundary (planned, not implemented) -Use relational 3NF by default. Named database/schema/persistence objects use at least two semantic words and `snake_case`, for example `policy_revision`, `collection_item`, `processing_purpose`, `review_finding`, `publication_revision`, and `legal_source_revision`. Item-level UPSERTs must declare their natural/idempotency key and conflict behavior. Publication is append-only/immutable with explicit supersession; writes across unrelated aggregates must not share a transaction merely for convenience. +Use relational 3NF by default. Named database/schema/persistence objects use at least two semantic words and `snake_case`, for example `policy_revision`, `collection_item`, `processing_purpose`, `review_finding`, `publication_revision`, and `legal_source_revision`. The explicit no-collection state belongs to `policy_revision`/its owned service-processing facts rather than being represented by a fake `collection_item`. Item-level UPSERTs must declare their natural/idempotency key and conflict behavior. Publication is append-only/immutable with explicit supersession; writes across unrelated aggregates must not share a transaction merely for convenience. Separate write-side draft commands from read-side rendered/review projections once hosted traffic justifies it. Account for revision hot spots and optimistic/constrained writes before adding collaborative editing. Keep source/customer integrations behind ACLs; do not form a shared kernel with unrelated ContextualWisdomLab products without demonstrated reuse. diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e3f974..c33b7c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,15 +6,18 @@ All notable product changes are recorded here. PolicyWeave is pre-release; entri ### Added - Seven-step authoring workflow with distinct editing surfaces for service information, collection items, processing purposes, retention, third-party transfer, international transfer, and privacy contact. -- Warning-to-source navigation for missing collection selection, collection mode, collection-path evidence, processing purposes, service identity, retention, transfer statuses/details, and privacy contact. +- Explicit `개인정보를 수집하지 않음` operator attestation so a genuine no-collection service can complete authoring without treating an empty item list as `none`. +- Warning-to-source navigation for missing collection selection/no-collection confirmation, collection mode, collection-path evidence, processing purposes, service identity, retention, transfer statuses/details, and privacy contact. - 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, 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, and non-deceptive handling of unshipped affordances. +- Regression coverage for all seven step routes, zero-inferred startup facts, explicit no-collection state and stale-item 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, and non-deceptive handling of unshipped affordances. - Product/technical gap ledger, architecture, technical requirements, security baseline, and legal-source traceability. ### Changed -- Fresh workspaces no longer preselect collection items or prefill collection modes/processing purposes; readiness fails closed until the operator establishes actual collection facts. -- Every selected collection item now requires explicit collection mode, processing purpose, and nonblank collection-path evidence before readiness can pass; collection-path findings navigate back to the collection step. -- Public-readiness now includes product-defined service name/URL, retention, transfer-status/detail, and privacy-contact completeness rather than allowing the CTA to enable while those steps remain visibly unresolved. +- Fresh workspaces no longer preselect collection items or prefill collection modes/processing purposes; readiness fails closed until the operator establishes actual collection facts or explicitly attests that the service collects no personal data. +- Empty collection selection remains unresolved rather than being interpreted as `none`; no-collection and selected-item states are mutually exclusive and contradictory state fails closed. +- Turning on no-collection confirmation clears selected-item mode, purpose, and path evidence; later removing the confirmation does not silently restore stale customer facts. +- Every selected collection item requires explicit collection mode, processing purpose, and nonblank collection-path evidence before readiness can pass; collection-path findings navigate back to the collection step. +- Public-readiness includes product-defined service name/URL, retention, transfer-status/detail, and privacy-contact completeness rather than allowing the CTA to enable while those steps remain visibly unresolved. - Service URL and privacy-contact email are shape-validated as usability contracts without claiming endpoint reachability or mailbox ownership. - 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. diff --git a/docs/ADR-0002-seven-step-authoring-workflow.md b/docs/ADR-0002-seven-step-authoring-workflow.md index 91350b3..8f765e9 100644 --- a/docs/ADR-0002-seven-step-authoring-workflow.md +++ b/docs/ADR-0002-seven-step-authoring-workflow.md @@ -10,10 +10,14 @@ A later commercialization pass found a second workflow-integrity defect: the fre A subsequent exact-head pass found that even after collection facts were repaired, the readiness CTA could enable while service identity, retention, transfer statuses, and privacy contact were still visibly unresolved in the review draft. Blank transfer fields were also ambiguous between `none` and `not yet checked`. +The next pass found a separate completeness hole: collection-path evidence was editable but not required by readiness. After that was repaired, the remaining collection-state model still made a service that genuinely collects no personal data impossible to complete because an empty item selection was always unresolved. Treating empty selection as `none` would reintroduce inference, so the workflow needs an explicit negative attestation. + ## Decision Each PRD stage is an explicit editable state of the Policy Fact Authoring context: service information, collection items, processing purposes, retention, third-party transfer, international transfer, and privacy contact. The UI router must render a corresponding editor for the selected stage. Review findings carry enough domain context to navigate to the responsible stage. -The collection catalog is metadata, not an assertion about a customer's service. A fresh workspace therefore starts with every collection item unselected and with collection mode and processing purpose unresolved. Selecting an item requires the operator to establish its collection mode and purpose before readiness can clear. An empty selection is treated as unresolved, not as an inferred assertion that the service collects no personal data. Disabling an item invalidates its mode, purpose, and collection-path evidence so re-enabling cannot silently restore stale facts. +The collection catalog is metadata, not an assertion about a customer's service. A fresh workspace starts with every collection item unselected and with collection mode, processing purpose, and collection path unresolved. Collection readiness is established by either selecting at least one actual collection item or explicitly attesting that the service collects no personal data. Empty selection alone remains unresolved. The no-collection attestation and selected items are mutually exclusive; contradictory state fails closed. Turning the attestation on clears selected items plus their mode, purpose, and path evidence, and later turning it off does not restore those stale facts. + +When collection items are present, each selected item requires the operator to establish collection mode, processing purpose, and collection-path evidence before readiness can clear. Disabling an item invalidates those dependent facts so re-enabling cannot silently restore stale evidence. Readiness also requires the product-defined facts owned by the other authoring stages: service name and URL, retention period, third-party provision status, international-transfer status, and privacy-contact owner/email. Third-party provision and international transfer use explicit unresolved/yes/no states. `no` is an operator attestation, not an inference from a blank field. `yes` requires its dependent recipient/purpose or country/recipient facts. A transition away from `yes` clears those dependent values so stale operational facts do not silently revive. @@ -22,7 +26,9 @@ These readiness rules are authoring-completeness rules, not a legal state machin ## Consequences - Navigation and review-to-source behavior are regression-tested. - Production startup state contains taxonomy metadata only, not inferred customer operational facts. -- Selection, collection mode, processing purpose, service identity, retention, transfer status/detail, and privacy contact findings fail closed and navigate to their owning step. +- Empty collection selection cannot masquerade as `none`; an explicit no-collection attestation is required for a no-collection service. +- No-collection and selected-item states are mutually exclusive, and switching to no-collection invalidates stale item evidence. +- Selection/no-collection, collection mode, processing purpose, collection path, service identity, retention, transfer status/detail, and privacy contact findings fail closed and navigate to their owning step. - Blank transfer state can no longer masquerade as an explicit `none` attestation. - Stale collection and transfer-dependent facts are invalidated when their owning status changes. - The preview remains a projection over structured facts and cannot become an independent source of truth. diff --git a/docs/PRD.md b/docs/PRD.md index 0a1d100..99ad48d 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -6,7 +6,7 @@ ## 제품 원칙 -PolicyWeave는 법률 문장을 임의로 창작하는 도구가 아니다. 운영자가 입력한 사실을 구조화하고, 적용 근거와 확인이 필요한 부분을 분리해 검토본을 만든다. 자동 생성 결과는 변호사·개인정보보호책임자의 검토를 대체하지 않는다. 새 작업공간은 고객의 실제 수집 여부, 수집 구분, 처리 목적, 수집 경로, 제3자 제공 여부 또는 국외 이전 여부를 추정하지 않는다. 비어 있는 값은 `없음`이 아니라 `미확인`이다. +PolicyWeave는 법률 문장을 임의로 창작하는 도구가 아니다. 운영자가 입력한 사실을 구조화하고, 적용 근거와 확인이 필요한 부분을 분리해 검토본을 만든다. 자동 생성 결과는 변호사·개인정보보호책임자의 검토를 대체하지 않는다. 새 작업공간은 고객의 실제 수집 여부, 수집 구분, 처리 목적, 수집 경로, 제3자 제공 여부 또는 국외 이전 여부를 추정하지 않는다. 비어 있는 값은 `없음`이 아니라 `미확인`이다. 실제 개인정보를 수집하지 않는 서비스는 운영자가 이를 명시적으로 확인할 수 있어야 하며, 그 확인은 빈 항목 목록에서 추론되지 않는다. ## MVP @@ -14,6 +14,7 @@ PolicyWeave는 법률 문장을 임의로 창작하는 도구가 아니다. 운 - 입력과 동시에 갱신되는 개인정보처리방침 검토본 - 제품 정의 필수 사실 누락과 권장 검토 항목의 구분 - 처리 항목별 수집 구분·목적·수집 경로 연결 +- 개인정보를 수집하지 않는 경우의 명시적 운영자 확인과 수집 항목 사실의 상호배타성 - 제3자 제공과 국외 이전의 명시적 `있음`/`없음` 확인; `있음`일 때만 종속 상세 사실 요구 - 공개 전 검토 요약과 버전 정보 - 정적 공개 URL 발행 계약(후속 백엔드에서 구현) @@ -30,7 +31,9 @@ PolicyWeave는 법률 문장을 임의로 창작하는 도구가 아니다. 운 - 사용자는 일곱 작성 책임의 제품 정의 필수 사실을 누락 없이 검토할 수 있다. - 새 작업공간은 고객 운영 사실을 미리 채우지 않는다. -- 실제 수집 항목과 각 항목의 수집 구분·처리 목적·수집 경로, 서비스 이름·URL, 보유 기간, 제3자 제공 여부, 국외 이전 여부, 개인정보 보호 담당자·연락 이메일이 확인되기 전에는 공개 준비 상태를 통과하지 않는다. +- 수집 단계는 실제 수집 항목을 하나 이상 확인하거나 `개인정보를 수집하지 않음`을 명시적으로 확인해야 한다. 두 상태가 동시에 참인 데이터는 공개 준비를 통과하지 않는다. +- `개인정보를 수집하지 않음`으로 전환하면 이전 수집 항목의 수집 구분·처리 목적·수집 경로를 폐기하고, 확인을 해제해도 해당 사실을 자동 복원하지 않는다. +- 실제 수집 항목이 있는 경우 각 항목의 수집 구분·처리 목적·수집 경로, 서비스 이름·URL, 보유 기간, 제3자 제공 여부, 국외 이전 여부, 개인정보 보호 담당자·연락 이메일이 확인되기 전에는 공개 준비 상태를 통과하지 않는다. - 서비스 URL은 절대 HTTP(S) 주소 형식을 만족해야 하고, 개인정보 보호 연락 이메일은 최소 주소 형식 검사를 통과해야 한다. 이 검사는 서비스 존재나 메일함 소유권을 주장하지 않는다. - 제3자 제공 또는 국외 이전을 `있음`으로 확인한 경우 해당 수령자/목적 또는 국가/수령자 사실까지 확인되어야 한다. - `없음` 확인은 명시적 운영자 사실이며, 이전에 입력한 종속 제공·이전 상세 사실은 상태 변경 시 폐기된다. diff --git a/docs/TRD.md b/docs/TRD.md index a929345..a57ca17 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -6,21 +6,21 @@ 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. -- `src/policy.ts` owns deterministic review logic for collection selection/mode/purpose/path and the non-collection authoring-completeness findings for service identity, retention, transfer statuses/details, and privacy contact. -- `src/App.tsx` provides the seven-step authoring flow, review navigation, conditional transfer editors, and deterministic preview projection. +- `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, retention, transfer statuses/details, and privacy contact. +- `src/App.tsx` provides the seven-step authoring flow, review navigation, explicit no-collection and transfer-status capture, stale dependent-fact invalidation, and deterministic preview projection. - The current CI contract is lint, Vitest, and TypeScript/Vite build plus live organization-required security/review workflows. ## Functional contracts 1. Every PRD step must route to an editable surface; selecting a step cannot change only the rail indicator. 2. A fresh workspace contains no inferred customer operational facts. Blank authoring values mean unresolved, not `none`. -3. Until at least one actual collection item is selected, readiness produces a blocking selection-not-established finding that navigates to the collection step. +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. Service name, service URL, retention period, 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. 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. 7. Blank/whitespace authoring facts are normalized as unresolved. 8. Disabling a collection item invalidates dependent collection-mode, processing-purpose, and collection-path evidence; re-enabling requires renewed confirmation. 9. Every blocking finding links to the responsible editing step. -10. Preview text is derived from current structured facts; it does not become an independently editable authority. +10. Preview text is derived from current structured facts, including the explicit no-collection attestation; it does not become an independently editable authority. 11. UI copy distinguishes a review draft from legal advice, certification, or a compliance guarantee. These are authoring-completeness contracts, not claims that the resulting policy is legally sufficient. Legal sufficiency remains a separate versioned-rule responsibility backed by authoritative source evidence. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index ef7d8ad..d24a892 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -14,37 +14,39 @@ A 2026-09-02 exact-head commercialization pass found that `initialItems` presele The next exact-head pass found a second buyer-facing authority defect: after completing one selected collection item, the readiness CTA could enable while service identity, retention, third-party provision, international transfer, and privacy contact remained visibly unresolved in the review draft. Blank transfer fields also conflated `not checked` with `none`. Regression/edge-case tests were committed first at `1e77d4424f4b028e56f0453afe17f78dd5cebeca`. Production was repaired at `cbe3a743bd8ba09b3c63d4b79507975a6a77547a`: `getDraftReview` now derives product-defined findings for all non-collection authoring responsibilities; third-party provision and international transfer use explicit unresolved/yes/no states; confirmed `yes` requires dependent details; changing away from `yes` invalidates stale dependent details; total readiness and warning navigation include these findings. These rules establish authoring completeness only and introduce no new legal conclusion, heuristic weighting, or synthetic production facts. -The current pass found that collection-path evidence was editable and stale-path invalidation already existed, but readiness could still pass a selected item whose collection path was blank. A concurrent writer established the failing contract first at `0b2c8a5b13851fa20c61d17e3758892f67858c10`. Production was repaired at `7b843ef8fdc049e7f24513557cd86c7c4bbbcca6`: `getReview` now derives independent whitespace-normalized path blockers, the buyer-facing preview explains which item still needs collection-path evidence and navigates to the owning collection step, and UI regressions were reconciled so the readiness count remains internally consistent. Existing service-URL and privacy-contact-email shape validation was also revalidated on that exact implementation rather than left as a stale planned gap. +A concurrent writer then established the missing collection-path readiness contract first at `0b2c8a5b13851fa20c61d17e3758892f67858c10`. Production/UI repair `7b843ef8fdc049e7f24513557cd86c7c4bbbcca6` made nonblank collection-path evidence an independent blocker with direct warning navigation, and docs/baseline reconciliation followed at `aa3be87a37fa78d55627c9e65464e5cda8fbff60`. Existing service-URL and privacy-contact-email shape validation was revalidated on that implementation rather than left as a stale planned gap. + +The following commercialization pass identified a remaining buyer-flow dead end: a service that genuinely collects no personal data could never clear collection readiness because an empty item selection was always unresolved. Interpreting emptiness as `none` would have reintroduced inference. Test-first commit `97063fc34fbc0e6db2652922f78bc74c00e1ef93` specifies an explicit no-collection attestation, contradiction fail-closed behavior, and stale-item invalidation. Production commit `e73eb83a913597fb05ddea97519d549d58007faf` adds the operator attestation, makes it mutually exclusive with selected items, clears item mode/purpose/path facts when enabled, prevents their later silent revival, and projects the explicit negative fact into the review draft. This is a product authoring-completeness state, not a new legal conclusion. ## Current baseline | Area | Evidence | Status | Commercialization gap | Owner/action | Next verification | | --- | --- | --- | --- | --- | --- | -| Guided authoring | PRD, ADR-0002, seven routed editors, `getReview`, `getDraftReview`, explicit transfer statuses | Repaired foundation | Selection/mode/purpose/path and non-collection authoring responsibilities fail closed; URL/email shape checks exist, but collection-path evidence remains unstructured free text and legal sufficiency is deliberately separate | Policy Fact Authoring: preserve deterministic completeness; only add structured path-evidence types when a real integration/use case proves the need | Exact-head unit/UI edge tests, then browser E2E | -| Customer-fact authority | Zero-inferred startup facts; explicit collection/transfer states; stale dependent-fact invalidation | Repaired | No first-class evidence-backed state yet for a service that explicitly collects no personal data; empty collection selection intentionally remains unresolved | Policy Fact Authoring: add explicit no-collection attestation only when product/source requirements justify it | Contract tests distinguishing unresolved from explicit none | +| Guided authoring | PRD, ADR-0002, seven routed editors, `getReview`, `getDraftReview`, explicit no-collection/transfer states | Repaired foundation | Selection/no-collection, mode, purpose, path and non-collection authoring responsibilities fail closed; URL/email shape checks exist, but collection-path evidence remains unstructured free text and legal sufficiency is deliberately separate | Policy Fact Authoring: preserve deterministic completeness; add structured path-evidence types only when a real integration/use case proves the need | Exact-head unit/UI edge tests, then browser E2E | +| Customer-fact authority | Zero-inferred startup facts; explicit no-collection state; explicit collection/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 the same distinctions | Policy Fact Authoring: encode no-collection as a revision-owned fact, never as a fake collection item | Persistence/schema invariant tests and exact-head UI tests | | Review workspace | Live preview, total blocker count, warning-to-owner navigation, buyer-facing readiness guidance | Implemented foundation | No browser screenshot matrix or realistic interaction evidence | UX: add Playwright + automated accessibility harness and verify action edges | Desktop/tablet/mobile screenshots, keyboard/focus and accessibility checks | | Accessibility | Semantic controls and focus-visible behavior | Partial | No automated WCAG 2.2 evidence or screen-reader contract | UX/Test Engineering: add axe/Playwright and manual interaction record | Exact-head WCAG audit and browser matrix | | Legal/rule traceability | `docs/research-traceability.md`; source/effective-date model | Source baseline established | Current readiness rules are product completeness only; no versioned article/section-level legal-rule engine | Legal Source Registry: map future legal validations to source/effective date/revision | Snapshot fixtures across effective dates | -| Policy model | ADRs, ARCHITECTURE, TRD | Designed, browser-memory only | No versioned durable schema, migrations, audit history, 3NF implementation, or explicit UPSERT/idempotency | Platform: define `policy_revision` schema and item-level conflict contracts before storage | Schema tests and migration round trip | +| Policy model | ADRs, ARCHITECTURE, TRD | Designed, browser-memory only | No versioned durable schema, migrations, audit history, 3NF implementation, or explicit UPSERT/idempotency | Platform: define `policy_revision` schema and item-level conflict contracts before storage; persist explicit no-collection state on the revision-owned fact model | Schema tests and migration round trip | | 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 | Test-first startup authority, seven-step completeness, and collection-path regressions plus earlier routing/responsive/SAST regressions | Improved | Browser E2E/accessibility and repository-wide 100% execution/docstring coverage are not yet evidenced | Test Engineering: measure coverage and add browser-level behavior tests | Exact-head coverage + browser evidence | +| Tests | Test-first startup authority, seven-step completeness, collection-path, and explicit no-collection regressions plus earlier routing/responsive/SAST regressions | Improved | Browser E2E/accessibility and repository-wide 100% execution/docstring coverage are not yet evidenced | Test Engineering: measure coverage and add browser-level behavior tests | Exact-head coverage + browser 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 | Live external gate | Every branch movement invalidates predecessor evidence and stale approval; dependency/reviewer control-plane failures can fail closed independently | Re-fetch exact-head workflows/reviews; repair causal owner if failure is real | Terminal exact-head checks + independent approval + resolved threads | +| CI/security merge gate | Repo CI plus active organization ruleset-required workflows | Live external gate | 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 + independent approval + resolved threads | ## DDD/context map Core subdomain: **Policy Fact Authoring**. Supporting: **Review & Publication**, **Legal Source Registry**. Generic infrastructure remains outside domain authority. `Policy Fact Authoring -> Review & Publication` supplies a versioned fact contract. `Legal Source Registry -> Review & Publication` supplies versioned source/rule evidence through an ACL. Customer/source-system integrations remain adapters and may not mutate foreign systems through hidden coupling. -Ubiquitous language: `policy_revision`, `service_profile`, `collection_item`, `processing_purpose`, `retention_rule`, `third_party_transfer`, `international_transfer`, `privacy_contact`, `review_finding`, `legal_source_revision`, `publication_revision`. +Ubiquitous language: `policy_revision`, `service_profile`, explicit no-collection attestation, `collection_item`, `processing_purpose`, `retention_rule`, `third_party_transfer`, `international_transfer`, `privacy_contact`, `review_finding`, `legal_source_revision`, `publication_revision`. -The future aggregate root is the minimal `policy_revision` boundary. Rendered prose is a read projection. `publication_revision` is immutable and explicitly superseded rather than updated. Relational persistence is 3NF by default; named persistence objects use at least two semantic words and `snake_case`, and item-level UPSERT/idempotency semantics must be explicit. +The future aggregate root is the minimal `policy_revision` boundary. The no-collection fact is revision-owned state rather than a synthetic `collection_item`. Rendered prose is a read projection. `publication_revision` is immutable and explicitly superseded rather than updated. Relational persistence is 3NF by default; named persistence objects use at least two semantic words and `snake_case`, and item-level UPSERT/idempotency semantics must be explicit. ## Buyer-visible release gates -A publishable release requires complete fact-to-warning navigation, deterministic rendering of captured source facts, no inferred customer operational facts, explicit unresolved-review blocking across the seven authoring responsibilities, immutable reviewed publication revisions, accessible responsive interaction, authoritative source/version traceability for legal rules, hosted security/privacy evidence, and exact-head CI/security/independent review. Buyer-facing wording describes assistance and reviewability, never a compliance guarantee or an internal implementation boundary. +A publishable release requires complete fact-to-warning navigation, deterministic rendering of captured source facts, no inferred customer operational facts, explicit unresolved-review blocking across the seven authoring responsibilities, an explicit no-collection path for genuine no-data services, immutable reviewed publication revisions, accessible responsive interaction, authoritative source/version traceability for legal rules, hosted security/privacy evidence, and exact-head CI/security/independent review. Buyer-facing wording describes assistance and reviewability, never a compliance guarantee or an internal implementation boundary. ## Active commercialization order 1. Reacquire exact-current-head CI/security/SAST/central workflow execution and independent approval; merge PR #1 only through ordinary protection. -2. Add browser-level responsive/accessibility evidence for the now fail-closed seven-step workflow and verify remaining interaction edges. -3. Define and test versioned `policy_revision` 3NF persistence/audit and explicit item-level UPSERT contracts. +2. Add browser-level responsive/accessibility evidence for the now fail-closed seven-step workflow, including the no-collection transition and verify remaining interaction edges. +3. Define and test versioned `policy_revision` 3NF persistence/audit, explicit no-collection storage, and item-level UPSERT contracts. 4. Implement secure review/publication with immutable releases and explicit supersession. 5. Add hosted tenant isolation, audit/incident evidence, compose deployment, and realistic performance/load tests when network services exist. From e5b77f1897ab13dd4f27ccd8b7fa724ba3e74bb0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 14:27:09 +0900 Subject: [PATCH 086/282] test(a11y): require high-contrast keyboard focus indicators --- src/styles.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/styles.test.ts b/src/styles.test.ts index a75c76d..5dd389b 100644 --- a/src/styles.test.ts +++ b/src/styles.test.ts @@ -24,4 +24,9 @@ describe('responsive review workspace CSS contract', () => { const mobile = mediaBlock(720) expect(mobile).toContain('.review-bar output { position: static; grid-column: 1 / -1;') }) + + it('uses the high-contrast product token for keyboard focus indicators', () => { + expect(css).toContain(':focus-visible { outline: 3px solid var(--green); outline-offset: 2px; }') + expect(css).toContain('.check-label input:focus-visible + .box { outline: 3px solid var(--green); outline-offset: 2px; }') + }) }) From e9e7dcc5c4160a70a9c483574fb651125589392d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 14:27:45 +0900 Subject: [PATCH 087/282] fix(a11y): increase authored keyboard focus contrast --- src/styles.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/styles.css b/src/styles.css index 3bc4f14..2eb87f4 100644 --- a/src/styles.css +++ b/src/styles.css @@ -14,7 +14,7 @@ body { margin: 0; min-width: 320px; } button, input, select, textarea { font: inherit; } button { cursor: pointer; } -:focus-visible { outline: 3px solid #8eb59e; outline-offset: 2px; } +:focus-visible { outline: 3px solid var(--green); outline-offset: 2px; } .topbar { min-height: 58px; display: flex; @@ -78,7 +78,7 @@ button { cursor: pointer; } .check-label { display: flex; align-items: center; gap: 8px; } .check-label input { position: absolute; opacity: 0; } .box { width: 17px; height: 17px; border: 1px solid #b8beba; display: flex; align-items: center; justify-content: center; color: transparent; } -.check-label input:focus-visible + .box { outline: 3px solid #8eb59e; outline-offset: 2px; } +.check-label input:focus-visible + .box { outline: 3px solid var(--green); outline-offset: 2px; } .check-label input:checked + .box { background: var(--green); border-color: var(--green); color: white; } .item-row > span { color: #5f6862; } .select-wrap { position: relative; } From ee73bc24e80b32851dcc6f519a457ca9d5efed56 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 14:28:12 +0900 Subject: [PATCH 088/282] test(a11y): verify focus token contrast ratio --- src/styles.test.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/styles.test.ts b/src/styles.test.ts index 5dd389b..2c96d59 100644 --- a/src/styles.test.ts +++ b/src/styles.test.ts @@ -14,6 +14,18 @@ function mediaBlock(maxWidth: keyof typeof mediaPatterns) { return match[1] } +function relativeLuminance(hex: string) { + const channels = hex.slice(1).match(/.{2}/g)?.map((channel) => Number.parseInt(channel, 16) / 255) + if (!channels || channels.length !== 3) throw new Error(`Invalid RGB hex: ${hex}`) + const linear = channels.map((channel) => channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4) + return 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2] +} + +function contrastRatio(left: string, right: string) { + const luminances = [relativeLuminance(left), relativeLuminance(right)].sort((a, b) => b - a) + return (luminances[0] + 0.05) / (luminances[1] + 0.05) +} + describe('responsive review workspace CSS contract', () => { it('keeps the policy preview available through tablet widths', () => { const tablet = mediaBlock(1300) @@ -25,7 +37,10 @@ describe('responsive review workspace CSS contract', () => { expect(mobile).toContain('.review-bar output { position: static; grid-column: 1 / -1;') }) - it('uses the high-contrast product token for keyboard focus indicators', () => { + it('uses a focus token that preserves at least 3:1 contrast against white', () => { + const green = css.match(/--green:\s*(#[0-9a-fA-F]{6});/)?.[1] + expect(green).toBeDefined() + expect(contrastRatio(green!, '#ffffff')).toBeGreaterThanOrEqual(3) expect(css).toContain(':focus-visible { outline: 3px solid var(--green); outline-offset: 2px; }') expect(css).toContain('.check-label input:focus-visible + .box { outline: 3px solid var(--green); outline-offset: 2px; }') }) From d050b1e6e69f02d32e942f866c8227d3455167bb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 14:28:42 +0900 Subject: [PATCH 089/282] docs(a11y): trace WCAG focus contrast evidence --- docs/research-traceability.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/research-traceability.md b/docs/research-traceability.md index 128fed7..393c5f9 100644 --- a/docs/research-traceability.md +++ b/docs/research-traceability.md @@ -12,6 +12,10 @@ PolicyWeave treats legal and regulatory material as versioned evidence, not as i | Republic of Korea, Enforcement Decree of the Personal Information Protection Act (개인정보 보호법 시행령), National Law Information Center | Presidential Decree No. 36121, promulgated 2026-02-19 and effective 2026-08-20. Retrieved 2026-09-02; canonical National Law Information Center legal-information sequence 283503. https://www.law.go.kr/LSW/lsSideInfoP.do?docCls=jo&joNo=0032&lsiSeq=283503 | Subordinate-rule authority/effective-date anchor | Registry only; no decree-level rules are encoded yet. Future mappings must record the exact decree article and effective-date snapshot. | | Personal Information Protection Commission. (2026, April 23). 개인정보 처리방침 작성지침(2026.4. 개정) [Guideline for writing privacy policies, April 2026 revision]. | PIPC marks this as the current guide. Retrieved 2026-09-02. https://pipc.go.kr/np/cop/bbs/selectBoardList.do?bbsId=BS217&mCode=D010030000 | Authoring/review guidance and template requirement discovery | Source registered; requirement-by-requirement mapping still required | +## Accessibility standards traceability + +W3C's WCAG 2.2 guidance states that authored visual focus indicators are subject to the Level AA non-text contrast requirement, and its Focus Visible guidance explicitly points focus indication to Success Criterion 1.4.11. The previous PolicyWeave focus color `#8eb59e` was approximately 2.27:1 against white, so it did not provide the 3:1 contrast expected for an authored focus indicator. Test-first commit `e5b77f1897ab13dd4f27ccd8b7fa724ba3e74bb0` required the high-contrast product token; production commit `e9e7dcc5c4160a70a9c483574fb651125589392d` moved generic and custom-checkbox focus outlines to `--green` (`#174f35`, approximately 9.52:1 against white); regression commit `ee73bc24e80b32851dcc6f519a457ca9d5efed56` computes the token contrast and enforces a minimum 3:1 ratio rather than relying only on literal CSS text. This is CSS-level evidence only; browser keyboard traversal, focus-not-obscured behavior, zoom, screen-reader behavior, and responsive screenshots remain open. + ## APA 7 references Personal Information Protection Commission. (2026, April 23). *개인정보 처리방침 작성지침(2026.4. 개정)* [Guideline for writing privacy policies, April 2026 revision]. https://pipc.go.kr/np/cop/bbs/selectBoardList.do?bbsId=BS217&mCode=D010030000 @@ -22,6 +26,10 @@ Republic of Korea. (2026). *개인정보 보호법 일부개정법률* [Amendmen Republic of Korea. (2026). *개인정보 보호법 시행령* [Enforcement Decree of the Personal Information Protection Act] (Presidential Decree No. 36121, promulgated February 19, 2026, effective August 20, 2026; legal-information sequence 283503). National Law Information Center. https://www.law.go.kr/LSW/lsSideInfoP.do?docCls=jo&joNo=0032&lsiSeq=283503 +World Wide Web Consortium. (2025). *Understanding Success Criterion 1.4.11: Non-text contrast*. Web Accessibility Initiative. https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html + +World Wide Web Consortium. (2025). *Understanding Success Criterion 2.4.7: Focus visible*. Web Accessibility Initiative. https://www.w3.org/WAI/WCAG22/Understanding/focus-visible.html + ## Traceability rules 1. Every future legal/rule/template implementation records source identifier, article/section where applicable, effective date, source revision or retrieval timestamp/digest, implementation symbol, and tests. 2. Future-effective law is never silently treated as already-effective law. A rule set declares the jurisdiction/effective-date snapshot it evaluates; provisions with distinct commencement dates remain distinct entries rather than inheriting a statute-level date. @@ -30,4 +38,4 @@ Republic of Korea. (2026). *개인정보 보호법 시행령* [Enforcement Decre 5. LLM output, if later used to explain or propose wording, is never an authoritative legal source and cannot change review/publication state. ## Current gap -The seven-step workspace now captures the product's intended fact categories, but retention, third-party provision, international transfer, contact/controller information, and legal-basis review still need requirement-level mappings to the authoritative register, deterministic validation, and regression fixtures before PolicyWeave can claim those steps are legally complete. +The seven-step workspace now captures the product's intended fact categories, but retention, third-party provision, international transfer, contact/controller information, and legal-basis review still need requirement-level mappings to the authoritative register, deterministic validation, and regression fixtures before PolicyWeave can claim those steps are legally complete. CSS-level focus contrast now has an executable regression contract, but browser-level accessibility evidence remains required before claiming WCAG conformance. From 27364e3fabcf58e2458b693d993de192b9c1c0c6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 14:28:59 +0900 Subject: [PATCH 090/282] docs(changelog): record focus contrast repair --- CHANGELOG.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c33b7c5..c96fd99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,8 @@ All notable product changes are recorded here. PolicyWeave is pre-release; entri - Explicit `개인정보를 수집하지 않음` operator attestation so a genuine no-collection service can complete authoring without treating an empty item list as `none`. - Warning-to-source navigation for missing collection selection/no-collection confirmation, collection mode, collection-path evidence, processing purposes, service identity, retention, transfer statuses/details, and privacy contact. - 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, explicit no-collection state and stale-item 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, and non-deceptive handling of unshipped affordances. -- Product/technical gap ledger, architecture, technical requirements, security baseline, and legal-source traceability. +- Regression coverage for all seven step routes, zero-inferred startup facts, explicit no-collection state and stale-item 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, and authored focus-indicator contrast. +- Product/technical gap ledger, architecture, technical requirements, security baseline, and legal-source/accessibility traceability. ### Changed - Fresh workspaces no longer preselect collection items or prefill collection modes/processing purposes; readiness fails closed until the operator establishes actual collection facts or explicitly attests that the service collects no personal data. @@ -27,13 +27,14 @@ All notable product changes are recorded here. PolicyWeave is pre-release; entri - Review preview applies the same whitespace-normalized purpose-completeness contract as the blocking review rule. - 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. +- 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. - GitHub Actions checkout dependencies are SHA pinned and credentials are not persisted in the working tree. ### Not yet shipped - Authoritative legal-rule snapshots that can determine legal sufficiency beyond product-defined fact completeness. -- Browser-matrix/accessibility evidence and screenshot verification. +- Browser-matrix/accessibility evidence and screenshot verification beyond the CSS-level focus-contrast contract. - Durable versioned policy persistence and audit history. - Authenticated immutable publication revisions and public URL lifecycle. - Hosted tenant/security/operability evidence and endpoint load testing. From aa194db5ef5f2c7fd76ced2fc53693ee2173c58f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 14:29:24 +0900 Subject: [PATCH 091/282] docs(gap): record focus contrast repair and remaining browser evidence --- docs/product-technical-gap-baseline.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index d24a892..fbab05a 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -18,6 +18,8 @@ A concurrent writer then established the missing collection-path readiness contr The following commercialization pass identified a remaining buyer-flow dead end: a service that genuinely collects no personal data could never clear collection readiness because an empty item selection was always unresolved. Interpreting emptiness as `none` would have reintroduced inference. Test-first commit `97063fc34fbc0e6db2652922f78bc74c00e1ef93` specifies an explicit no-collection attestation, contradiction fail-closed behavior, and stale-item invalidation. Production commit `e73eb83a913597fb05ddea97519d549d58007faf` adds the operator attestation, makes it mutually exclusive with selected items, clears item mode/purpose/path facts when enabled, prevents their later silent revival, and projects the explicit negative fact into the review draft. This is a product authoring-completeness state, not a new legal conclusion. +The current accessibility pass established a concrete authored-focus defect rather than claiming generic WCAG conformance: `styles.css` used `#8eb59e` for generic and custom-checkbox keyboard focus outlines, approximately 2.27:1 against white. W3C WCAG 2.2 guidance ties authored focus indication to the Level AA non-text contrast requirement. Test-first commit `e5b77f1897ab13dd4f27ccd8b7fa724ba3e74bb0` required the high-contrast product token; production commit `e9e7dcc5c4160a70a9c483574fb651125589392d` changed both focus paths to `--green` (`#174f35`, approximately 9.52:1 against white); regression commit `ee73bc24e80b32851dcc6f519a457ca9d5efed56` now computes the token contrast and enforces a minimum 3:1 ratio. `docs/research-traceability.md` records the W3C authority and intentionally leaves browser keyboard traversal, focus-not-obscured, zoom, screen-reader, and responsive screenshot evidence open. + ## Current baseline | Area | Evidence | Status | Commercialization gap | Owner/action | Next verification | @@ -25,12 +27,12 @@ The following commercialization pass identified a remaining buyer-flow dead end: | Guided authoring | PRD, ADR-0002, seven routed editors, `getReview`, `getDraftReview`, explicit no-collection/transfer states | Repaired foundation | Selection/no-collection, mode, purpose, path and non-collection authoring responsibilities fail closed; URL/email shape checks exist, but collection-path evidence remains unstructured free text and legal sufficiency is deliberately separate | Policy Fact Authoring: preserve deterministic completeness; add structured path-evidence types only when a real integration/use case proves the need | Exact-head unit/UI edge tests, then browser E2E | | Customer-fact authority | Zero-inferred startup facts; explicit no-collection state; explicit collection/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 the same distinctions | Policy Fact Authoring: encode no-collection as a revision-owned fact, never as a fake collection item | Persistence/schema invariant tests and exact-head UI tests | | Review workspace | Live preview, total blocker count, warning-to-owner navigation, buyer-facing readiness guidance | Implemented foundation | No browser screenshot matrix or realistic interaction evidence | UX: add Playwright + automated accessibility harness and verify action edges | Desktop/tablet/mobile screenshots, keyboard/focus and accessibility checks | -| Accessibility | Semantic controls and focus-visible behavior | Partial | No automated WCAG 2.2 evidence or screen-reader contract | UX/Test Engineering: add axe/Playwright and manual interaction record | Exact-head WCAG audit and browser matrix | +| Accessibility | Semantic controls, visible focus behavior, WCAG-traced focus token, executable >=3:1 contrast regression | Partial, CSS focus defect repaired | Browser keyboard traversal, focus-not-obscured, zoom, screen-reader and full automated WCAG 2.2 evidence remain absent | UX/Test Engineering: add axe/Playwright and manual interaction record without claiming conformance from CSS alone | Exact-head WCAG/browser matrix including no-collection transition | | Legal/rule traceability | `docs/research-traceability.md`; source/effective-date model | Source baseline established | Current readiness rules are product completeness only; no versioned article/section-level legal-rule engine | Legal Source Registry: map future legal validations to source/effective date/revision | Snapshot fixtures across effective dates | | Policy model | ADRs, ARCHITECTURE, TRD | Designed, browser-memory only | No versioned durable schema, migrations, audit history, 3NF implementation, or explicit UPSERT/idempotency | Platform: define `policy_revision` schema and item-level conflict contracts before storage; persist explicit no-collection state on the revision-owned fact model | Schema tests and migration round trip | | 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 | Test-first startup authority, seven-step completeness, collection-path, and explicit no-collection regressions plus earlier routing/responsive/SAST regressions | Improved | Browser E2E/accessibility and repository-wide 100% execution/docstring coverage are not yet evidenced | Test Engineering: measure coverage and add browser-level behavior tests | Exact-head coverage + browser evidence | +| Tests | Test-first startup authority, seven-step completeness, collection-path, explicit no-collection and focus-contrast regressions plus earlier routing/responsive/SAST regressions | Improved | Browser E2E/accessibility and repository-wide 100% execution/docstring coverage are not yet evidenced | Test Engineering: measure coverage and add browser-level behavior tests | Exact-head coverage + browser 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 | Live external gate | 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 + independent approval + resolved threads | @@ -46,7 +48,7 @@ A publishable release requires complete fact-to-warning navigation, deterministi ## Active commercialization order 1. Reacquire exact-current-head CI/security/SAST/central workflow execution and independent approval; merge PR #1 only through ordinary protection. -2. Add browser-level responsive/accessibility evidence for the now fail-closed seven-step workflow, including the no-collection transition and verify remaining interaction edges. +2. Complete browser-level responsive/accessibility evidence for the fail-closed seven-step workflow, including keyboard traversal, no-collection transition, focus-not-obscured behavior, zoom/screen-reader checks and screenshots; the CSS focus-contrast defect is already repaired. 3. Define and test versioned `policy_revision` 3NF persistence/audit, explicit no-collection storage, and item-level UPSERT contracts. 4. Implement secure review/publication with immutable releases and explicit supersession. 5. Add hosted tenant isolation, audit/incident evidence, compose deployment, and realistic performance/load tests when network services exist. From e9ff9cd49532560b28d970236d4f64fa03f21d25 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 15:52:17 +0900 Subject: [PATCH 092/282] test: specify authoring focus transfer --- src/authoring-focus.test.tsx | 51 ++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 src/authoring-focus.test.tsx diff --git a/src/authoring-focus.test.tsx b/src/authoring-focus.test.tsx new file mode 100644 index 0000000..b1dc39c --- /dev/null +++ b/src/authoring-focus.test.tsx @@ -0,0 +1,51 @@ +// @vitest-environment jsdom +import { cleanup, fireEvent, render, waitFor } from '@testing-library/react' +import { afterEach, describe, expect, it } from 'vitest' +import App from './App' +import { AuthoringFocusController } from './AuthoringFocusController' + +afterEach(cleanup) + +function renderAuthoringWorkspace() { + return render(<>) +} + +async function expectActiveStepHeading(container: HTMLElement, text: string) { + await waitFor(() => { + const heading = container.querySelector('.form-panel .section-head h1') + expect(heading?.textContent).toBe(text) + expect(document.activeElement).toBe(heading) + expect(heading?.tabIndex).toBe(-1) + }) +} + +describe('authoring focus transitions', () => { + it('moves focus to the newly selected step heading from the authoring rail', async () => { + const { container } = renderAuthoringWorkspace() + fireEvent.click(container.querySelectorAll('.rail li button')[0]) + + await expectActiveStepHeading(container, '1. 서비스 정보') + }) + + it('moves focus to the next step heading after sequential navigation', async () => { + const { container } = renderAuthoringWorkspace() + fireEvent.click(container.querySelectorAll('.rail li button')[0]) + await expectActiveStepHeading(container, '1. 서비스 정보') + + fireEvent.click(container.querySelector('.form-actions .primary')!) + + await expectActiveStepHeading(container, '2. 수집 항목') + }) + + it('moves focus to the owning step when a review warning is activated', async () => { + const { container } = renderAuthoringWorkspace() + fireEvent.click(container.querySelectorAll('.rail li button')[0]) + await expectActiveStepHeading(container, '1. 서비스 정보') + + const warningButton = Array.from(container.querySelectorAll('.document-warning button')) + .find((button) => button.textContent?.includes('수집 항목'))! + fireEvent.click(warningButton) + + await expectActiveStepHeading(container, '2. 수집 항목') + }) +}) From 960432e7557c55d13ab83106970f1e9bc93b415f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 15:52:33 +0900 Subject: [PATCH 093/282] feat: preserve focus across authoring steps --- src/AuthoringFocusController.tsx | 39 ++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/AuthoringFocusController.tsx diff --git a/src/AuthoringFocusController.tsx b/src/AuthoringFocusController.tsx new file mode 100644 index 0000000..9d51b1e --- /dev/null +++ b/src/AuthoringFocusController.tsx @@ -0,0 +1,39 @@ +import { useEffect } from 'react' + +/** + * Transfers focus to the active authoring-step heading after explicit step navigation. + * + * PolicyWeave replaces the editing panel when the operator uses the step rail, sequential + * previous/next controls, or a review warning. Moving focus to the new heading keeps keyboard and + * assistive-technology context aligned with the visible step without stealing focus from ordinary + * form controls or from the dedicated preview shortcut. + */ +export function AuthoringFocusController() { + useEffect(() => { + const pendingFocusTransfers = new Set() + + const handleNavigationClick = (event: MouseEvent) => { + if (!(event.target instanceof Element)) return + const button = event.target.closest('button') + if (!(button instanceof HTMLButtonElement)) return + if (!button.closest('.rail li, .form-actions, .document-warning')) return + + const timer = window.setTimeout(() => { + pendingFocusTransfers.delete(timer) + const heading = document.querySelector('.form-panel .section-head h1') + if (!heading) return + heading.tabIndex = -1 + heading.focus({ preventScroll: true }) + }, 0) + pendingFocusTransfers.add(timer) + } + + document.addEventListener('click', handleNavigationClick) + return () => { + document.removeEventListener('click', handleNavigationClick) + pendingFocusTransfers.forEach((timer) => window.clearTimeout(timer)) + } + }, []) + + return null +} From 2ec1e10d44a04332d223e1be42a3fde9c28c7f40 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 15:52:45 +0900 Subject: [PATCH 094/282] feat: activate authoring focus transfer --- src/main.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main.tsx b/src/main.tsx index 571dd17..97892e0 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,6 +1,12 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import App from './App' +import { AuthoringFocusController } from './AuthoringFocusController' import './styles.css' -createRoot(document.getElementById('root')!).render() +createRoot(document.getElementById('root')!).render( + + + + , +) From cc87e9ca23b5bc5399895fa697e15ba4f0f2ed90 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 15:53:19 +0900 Subject: [PATCH 095/282] docs: record authoring focus transition --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c96fd99..3413141 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ All notable product changes are recorded here. PolicyWeave is pre-release; entri - Explicit `개인정보를 수집하지 않음` operator attestation so a genuine no-collection service can complete authoring without treating an empty item list as `none`. - Warning-to-source navigation for missing collection selection/no-collection confirmation, collection mode, collection-path evidence, processing purposes, service identity, retention, transfer statuses/details, and privacy contact. - 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, explicit no-collection state and stale-item 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, and authored focus-indicator contrast. +- Regression coverage for all seven step routes, zero-inferred startup facts, explicit no-collection state and stale-item 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. ### Changed @@ -25,6 +25,7 @@ All notable product changes are recorded here. PolicyWeave is pre-release; entri - Service URL entered in the service-information step is projected into the review draft. - Collection-path editing remains in the collection step while processing-purpose editing stays in its dedicated purpose step. - Review preview applies the same whitespace-normalized purpose-completeness contract as the blocking review rule. +- 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. - 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. - 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. @@ -34,7 +35,7 @@ All notable product changes are recorded here. PolicyWeave is pre-release; entri ### Not yet shipped - Authoritative legal-rule snapshots that can determine legal sufficiency beyond product-defined fact completeness. -- Browser-matrix/accessibility evidence and screenshot verification beyond the CSS-level focus-contrast contract. +- Full browser-matrix/accessibility evidence and screenshot verification beyond CSS focus contrast and deterministic step-focus transition tests. - Durable versioned policy persistence and audit history. - Authenticated immutable publication revisions and public URL lifecycle. - Hosted tenant/security/operability evidence and endpoint load testing. From 67b7f6657c0db7402c90f1d146481bb80e880032 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 15:53:51 +0900 Subject: [PATCH 096/282] docs: specify logical focus order contract --- docs/TRD.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/TRD.md b/docs/TRD.md index a57ca17..1d204e7 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -8,6 +8,7 @@ This TRD covers the pre-release PolicyWeave browser workspace and the contracts - Structured authoring state is in browser memory; no production database or backend exists. - `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, retention, transfer statuses/details, and privacy contact. - `src/App.tsx` provides the seven-step authoring flow, review navigation, explicit no-collection and 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; ordinary form controls and the dedicated preview shortcut are outside this behavior. - The current CI contract is lint, Vitest, and TypeScript/Vite build plus live organization-required security/review workflows. ## Functional contracts @@ -20,8 +21,9 @@ This TRD covers the pre-release PolicyWeave browser workspace and the contracts 7. Blank/whitespace authoring facts are normalized as unresolved. 8. Disabling a collection item invalidates dependent collection-mode, processing-purpose, and collection-path evidence; re-enabling requires renewed confirmation. 9. Every blocking finding links to the responsible editing step. -10. Preview text is derived from current structured facts, including the explicit no-collection attestation; it does not become an independently editable authority. -11. UI copy distinguishes a review draft from legal advice, certification, or a compliance guarantee. +10. Explicit step navigation through the rail, previous/next controls, or a blocking-finding action transfers focus to the newly active step heading after that surface renders. This logical focus-order contract must not steal focus from ordinary editing controls or the dedicated preview shortcut. +11. Preview text is derived from current structured facts, including the explicit no-collection attestation; it does not become an independently editable authority. +12. UI copy distinguishes a review draft from legal advice, certification, or a compliance guarantee. These are authoring-completeness contracts, not claims that the resulting policy is legally sufficient. Legal sufficiency remains a separate versioned-rule responsibility backed by authoritative source evidence. @@ -29,7 +31,7 @@ These are authoring-completeness contracts, not claims that the resulting policy - Touched production behavior requires regression and edge-case tests first. - Touched production functions carry descriptive JSDoc rather than relying on implicit behavior. - Do not suppress deprecation warnings to pass CI. -- Browser/accessibility validation must include keyboard/focus order, WCAG 2.2 automated checks, responsive desktop/tablet/mobile interactions, and screenshot evidence before a buyer-facing accessibility claim. +- Browser/accessibility validation must include keyboard/focus order, WCAG 2.2 automated checks, responsive desktop/tablet/mobile interactions, and screenshot evidence before a buyer-facing accessibility claim. Deterministic jsdom focus-transition coverage is supporting evidence only and does not substitute for real-browser focus-not-obscured, zoom, screen-reader, or responsive verification. - 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. From 7e37eade9e6a17c37ed06e84082ca866a4040f6a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 15:54:38 +0900 Subject: [PATCH 097/282] docs: trace focus order evidence --- docs/research-traceability.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/research-traceability.md b/docs/research-traceability.md index 393c5f9..6c08e68 100644 --- a/docs/research-traceability.md +++ b/docs/research-traceability.md @@ -14,7 +14,9 @@ PolicyWeave treats legal and regulatory material as versioned evidence, not as i ## Accessibility standards traceability -W3C's WCAG 2.2 guidance states that authored visual focus indicators are subject to the Level AA non-text contrast requirement, and its Focus Visible guidance explicitly points focus indication to Success Criterion 1.4.11. The previous PolicyWeave focus color `#8eb59e` was approximately 2.27:1 against white, so it did not provide the 3:1 contrast expected for an authored focus indicator. Test-first commit `e5b77f1897ab13dd4f27ccd8b7fa724ba3e74bb0` required the high-contrast product token; production commit `e9e7dcc5c4160a70a9c483574fb651125589392d` moved generic and custom-checkbox focus outlines to `--green` (`#174f35`, approximately 9.52:1 against white); regression commit `ee73bc24e80b32851dcc6f519a457ca9d5efed56` computes the token contrast and enforces a minimum 3:1 ratio rather than relying only on literal CSS text. This is CSS-level evidence only; browser keyboard traversal, focus-not-obscured behavior, zoom, screen-reader behavior, and responsive screenshots remain open. +W3C's WCAG 2.2 guidance states that authored visual focus indicators are subject to the Level AA non-text contrast requirement, and its Focus Visible guidance explicitly points focus indication to Success Criterion 1.4.11. The previous PolicyWeave focus color `#8eb59e` was approximately 2.27:1 against white, so it did not provide the 3:1 contrast expected for an authored focus indicator. Test-first commit `e5b77f1897ab13dd4f27ccd8b7fa724ba3e74bb0` required the high-contrast product token; production commit `e9e7dcc5c4160a70a9c483574fb651125589392d` moved generic and custom-checkbox focus outlines to `--green` (`#174f35`, approximately 9.52:1 against white); regression commit `ee73bc24e80b32851dcc6f519a457ca9d5efed56` computes the token contrast and enforces a minimum 3:1 ratio rather than relying only on literal CSS text. + +WCAG 2.2 Success Criterion 2.4.3 requires sequential focus navigation to preserve meaning and operability; W3C's understanding guidance also recognizes programmatically moving focus to non-interactive content when that content provides the logical next context. PolicyWeave changed the visible authoring surface after step-rail, previous/next, and review-warning activation while leaving focus on the triggering control. Test-first commit `e9ff9cd49532560b28d970236d4f64fa03f21d25` specifies that the newly active step heading receives programmatic focus. Production commits `960432e7557c55d13ab83106970f1e9bc93b415f` and `2ec1e10d44a04332d223e1be42a3fde9c28c7f40` implement and activate that bounded transition while excluding ordinary editing controls and the dedicated preview shortcut. These jsdom regressions establish logical focus-transition behavior only; real-browser keyboard traversal, Success Criterion 2.4.11 focus-not-obscured evidence, zoom, screen-reader behavior, and responsive screenshots remain open, so PolicyWeave does not claim full WCAG conformance from these repairs. ## APA 7 references @@ -28,6 +30,8 @@ Republic of Korea. (2026). *개인정보 보호법 시행령* [Enforcement Decre World Wide Web Consortium. (2025). *Understanding Success Criterion 1.4.11: Non-text contrast*. Web Accessibility Initiative. https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html +World Wide Web Consortium. (2025). *Understanding Success Criterion 2.4.3: Focus order*. Web Accessibility Initiative. https://www.w3.org/WAI/WCAG22/Understanding/focus-order.html + World Wide Web Consortium. (2025). *Understanding Success Criterion 2.4.7: Focus visible*. Web Accessibility Initiative. https://www.w3.org/WAI/WCAG22/Understanding/focus-visible.html ## Traceability rules @@ -38,4 +42,4 @@ World Wide Web Consortium. (2025). *Understanding Success Criterion 2.4.7: Focus 5. LLM output, if later used to explain or propose wording, is never an authoritative legal source and cannot change review/publication state. ## Current gap -The seven-step workspace now captures the product's intended fact categories, but retention, third-party provision, international transfer, contact/controller information, and legal-basis review still need requirement-level mappings to the authoritative register, deterministic validation, and regression fixtures before PolicyWeave can claim those steps are legally complete. CSS-level focus contrast now has an executable regression contract, but browser-level accessibility evidence remains required before claiming WCAG conformance. +The seven-step workspace now captures the product's intended fact categories, but retention, third-party provision, international transfer, contact/controller information, and legal-basis review still need requirement-level mappings to the authoritative register, deterministic validation, and regression fixtures before PolicyWeave can claim those steps are legally complete. CSS-level focus contrast and deterministic step-focus transfer now have executable regression contracts, but real-browser accessibility evidence remains required before claiming WCAG conformance. From 260de02fd26fde4f82475843a9bbe767052ca1c3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 15:55:22 +0900 Subject: [PATCH 098/282] docs: reconcile focus accessibility gap --- docs/product-technical-gap-baseline.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index fbab05a..d4b2cd7 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -18,7 +18,9 @@ A concurrent writer then established the missing collection-path readiness contr The following commercialization pass identified a remaining buyer-flow dead end: a service that genuinely collects no personal data could never clear collection readiness because an empty item selection was always unresolved. Interpreting emptiness as `none` would have reintroduced inference. Test-first commit `97063fc34fbc0e6db2652922f78bc74c00e1ef93` specifies an explicit no-collection attestation, contradiction fail-closed behavior, and stale-item invalidation. Production commit `e73eb83a913597fb05ddea97519d549d58007faf` adds the operator attestation, makes it mutually exclusive with selected items, clears item mode/purpose/path facts when enabled, prevents their later silent revival, and projects the explicit negative fact into the review draft. This is a product authoring-completeness state, not a new legal conclusion. -The current accessibility pass established a concrete authored-focus defect rather than claiming generic WCAG conformance: `styles.css` used `#8eb59e` for generic and custom-checkbox keyboard focus outlines, approximately 2.27:1 against white. W3C WCAG 2.2 guidance ties authored focus indication to the Level AA non-text contrast requirement. Test-first commit `e5b77f1897ab13dd4f27ccd8b7fa724ba3e74bb0` required the high-contrast product token; production commit `e9e7dcc5c4160a70a9c483574fb651125589392d` changed both focus paths to `--green` (`#174f35`, approximately 9.52:1 against white); regression commit `ee73bc24e80b32851dcc6f519a457ca9d5efed56` now computes the token contrast and enforces a minimum 3:1 ratio. `docs/research-traceability.md` records the W3C authority and intentionally leaves browser keyboard traversal, focus-not-obscured, zoom, screen-reader, and responsive screenshot evidence open. +The next accessibility pass established a concrete authored-focus defect rather than claiming generic WCAG conformance: `styles.css` used `#8eb59e` for generic and custom-checkbox keyboard focus outlines, approximately 2.27:1 against white. W3C WCAG 2.2 guidance ties authored focus indication to the Level AA non-text contrast requirement. Test-first commit `e5b77f1897ab13dd4f27ccd8b7fa724ba3e74bb0` required the high-contrast product token; production commit `e9e7dcc5c4160a70a9c483574fb651125589392d` changed both focus paths to `--green` (`#174f35`, approximately 9.52:1 against white); regression commit `ee73bc24e80b32851dcc6f519a457ca9d5efed56` now computes the token contrast and enforces a minimum 3:1 ratio. + +The current focus-order pass found that explicit step navigation replaced the visible editor but left focus on the triggering rail/action/warning control, weakening keyboard and assistive-technology context after the screen changed. Test-first commit `e9ff9cd49532560b28d970236d4f64fa03f21d25` specifies focus transfer for rail selection, sequential next navigation, and review-warning jumps. Production commits `960432e7557c55d13ab83106970f1e9bc93b415f` and `2ec1e10d44a04332d223e1be42a3fde9c28c7f40` add and activate a bounded focus controller that targets the newly rendered step heading and deliberately excludes ordinary editing controls and the existing preview shortcut. W3C WCAG 2.2 Focus Order evidence is now traced, but jsdom does not establish real-browser traversal, focus-not-obscured, zoom, screen-reader, or responsive screenshot conformance; those remain open and no full-WCAG claim is made. ## Current baseline @@ -26,13 +28,13 @@ The current accessibility pass established a concrete authored-focus defect rath | --- | --- | --- | --- | --- | --- | | Guided authoring | PRD, ADR-0002, seven routed editors, `getReview`, `getDraftReview`, explicit no-collection/transfer states | Repaired foundation | Selection/no-collection, mode, purpose, path and non-collection authoring responsibilities fail closed; URL/email shape checks exist, but collection-path evidence remains unstructured free text and legal sufficiency is deliberately separate | Policy Fact Authoring: preserve deterministic completeness; add structured path-evidence types only when a real integration/use case proves the need | Exact-head unit/UI edge tests, then browser E2E | | Customer-fact authority | Zero-inferred startup facts; explicit no-collection state; explicit collection/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 the same distinctions | Policy Fact Authoring: encode no-collection as a revision-owned fact, never as a fake collection item | Persistence/schema invariant tests and exact-head UI tests | -| Review workspace | Live preview, total blocker count, warning-to-owner navigation, buyer-facing readiness guidance | Implemented foundation | No browser screenshot matrix or realistic interaction evidence | UX: add Playwright + automated accessibility harness and verify action edges | Desktop/tablet/mobile screenshots, keyboard/focus and accessibility checks | -| Accessibility | Semantic controls, visible focus behavior, WCAG-traced focus token, executable >=3:1 contrast regression | Partial, CSS focus defect repaired | Browser keyboard traversal, focus-not-obscured, zoom, screen-reader and full automated WCAG 2.2 evidence remain absent | UX/Test Engineering: add axe/Playwright and manual interaction record without claiming conformance from CSS alone | Exact-head WCAG/browser matrix including no-collection transition | +| Review workspace | Live preview, total blocker count, warning-to-owner navigation, buyer-facing readiness guidance, deterministic step-heading focus transfer | Implemented foundation | No real-browser screenshot matrix or complete interaction evidence | UX: add Playwright + automated accessibility harness and verify action edges | Desktop/tablet/mobile screenshots, keyboard/focus and accessibility checks | +| Accessibility | Semantic controls, visible focus behavior, WCAG-traced focus token, executable >=3:1 contrast regression, jsdom step-focus transition regression | Partial, focus contrast and logical transition repaired | Real-browser keyboard traversal, focus-not-obscured, zoom, screen-reader and full automated WCAG 2.2 evidence remain absent | UX/Test Engineering: add axe/Playwright and manual interaction record without claiming conformance from unit/CSS evidence alone | Exact-head WCAG/browser matrix including no-collection transition | | Legal/rule traceability | `docs/research-traceability.md`; source/effective-date model | Source baseline established | Current readiness rules are product completeness only; no versioned article/section-level legal-rule engine | Legal Source Registry: map future legal validations to source/effective date/revision | Snapshot fixtures across effective dates | | Policy model | ADRs, ARCHITECTURE, TRD | Designed, browser-memory only | No versioned durable schema, migrations, audit history, 3NF implementation, or explicit UPSERT/idempotency | Platform: define `policy_revision` schema and item-level conflict contracts before storage; persist explicit no-collection state on the revision-owned fact model | Schema tests and migration round trip | | 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 | Test-first startup authority, seven-step completeness, collection-path, explicit no-collection and focus-contrast regressions plus earlier routing/responsive/SAST regressions | Improved | Browser E2E/accessibility and repository-wide 100% execution/docstring coverage are not yet evidenced | Test Engineering: measure coverage and add browser-level behavior tests | Exact-head coverage + browser evidence | +| Tests | Test-first startup authority, seven-step completeness, collection-path, explicit no-collection, focus-contrast, and logical focus-transition regressions plus earlier routing/responsive/SAST regressions | Improved | Real-browser E2E/accessibility and repository-wide 100% execution/docstring coverage are not yet evidenced | Test Engineering: measure coverage and add browser-level behavior tests | Exact-head coverage + browser 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 | Live external gate | 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 + independent approval + resolved threads | @@ -48,7 +50,7 @@ A publishable release requires complete fact-to-warning navigation, deterministi ## Active commercialization order 1. Reacquire exact-current-head CI/security/SAST/central workflow execution and independent approval; merge PR #1 only through ordinary protection. -2. Complete browser-level responsive/accessibility evidence for the fail-closed seven-step workflow, including keyboard traversal, no-collection transition, focus-not-obscured behavior, zoom/screen-reader checks and screenshots; the CSS focus-contrast defect is already repaired. +2. Complete real-browser responsive/accessibility evidence for the fail-closed seven-step workflow, including keyboard traversal, no-collection transition, focus-not-obscured behavior, zoom/screen-reader checks and screenshots; CSS focus contrast and deterministic logical step-focus transfer are already repaired. 3. Define and test versioned `policy_revision` 3NF persistence/audit, explicit no-collection storage, and item-level UPSERT contracts. 4. Implement secure review/publication with immutable releases and explicit supersession. 5. Add hosted tenant isolation, audit/incident evidence, compose deployment, and realistic performance/load tests when network services exist. From ed9f907c4ada15c560836268d266c5f510bf164e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 15:55:44 +0900 Subject: [PATCH 099/282] docs: align focus interaction architecture --- ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 9eaf9ef..3d2fa62 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. +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. 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. From 91ba89706bcaf040c9c1ef6b8e9a38c81bb77ea1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 15:57:33 +0900 Subject: [PATCH 100/282] test: start fresh workspace at first responsibility --- src/initial-workspace.test.tsx | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/initial-workspace.test.tsx diff --git a/src/initial-workspace.test.tsx b/src/initial-workspace.test.tsx new file mode 100644 index 0000000..74ac9f2 --- /dev/null +++ b/src/initial-workspace.test.tsx @@ -0,0 +1,17 @@ +// @vitest-environment jsdom +import { cleanup, render } from '@testing-library/react' +import { afterEach, describe, expect, it } from 'vitest' +import App from './App' + +afterEach(cleanup) + +describe('fresh workspace position', () => { + it('starts with the first unresolved authoring responsibility instead of marking it complete', () => { + const { container } = render() + + expect(container.querySelector('.form-panel h1')?.textContent).toBe('1. 서비스 정보') + expect(container.querySelector('.progress-copy span')?.textContent).toBe('1/7 단계') + expect(container.querySelectorAll('.rail li.done')).toHaveLength(0) + expect(container.querySelector('.rail li.active small')?.textContent).toBe('확인 및 입력') + }) +}) From a892bbd659bc26657a031b6e6e5eccf51fc08d1c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 15:59:49 +0900 Subject: [PATCH 101/282] fix: start fresh workspace at service information --- src/App.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 56a8ab2..a150f87 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -199,7 +199,7 @@ export default function App() { const [items, setItems] = useState(initialItems) const [noCollectionAttested, setNoCollectionAttested] = useState(false) const [facts, setFacts] = useState(initialFacts) - const [current, setCurrent] = useState(2) + const [current, setCurrent] = useState(1) const collectionReview = useMemo(() => getReview(items, noCollectionAttested), [items, noCollectionAttested]) const draftFindings = useMemo(() => getDraftReview(facts), [facts]) const blockingCount = collectionReview.blockingCount + draftFindings.length @@ -210,4 +210,4 @@ export default function App() {
검토 요약확인을 마친 뒤 공개 준비 상태를 확인하세요.
필수 확인 {blockingCount}건
권장 검토 {collectionReview.recommended.length}건
{message}
-} \ No newline at end of file +} From 4657b3e88bb9bab8c6c0c8a21e588bc4f0f385c4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 16:00:38 +0900 Subject: [PATCH 102/282] docs: require first unresolved step on startup --- docs/PRD.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/PRD.md b/docs/PRD.md index 99ad48d..1005df9 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -31,6 +31,7 @@ PolicyWeave는 법률 문장을 임의로 창작하는 도구가 아니다. 운 - 사용자는 일곱 작성 책임의 제품 정의 필수 사실을 누락 없이 검토할 수 있다. - 새 작업공간은 고객 운영 사실을 미리 채우지 않는다. +- 새 작업공간은 첫 번째 미확인 책임인 `서비스 정보`에서 시작하며, 사용자가 실제로 확인하지 않은 이전 단계를 완료 상태로 표시하지 않는다. - 수집 단계는 실제 수집 항목을 하나 이상 확인하거나 `개인정보를 수집하지 않음`을 명시적으로 확인해야 한다. 두 상태가 동시에 참인 데이터는 공개 준비를 통과하지 않는다. - `개인정보를 수집하지 않음`으로 전환하면 이전 수집 항목의 수집 구분·처리 목적·수집 경로를 폐기하고, 확인을 해제해도 해당 사실을 자동 복원하지 않는다. - 실제 수집 항목이 있는 경우 각 항목의 수집 구분·처리 목적·수집 경로, 서비스 이름·URL, 보유 기간, 제3자 제공 여부, 국외 이전 여부, 개인정보 보호 담당자·연락 이메일이 확인되기 전에는 공개 준비 상태를 통과하지 않는다. From 26c55ee13f4b49d37fc2b53541f4a83102c2fd81 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 16:01:10 +0900 Subject: [PATCH 103/282] docs: align fresh-workspace start contract --- docs/TRD.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/TRD.md b/docs/TRD.md index 1d204e7..8bdbc1d 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -13,7 +13,7 @@ This TRD covers the pre-release PolicyWeave browser workspace and the contracts ## Functional contracts 1. Every PRD step must route to an editable surface; selecting a step cannot change only the rail indicator. -2. A fresh workspace contains no inferred customer operational facts. Blank authoring values mean unresolved, not `none`. +2. A fresh workspace contains no inferred customer operational facts. Blank authoring values mean unresolved, not `none`. It starts at the first unresolved responsibility (`서비스 정보`) and must not mark an untouched earlier responsibility as completed merely because a later editor was shown. 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. Service name, service URL, retention period, 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. From e98cfd03b032baf19203af16849d5d0e37aaad38 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 16:01:41 +0900 Subject: [PATCH 104/282] docs: record truthful fresh-workspace progress --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3413141..01cfb88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,11 +9,12 @@ All notable product changes are recorded here. PolicyWeave is pre-release; entri - Explicit `개인정보를 수집하지 않음` operator attestation so a genuine no-collection service can complete authoring without treating an empty item list as `none`. - Warning-to-source navigation for missing collection selection/no-collection confirmation, collection mode, collection-path evidence, processing purposes, service identity, retention, transfer statuses/details, and privacy contact. - 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, explicit no-collection state and stale-item 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. +- Regression coverage for all seven step routes, zero-inferred startup facts, first-responsibility startup state, explicit no-collection state and stale-item 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. ### Changed - Fresh workspaces no longer preselect collection items or prefill collection modes/processing purposes; readiness fails closed until the operator establishes actual collection facts or explicitly attests that the service collects no personal data. +- Fresh workspaces now open at step 1, service information, rather than displaying step 2 while falsely rendering the untouched first responsibility as completed. - Empty collection selection remains unresolved rather than being interpreted as `none`; no-collection and selected-item states are mutually exclusive and contradictory state fails closed. - Turning on no-collection confirmation clears selected-item mode, purpose, and path evidence; later removing the confirmation does not silently restore stale customer facts. - Every selected collection item requires explicit collection mode, processing purpose, and nonblank collection-path evidence before readiness can pass; collection-path findings navigate back to the collection step. From 89e4571d292e36708ba6fd153435a72a9bbcf2a6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 16:02:48 +0900 Subject: [PATCH 105/282] docs: reconcile truthful startup gap --- docs/product-technical-gap-baseline.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index d4b2cd7..ab43428 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -20,21 +20,23 @@ The following commercialization pass identified a remaining buyer-flow dead end: The next accessibility pass established a concrete authored-focus defect rather than claiming generic WCAG conformance: `styles.css` used `#8eb59e` for generic and custom-checkbox keyboard focus outlines, approximately 2.27:1 against white. W3C WCAG 2.2 guidance ties authored focus indication to the Level AA non-text contrast requirement. Test-first commit `e5b77f1897ab13dd4f27ccd8b7fa724ba3e74bb0` required the high-contrast product token; production commit `e9e7dcc5c4160a70a9c483574fb651125589392d` changed both focus paths to `--green` (`#174f35`, approximately 9.52:1 against white); regression commit `ee73bc24e80b32851dcc6f519a457ca9d5efed56` now computes the token contrast and enforces a minimum 3:1 ratio. -The current focus-order pass found that explicit step navigation replaced the visible editor but left focus on the triggering rail/action/warning control, weakening keyboard and assistive-technology context after the screen changed. Test-first commit `e9ff9cd49532560b28d970236d4f64fa03f21d25` specifies focus transfer for rail selection, sequential next navigation, and review-warning jumps. Production commits `960432e7557c55d13ab83106970f1e9bc93b415f` and `2ec1e10d44a04332d223e1be42a3fde9c28c7f40` add and activate a bounded focus controller that targets the newly rendered step heading and deliberately excludes ordinary editing controls and the existing preview shortcut. W3C WCAG 2.2 Focus Order evidence is now traced, but jsdom does not establish real-browser traversal, focus-not-obscured, zoom, screen-reader, or responsive screenshot conformance; those remain open and no full-WCAG claim is made. +The focus-order pass found that explicit step navigation replaced the visible editor but left focus on the triggering rail/action/warning control, weakening keyboard and assistive-technology context after the screen changed. Test-first commit `e9ff9cd49532560b28d970236d4f64fa03f21d25` specifies focus transfer for rail selection, sequential next navigation, and review-warning jumps. Production commits `960432e7557c55d13ab83106970f1e9bc93b415f` and `2ec1e10d44a04332d223e1be42a3fde9c28c7f40` add and activate a bounded focus controller that targets the newly rendered step heading and deliberately excludes ordinary editing controls and the existing preview shortcut. W3C WCAG 2.2 Focus Order evidence is traced, but jsdom does not establish real-browser traversal, focus-not-obscured, zoom, screen-reader, or responsive screenshot conformance; those remain open and no full-WCAG claim is made. + +A subsequent buyer-flow pass found that a fresh workspace still initialized `current` to step 2. That displayed `수집 항목` before the first service-identity responsibility had been addressed and, more seriously, rendered step 1 as `입력 확인됨` even though its facts were blank. Test-first commit `91ba89706bcaf040c9c1ef6b8e9a38c81bb77ea1` requires the first editor, `1/7 단계`, zero `done` rail entries, and unresolved copy on initial render. Production commit `a892bbd659bc26657a031b6e6e5eccf51fc08d1c` changes only the initial step from 2 to 1; its GitHub commit patch confirms the only semantic production change is `useState(2)` to `useState(1)`. PRD, TRD, and CHANGELOG were reconciled through `e98cfd03b032baf19203af16849d5d0e37aaad38`. This removes a false completion signal without inventing or modifying any customer fact. ## Current baseline | Area | Evidence | Status | Commercialization gap | Owner/action | Next verification | | --- | --- | --- | --- | --- | --- | -| Guided authoring | PRD, ADR-0002, seven routed editors, `getReview`, `getDraftReview`, explicit no-collection/transfer states | Repaired foundation | Selection/no-collection, mode, purpose, path and non-collection authoring responsibilities fail closed; URL/email shape checks exist, but collection-path evidence remains unstructured free text and legal sufficiency is deliberately separate | Policy Fact Authoring: preserve deterministic completeness; add structured path-evidence types only when a real integration/use case proves the need | Exact-head unit/UI edge tests, then browser E2E | -| Customer-fact authority | Zero-inferred startup facts; explicit no-collection state; explicit collection/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 the same distinctions | Policy Fact Authoring: encode no-collection as a revision-owned fact, never as a fake collection item | Persistence/schema invariant tests and exact-head UI tests | +| Guided authoring | PRD, ADR-0002, seven routed editors, first-responsibility startup, `getReview`, `getDraftReview`, explicit no-collection/transfer states | Repaired foundation | Fresh state no longer skips or falsely completes step 1; selection/no-collection, mode, purpose, path and non-collection responsibilities fail closed; collection-path evidence remains unstructured free text and legal sufficiency is deliberately separate | Policy Fact Authoring: preserve deterministic completeness; add structured path-evidence types only when a real integration/use case proves the need | Exact-head unit/UI edge tests, then browser E2E | +| Customer-fact authority | Zero-inferred startup facts; truthful initial rail state; explicit no-collection state; explicit collection/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 the same distinctions | Policy Fact Authoring: encode no-collection as a revision-owned fact, never as a fake collection item | 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 | Implemented foundation | No real-browser screenshot matrix or complete interaction evidence | UX: add Playwright + automated accessibility harness and verify action edges | Desktop/tablet/mobile screenshots, keyboard/focus and accessibility checks | | Accessibility | Semantic controls, visible focus behavior, WCAG-traced focus token, executable >=3:1 contrast regression, jsdom step-focus transition regression | Partial, focus contrast and logical transition repaired | Real-browser keyboard traversal, focus-not-obscured, zoom, screen-reader and full automated WCAG 2.2 evidence remain absent | UX/Test Engineering: add axe/Playwright and manual interaction record without claiming conformance from unit/CSS evidence alone | Exact-head WCAG/browser matrix including no-collection transition | | Legal/rule traceability | `docs/research-traceability.md`; source/effective-date model | Source baseline established | Current readiness rules are product completeness only; no versioned article/section-level legal-rule engine | Legal Source Registry: map future legal validations to source/effective date/revision | Snapshot fixtures across effective dates | | Policy model | ADRs, ARCHITECTURE, TRD | Designed, browser-memory only | No versioned durable schema, migrations, audit history, 3NF implementation, or explicit UPSERT/idempotency | Platform: define `policy_revision` schema and item-level conflict contracts before storage; persist explicit no-collection state on the revision-owned fact model | Schema tests and migration round trip | | 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 | Test-first startup authority, seven-step completeness, collection-path, explicit no-collection, focus-contrast, and logical focus-transition regressions plus earlier routing/responsive/SAST regressions | Improved | Real-browser E2E/accessibility and repository-wide 100% execution/docstring coverage are not yet evidenced | Test Engineering: measure coverage and add browser-level behavior tests | Exact-head coverage + browser evidence | +| Tests | Test-first startup authority, truthful initial step/progress, seven-step completeness, collection-path, explicit no-collection, focus-contrast, and logical focus-transition regressions plus earlier routing/responsive/SAST regressions | Improved | Real-browser E2E/accessibility and repository-wide 100% execution/docstring coverage are not yet evidenced | Test Engineering: measure coverage and add browser-level behavior tests | Exact-head coverage + browser 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 | Live external gate | 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 + independent approval + resolved threads | @@ -46,7 +48,7 @@ Ubiquitous language: `policy_revision`, `service_profile`, explicit no-collectio The future aggregate root is the minimal `policy_revision` boundary. The no-collection fact is revision-owned state rather than a synthetic `collection_item`. Rendered prose is a read projection. `publication_revision` is immutable and explicitly superseded rather than updated. Relational persistence is 3NF by default; named persistence objects use at least two semantic words and `snake_case`, and item-level UPSERT/idempotency semantics must be explicit. ## Buyer-visible release gates -A publishable release requires complete fact-to-warning navigation, deterministic rendering of captured source facts, no inferred customer operational facts, explicit unresolved-review blocking across the seven authoring responsibilities, an explicit no-collection path for genuine no-data services, immutable reviewed publication revisions, accessible responsive interaction, authoritative source/version traceability for legal rules, hosted security/privacy evidence, and exact-head CI/security/independent review. Buyer-facing wording describes assistance and reviewability, never a compliance guarantee or an internal implementation boundary. +A publishable release requires complete fact-to-warning navigation, deterministic rendering of captured source facts, no inferred customer operational facts, truthful first-responsibility startup/progress, explicit unresolved-review blocking across the seven authoring responsibilities, an explicit no-collection path for genuine no-data services, immutable reviewed publication revisions, accessible responsive interaction, authoritative source/version traceability for legal rules, hosted security/privacy evidence, and exact-head CI/security/independent review. Buyer-facing wording describes assistance and reviewability, never a compliance guarantee or an internal implementation boundary. ## Active commercialization order 1. Reacquire exact-current-head CI/security/SAST/central workflow execution and independent approval; merge PR #1 only through ordinary protection. From 74991d5d80d5aba5eba7474d4f7e9b4b7684030c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 16:05:48 +0900 Subject: [PATCH 106/282] test: prevent navigation from faking completion --- src/initial-workspace.test.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/initial-workspace.test.tsx b/src/initial-workspace.test.tsx index 74ac9f2..415d268 100644 --- a/src/initial-workspace.test.tsx +++ b/src/initial-workspace.test.tsx @@ -1,11 +1,11 @@ // @vitest-environment jsdom -import { cleanup, render } from '@testing-library/react' +import { cleanup, fireEvent, render } from '@testing-library/react' import { afterEach, describe, expect, it } from 'vitest' import App from './App' afterEach(cleanup) -describe('fresh workspace position', () => { +describe('authoring progress truthfulness', () => { it('starts with the first unresolved authoring responsibility instead of marking it complete', () => { const { container } = render() @@ -14,4 +14,15 @@ describe('fresh workspace position', () => { expect(container.querySelectorAll('.rail li.done')).toHaveLength(0) expect(container.querySelector('.rail li.active small')?.textContent).toBe('확인 및 입력') }) + + it('does not mark unresolved responsibilities complete merely because the operator navigates past them', () => { + const { container } = render() + const railButtons = container.querySelectorAll('.rail li button') + + fireEvent.click(railButtons[6]) + + expect(container.querySelector('.form-panel h1')?.textContent).toBe('7. 개인정보 보호 담당자') + expect(container.querySelectorAll('.rail li.done')).toHaveLength(0) + expect(Array.from(container.querySelectorAll('.rail li small')).every((label) => label.textContent === '확인 및 입력')).toBe(true) + }) }) From 93487399ca7eae9fe3be89cb089c0fdf90412d0d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 16:06:09 +0900 Subject: [PATCH 107/282] test: distinguish completion from navigation --- src/initial-workspace.test.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/initial-workspace.test.tsx b/src/initial-workspace.test.tsx index 415d268..b97810d 100644 --- a/src/initial-workspace.test.tsx +++ b/src/initial-workspace.test.tsx @@ -25,4 +25,15 @@ describe('authoring progress truthfulness', () => { expect(container.querySelectorAll('.rail li.done')).toHaveLength(0) expect(Array.from(container.querySelectorAll('.rail li small')).every((label) => label.textContent === '확인 및 입력')).toBe(true) }) + + it('marks a responsibility complete only after its blocking facts are actually satisfied', () => { + const { container } = render() + fireEvent.change(container.querySelector('input[name="serviceName"]')!, { target: { value: 'Example Service' } }) + fireEvent.change(container.querySelector('input[name="serviceUrl"]')!, { target: { value: 'https://example.com' } }) + fireEvent.click(container.querySelector('.form-actions .primary')!) + + const firstStep = container.querySelectorAll('.rail li')[0] + expect(firstStep.classList.contains('done')).toBe(true) + expect(firstStep.querySelector('small')?.textContent).toBe('입력 확인됨') + }) }) From bd5b20a8da2642b974d9cdb7ed58b175c4d2615f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 16:06:34 +0900 Subject: [PATCH 108/282] test: tie progress to verified completion --- src/initial-workspace.test.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/initial-workspace.test.tsx b/src/initial-workspace.test.tsx index b97810d..38a0de1 100644 --- a/src/initial-workspace.test.tsx +++ b/src/initial-workspace.test.tsx @@ -6,22 +6,23 @@ import App from './App' afterEach(cleanup) describe('authoring progress truthfulness', () => { - it('starts with the first unresolved authoring responsibility instead of marking it complete', () => { + it('starts with the first unresolved authoring responsibility and zero completed responsibilities', () => { const { container } = render() expect(container.querySelector('.form-panel h1')?.textContent).toBe('1. 서비스 정보') - expect(container.querySelector('.progress-copy span')?.textContent).toBe('1/7 단계') + expect(container.querySelector('.progress-copy span')?.textContent).toBe('0/7 완료') expect(container.querySelectorAll('.rail li.done')).toHaveLength(0) expect(container.querySelector('.rail li.active small')?.textContent).toBe('확인 및 입력') }) - it('does not mark unresolved responsibilities complete merely because the operator navigates past them', () => { + it('does not increase completion merely because the operator navigates past unresolved responsibilities', () => { const { container } = render() const railButtons = container.querySelectorAll('.rail li button') fireEvent.click(railButtons[6]) expect(container.querySelector('.form-panel h1')?.textContent).toBe('7. 개인정보 보호 담당자') + expect(container.querySelector('.progress-copy span')?.textContent).toBe('0/7 완료') expect(container.querySelectorAll('.rail li.done')).toHaveLength(0) expect(Array.from(container.querySelectorAll('.rail li small')).every((label) => label.textContent === '확인 및 입력')).toBe(true) }) @@ -33,6 +34,7 @@ describe('authoring progress truthfulness', () => { fireEvent.click(container.querySelector('.form-actions .primary')!) const firstStep = container.querySelectorAll('.rail li')[0] + expect(container.querySelector('.progress-copy span')?.textContent).toBe('1/7 완료') expect(firstStep.classList.contains('done')).toBe(true) expect(firstStep.querySelector('small')?.textContent).toBe('입력 확인됨') }) From 08d4a238de39e7ec72e81866b83b75019d036d24 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 16:07:12 +0900 Subject: [PATCH 109/282] feat: derive completion from verified facts --- src/policy.ts | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/policy.ts b/src/policy.ts index d143750..f4121b4 100644 --- a/src/policy.ts +++ b/src/policy.ts @@ -121,4 +121,23 @@ export function getDraftReview(facts: DraftFacts): DraftFinding[] { else if (!isContactEmail(privacyOfficerEmail)) findings.push({ code: 'privacy_contact_email_format', step: 7, label: '개인정보 보호 연락 이메일 형식' }) return findings -} \ No newline at end of file +} + +/** Derives completed authoring responsibilities from the same fail-closed facts used by readiness review. */ +export function getCompletedSteps(items: PolicyItem[], noCollectionAttested: boolean, facts: DraftFacts) { + const collectionReview = getReview(items, noCollectionAttested) + const draftBlockedSteps = new Set(getDraftReview(facts).map((finding) => finding.step)) + const completed = new Set() + + if (!draftBlockedSteps.has(1)) completed.add(1) + + const collectionEstablished = !collectionReview.selectionMissing && !collectionReview.collectionContradiction + if (collectionEstablished && collectionReview.modeBlocking.length === 0 && collectionReview.pathBlocking.length === 0) completed.add(2) + if (collectionEstablished && collectionReview.blocking.length === 0) completed.add(3) + + for (const step of [4, 5, 6, 7]) { + if (!draftBlockedSteps.has(step)) completed.add(step) + } + + return completed +} From 970ec4e65e40343ac586f2a3707271d5b0b77c2e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 16:08:53 +0900 Subject: [PATCH 110/282] fix: derive progress from completed responsibilities --- src/App.tsx | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index a150f87..22f2d1e 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, getDraftReview, getReview, initialFacts, initialItems, PolicyItem, steps } from './policy' +import { DraftFacts, getCompletedSteps, getDraftReview, getReview, initialFacts, initialItems, PolicyItem, steps } from './policy' type FactField = { key: keyof DraftFacts @@ -11,17 +11,21 @@ type FactField = { visibleWhen?: { key: keyof DraftFacts; equals: string } } -/** Renders the seven-step authoring rail and keyboard-targetable preview shortcut. */ -function StepRail({ current, setCurrent }: { current: number; setCurrent: (step: number) => void }) { +/** Renders the seven-step authoring rail and completion progress derived from verified facts. */ +function StepRail({ current, completedSteps, setCurrent }: { current: number; completedSteps: Set; setCurrent: (step: number) => void }) { return } @@ -202,12 +206,13 @@ export default function App() { const [current, setCurrent] = useState(1) const collectionReview = useMemo(() => getReview(items, noCollectionAttested), [items, noCollectionAttested]) const draftFindings = useMemo(() => getDraftReview(facts), [facts]) + const completedSteps = useMemo(() => getCompletedSteps(items, noCollectionAttested, facts), [items, noCollectionAttested, facts]) const blockingCount = collectionReview.blockingCount + draftFindings.length const [message, setMessage] = useState('') function publish() { setMessage(blockingCount ? '필수 확인 항목을 먼저 입력하세요.' : '필수 확인이 완료되었습니다. 현재 검토본을 책임자와 검토하고 필요한 사실을 보완하세요.') } return
PolicyWeave{facts.serviceName || '내 서비스'} 개인정보처리방침작성 중버전 0.1.0 (임시저장) 브라우저 작업 중
-
+
검토 요약확인을 마친 뒤 공개 준비 상태를 확인하세요.
필수 확인 {blockingCount}건
권장 검토 {collectionReview.recommended.length}건
{message}
} From fd48659ed749deeab84717d5eafd29d42cc7059f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 16:09:58 +0900 Subject: [PATCH 111/282] docs: define evidence-backed authoring progress --- docs/PRD.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/PRD.md b/docs/PRD.md index 1005df9..f5d52a9 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -31,7 +31,8 @@ PolicyWeave는 법률 문장을 임의로 창작하는 도구가 아니다. 운 - 사용자는 일곱 작성 책임의 제품 정의 필수 사실을 누락 없이 검토할 수 있다. - 새 작업공간은 고객 운영 사실을 미리 채우지 않는다. -- 새 작업공간은 첫 번째 미확인 책임인 `서비스 정보`에서 시작하며, 사용자가 실제로 확인하지 않은 이전 단계를 완료 상태로 표시하지 않는다. +- 새 작업공간은 첫 번째 미확인 책임인 `서비스 정보`에서 시작한다. +- 단계 이동 자체는 작성 책임의 완료를 의미하지 않는다. 진행률, 완료 체크와 `입력 확인됨` 표시는 해당 단계의 제품 정의 차단 사실이 실제로 해소된 경우에만 readiness와 동일한 fail-closed 사실 계약에서 파생한다. - 수집 단계는 실제 수집 항목을 하나 이상 확인하거나 `개인정보를 수집하지 않음`을 명시적으로 확인해야 한다. 두 상태가 동시에 참인 데이터는 공개 준비를 통과하지 않는다. - `개인정보를 수집하지 않음`으로 전환하면 이전 수집 항목의 수집 구분·처리 목적·수집 경로를 폐기하고, 확인을 해제해도 해당 사실을 자동 복원하지 않는다. - 실제 수집 항목이 있는 경우 각 항목의 수집 구분·처리 목적·수집 경로, 서비스 이름·URL, 보유 기간, 제3자 제공 여부, 국외 이전 여부, 개인정보 보호 담당자·연락 이메일이 확인되기 전에는 공개 준비 상태를 통과하지 않는다. From 01ff0548b2b89bd65d0478f2499dd7a9fb7a6f4d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 20:50:09 +0900 Subject: [PATCH 112/282] test: make no-collection retention inapplicable --- src/no-collection.test.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/no-collection.test.tsx b/src/no-collection.test.tsx index 138cee2..e7bc48c 100644 --- a/src/no-collection.test.tsx +++ b/src/no-collection.test.tsx @@ -22,6 +22,17 @@ describe('explicit no-collection attestation', () => { expect(review.blockingCount).toBe(1) }) + it('treats retention as not applicable instead of forcing an invented retention fact', () => { + const { container } = render() + fireEvent.click(container.querySelectorAll('.rail li button')[1]) + fireEvent.click(container.querySelector('input[name="noCollectionAttested"]')!) + + expect(container.querySelector('.review-stat.blocking b')?.textContent).toBe('6건') + expect(container.querySelectorAll('.rail li')[3].classList.contains('done')).toBe(true) + expect(container.querySelector('.paper')?.textContent).toContain('개인정보를 수집하지 않음으로 확인되어 보유 기간이 적용되지 않습니다.') + expect(container.querySelector('.paper')?.textContent).not.toContain('보유 기간 단계에서 확인한 운영 기준을 입력해야 합니다.') + }) + it('invalidates stale item facts when the operator attests no collection and does not revive them when the attestation is removed', () => { const { container } = render() const phone = container.querySelectorAll('.check-label input')[2] @@ -38,7 +49,7 @@ describe('explicit no-collection attestation', () => { expect(noCollection).not.toBeNull() fireEvent.click(noCollection) expect(container.querySelectorAll('.check-label input')[2].checked).toBe(false) - expect(container.querySelector('.review-stat.blocking b')?.textContent).toBe('7건') + expect(container.querySelector('.review-stat.blocking b')?.textContent).toBe('6건') fireEvent.click(noCollection) expect(container.querySelector('.review-stat.blocking b')?.textContent).toBe('8건') From 5ad05c6a9f51aece33672bc9b1e1dc5dfbe3ccf3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 20:51:24 +0900 Subject: [PATCH 113/282] fix: make no-collection retention inapplicable --- src/policy.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/policy.ts b/src/policy.ts index f4121b4..0bf5920 100644 --- a/src/policy.ts +++ b/src/policy.ts @@ -87,8 +87,8 @@ export function getReview(items: PolicyItem[], noCollectionAttested = false) { return { enabled, blocking, modeBlocking, pathBlocking, selectionMissing, collectionContradiction, noCollectionAttested, blockingCount, recommended } } -/** Derives authoring-completeness findings for the non-collection responsibilities in the seven-step workflow. */ -export function getDraftReview(facts: DraftFacts): DraftFinding[] { +/** Derives non-collection authoring findings, treating retention as inapplicable only after an explicit no-collection attestation. */ +export function getDraftReview(facts: DraftFacts, noCollectionAttested = false): DraftFinding[] { const findings: DraftFinding[] = [] const addWhenBlank = (value: string, code: string, step: number, label: string) => { if (!value.trim()) findings.push({ code, step, label }) @@ -99,7 +99,7 @@ export function getDraftReview(facts: DraftFacts): DraftFinding[] { if (!serviceUrl) findings.push({ code: 'service_url', step: 1, label: '서비스 URL' }) else if (!isWebServiceUrl(serviceUrl)) findings.push({ code: 'service_url_format', step: 1, label: '서비스 URL 형식' }) - addWhenBlank(facts.retentionPeriod, 'retention_period', 4, '보유 기간') + if (!noCollectionAttested) addWhenBlank(facts.retentionPeriod, 'retention_period', 4, '보유 기간') if (!facts.thirdPartyStatus) { findings.push({ code: 'third_party_status', step: 5, label: '제3자 제공 여부' }) @@ -126,7 +126,7 @@ export function getDraftReview(facts: DraftFacts): DraftFinding[] { /** Derives completed authoring responsibilities from the same fail-closed facts used by readiness review. */ export function getCompletedSteps(items: PolicyItem[], noCollectionAttested: boolean, facts: DraftFacts) { const collectionReview = getReview(items, noCollectionAttested) - const draftBlockedSteps = new Set(getDraftReview(facts).map((finding) => finding.step)) + const draftBlockedSteps = new Set(getDraftReview(facts, noCollectionAttested).map((finding) => finding.step)) const completed = new Set() if (!draftBlockedSteps.has(1)) completed.add(1) From fc35f8c1b597ad6b556e1fa3abfef466d80ac4e9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 20:52:40 +0900 Subject: [PATCH 114/282] fix: project no-collection retention truthfully --- src/App.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 22f2d1e..114140e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -168,7 +168,7 @@ function EditingPanel({ current, items, setItems, noCollectionAttested, setNoCol /** Projects verified authoring facts and deterministic readiness findings into the review draft. */ function DocumentPreview({ items, noCollectionAttested, facts, setCurrent }: { items: PolicyItem[]; noCollectionAttested: boolean; facts: DraftFacts; setCurrent: (step: number) => void }) { const review = useMemo(() => getReview(items, noCollectionAttested), [items, noCollectionAttested]) - const draftFindings = useMemo(() => getDraftReview(facts), [facts]) + const draftFindings = useMemo(() => getDraftReview(facts, noCollectionAttested), [facts, noCollectionAttested]) const blockingCount = review.blockingCount + draftFindings.length return

개인정보처리방침 미리보기

@@ -189,7 +189,7 @@ function DocumentPreview({ items, noCollectionAttested, facts, setCurrent }: { i {review.pathBlocking.length > 0 &&
공개 전 확인{review.pathBlocking.map((item) => item.label).join(', ')}의 수집 경로를 확인해야 합니다.
} {review.blocking.length > 0 &&
공개 전 확인{review.blocking.map((item) => item.label).join(', ')}의 처리 목적이 입력되지 않았습니다.
} {draftFindings.map((finding) =>
공개 전 확인{finding.label} 확인이 필요합니다.
)} -

제2조 (처리 및 보유 기간)

{facts.retentionPeriod || '보유 기간 단계에서 확인한 운영 기준을 입력해야 합니다.'}

+

제2조 (처리 및 보유 기간)

{noCollectionAttested ? '개인정보를 수집하지 않음으로 확인되어 보유 기간이 적용되지 않습니다.' : facts.retentionPeriod || '보유 기간 단계에서 확인한 운영 기준을 입력해야 합니다.'}

제3조 (제3자 제공)

{facts.thirdPartyStatus === 'no' ? '제3자 제공 없음으로 확인되었습니다.' : facts.thirdPartyStatus === 'yes' ? `${facts.thirdPartyRecipient || '제공받는 자 확인 필요'}에 ${facts.thirdPartyPurpose || '제공 목적 확인 필요'}으로 제공하는 흐름을 검토 중입니다.` : '제3자 제공 여부를 확인하는 단계가 남아 있습니다.'}

제4조 (국외 이전)

{facts.internationalStatus === 'no' ? '국외 이전 없음으로 확인되었습니다.' : facts.internationalStatus === 'yes' ? `${facts.internationalCountry || '국가 확인 필요'} · ${facts.internationalRecipient || '수령자 확인 필요'}` : '국외 이전 여부를 확인하는 단계가 남아 있습니다.'}

개인정보 보호 문의

{facts.privacyOfficerName || '담당자 확인 필요'} · {facts.privacyOfficerEmail || '연락처 확인 필요'}

@@ -205,7 +205,7 @@ export default function App() { const [facts, setFacts] = useState(initialFacts) const [current, setCurrent] = useState(1) const collectionReview = useMemo(() => getReview(items, noCollectionAttested), [items, noCollectionAttested]) - const draftFindings = useMemo(() => getDraftReview(facts), [facts]) + const draftFindings = useMemo(() => getDraftReview(facts, noCollectionAttested), [facts, noCollectionAttested]) const completedSteps = useMemo(() => getCompletedSteps(items, noCollectionAttested, facts), [items, noCollectionAttested, facts]) const blockingCount = collectionReview.blockingCount + draftFindings.length const [message, setMessage] = useState('') From ddd24eae92d1625f91bc41614dc2f977b848eb50 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 20:54:51 +0900 Subject: [PATCH 115/282] docs: define no-collection retention applicability --- docs/PRD.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/PRD.md b/docs/PRD.md index f5d52a9..5403aca 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -6,7 +6,7 @@ ## 제품 원칙 -PolicyWeave는 법률 문장을 임의로 창작하는 도구가 아니다. 운영자가 입력한 사실을 구조화하고, 적용 근거와 확인이 필요한 부분을 분리해 검토본을 만든다. 자동 생성 결과는 변호사·개인정보보호책임자의 검토를 대체하지 않는다. 새 작업공간은 고객의 실제 수집 여부, 수집 구분, 처리 목적, 수집 경로, 제3자 제공 여부 또는 국외 이전 여부를 추정하지 않는다. 비어 있는 값은 `없음`이 아니라 `미확인`이다. 실제 개인정보를 수집하지 않는 서비스는 운영자가 이를 명시적으로 확인할 수 있어야 하며, 그 확인은 빈 항목 목록에서 추론되지 않는다. +PolicyWeave는 법률 문장을 임의로 창작하는 도구가 아니다. 운영자가 입력한 사실을 구조화하고, 적용 근거와 확인이 필요한 부분을 분리해 검토본을 만든다. 자동 생성 결과는 변호사·개인정보보호책임자의 검토를 대체하지 않는다. 새 작업공간은 고객의 실제 수집 여부, 수집 구분, 처리 목적, 수집 경로, 제3자 제공 여부 또는 국외 이전 여부를 추정하지 않는다. 비어 있는 값은 `없음`이 아니라 `미확인`이다. 실제 개인정보를 수집하지 않는 서비스는 운영자가 이를 명시적으로 확인할 수 있어야 하며, 그 확인은 빈 항목 목록에서 추론되지 않는다. 명시적 no-collection 상태에서는 수집된 개인정보가 전제인 보유 기간을 임의로 만들어 요구하지 않고 `적용되지 않음`으로 투영한다. ## MVP @@ -35,6 +35,7 @@ PolicyWeave는 법률 문장을 임의로 창작하는 도구가 아니다. 운 - 단계 이동 자체는 작성 책임의 완료를 의미하지 않는다. 진행률, 완료 체크와 `입력 확인됨` 표시는 해당 단계의 제품 정의 차단 사실이 실제로 해소된 경우에만 readiness와 동일한 fail-closed 사실 계약에서 파생한다. - 수집 단계는 실제 수집 항목을 하나 이상 확인하거나 `개인정보를 수집하지 않음`을 명시적으로 확인해야 한다. 두 상태가 동시에 참인 데이터는 공개 준비를 통과하지 않는다. - `개인정보를 수집하지 않음`으로 전환하면 이전 수집 항목의 수집 구분·처리 목적·수집 경로를 폐기하고, 확인을 해제해도 해당 사실을 자동 복원하지 않는다. +- `개인정보를 수집하지 않음`이 명시적으로 확인된 동안에는 보유 기간 입력을 완료 조건으로 강제하거나 빈 보유 기간을 `미확인`으로 경고하지 않는다. 검토본은 보유 기간을 추정하지 않고 명시적으로 `적용되지 않음`으로 표시한다. - 실제 수집 항목이 있는 경우 각 항목의 수집 구분·처리 목적·수집 경로, 서비스 이름·URL, 보유 기간, 제3자 제공 여부, 국외 이전 여부, 개인정보 보호 담당자·연락 이메일이 확인되기 전에는 공개 준비 상태를 통과하지 않는다. - 서비스 URL은 절대 HTTP(S) 주소 형식을 만족해야 하고, 개인정보 보호 연락 이메일은 최소 주소 형식 검사를 통과해야 한다. 이 검사는 서비스 존재나 메일함 소유권을 주장하지 않는다. - 제3자 제공 또는 국외 이전을 `있음`으로 확인한 경우 해당 수령자/목적 또는 국가/수령자 사실까지 확인되어야 한다. From 773ecb3b26ce83144051a5122692bd3e6052e0ad Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 20:55:21 +0900 Subject: [PATCH 116/282] docs: align retention applicability contract --- docs/TRD.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/TRD.md b/docs/TRD.md index 8bdbc1d..03ce768 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. -- `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, retention, transfer statuses/details, and privacy contact. +- `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, retention applicability, transfer statuses/details, and privacy contact. - `src/App.tsx` provides the seven-step authoring flow, review navigation, explicit no-collection and 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; ordinary form controls and the dedicated preview shortcut are outside this behavior. - The current CI contract is lint, Vitest, and TypeScript/Vite build plus live organization-required security/review workflows. @@ -16,13 +16,13 @@ This TRD covers the pre-release PolicyWeave browser workspace and the contracts 2. A fresh workspace contains no inferred customer operational facts. Blank authoring values mean unresolved, not `none`. It starts at the first unresolved responsibility (`서비스 정보`) and must not mark an untouched earlier responsibility as completed merely because a later editor was shown. 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. Service name, service URL, retention period, 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. +5. 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. Retention period is additionally required whenever actual collection items exist. When the operator explicitly attests no collection, retention is inapplicable rather than unresolved: no retention value is inferred, the retention responsibility is non-blocking, and the preview projects that explicit applicability state. 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. 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. -7. Blank/whitespace authoring facts are normalized as unresolved. +7. Blank/whitespace authoring facts are normalized as unresolved unless a governing explicit fact makes that responsibility inapplicable, as with retention under no-collection. 8. Disabling a collection item invalidates dependent collection-mode, processing-purpose, and collection-path evidence; re-enabling requires renewed confirmation. 9. Every blocking finding links to the responsible editing step. 10. Explicit step navigation through the rail, previous/next controls, or a blocking-finding action transfers focus to the newly active step heading after that surface renders. This logical focus-order contract must not steal focus from ordinary editing controls or the dedicated preview shortcut. -11. Preview text is derived from current structured facts, including the explicit no-collection attestation; it does not become an independently editable authority. +11. Preview text is derived from current structured facts, including the explicit no-collection attestation and its retention-applicability consequence; it does not become an independently editable authority. 12. UI copy distinguishes a review draft from legal advice, certification, or a compliance guarantee. These are authoring-completeness contracts, not claims that the resulting policy is legally sufficient. Legal sufficiency remains a separate versioned-rule responsibility backed by authoritative source evidence. @@ -36,7 +36,7 @@ These are authoring-completeness contracts, not claims that the resulting policy - Production does not depend on synthetic demo data. ## 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. +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 state and retention applicability without manufacturing a `retention_rule` or synthetic collection item. A publication command must operate on a specific reviewed `policy_revision` and create a new immutable `publication_revision`. It must fail closed when blocking findings, incompatible rule/template versions, missing source receipts, or missing authorization exist. Publication never mutates foreign customer databases. From b3ab6b44ad935448b23aa2d0f854d7793740c2c6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 20:55:56 +0900 Subject: [PATCH 117/282] docs: record truthful no-collection retention --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01cfb88..26a8860 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ All notable product changes are recorded here. PolicyWeave is pre-release; entri - Explicit `개인정보를 수집하지 않음` operator attestation so a genuine no-collection service can complete authoring without treating an empty item list as `none`. - Warning-to-source navigation for missing collection selection/no-collection confirmation, collection mode, collection-path evidence, processing purposes, service identity, retention, transfer statuses/details, and privacy contact. - 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, 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. +- Regression coverage for all seven step routes, zero-inferred startup facts, first-responsibility startup state, explicit no-collection state and stale-item invalidation, no-collection retention applicability, 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. ### Changed @@ -17,8 +17,9 @@ All notable product changes are recorded here. PolicyWeave is pre-release; entri - Fresh workspaces now open at step 1, service information, rather than displaying step 2 while falsely rendering the untouched first responsibility as completed. - Empty collection selection remains unresolved rather than being interpreted as `none`; no-collection and selected-item states are mutually exclusive and contradictory state fails closed. - Turning on no-collection confirmation clears selected-item mode, purpose, and path evidence; later removing the confirmation does not silently restore stale customer facts. +- Explicit no-collection confirmation now makes the retention responsibility inapplicable instead of forcing the operator to invent a retention period; the review draft projects that applicability state without synthesizing a retention value. - Every selected collection item requires explicit collection mode, processing purpose, and nonblank collection-path evidence before readiness can pass; collection-path findings navigate back to the collection step. -- Public-readiness includes product-defined service name/URL, retention, transfer-status/detail, and privacy-contact completeness rather than allowing the CTA to enable while those steps remain visibly unresolved. +- Public-readiness includes product-defined service name/URL, transfer-status/detail, and privacy-contact completeness; retention completeness is additionally required whenever actual collection items exist rather than when no-collection has been explicitly established. - Service URL and privacy-contact email are shape-validated as usability contracts without claiming endpoint reachability or mailbox ownership. - 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. From 1d32e78980eb1c72eb00a2753b3b2dc10f3c85de Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 20:56:49 +0900 Subject: [PATCH 118/282] docs: reconcile no-collection retention gap --- docs/product-technical-gap-baseline.md | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index ab43428..ce7a052 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -18,41 +18,45 @@ A concurrent writer then established the missing collection-path readiness contr The following commercialization pass identified a remaining buyer-flow dead end: a service that genuinely collects no personal data could never clear collection readiness because an empty item selection was always unresolved. Interpreting emptiness as `none` would have reintroduced inference. Test-first commit `97063fc34fbc0e6db2652922f78bc74c00e1ef93` specifies an explicit no-collection attestation, contradiction fail-closed behavior, and stale-item invalidation. Production commit `e73eb83a913597fb05ddea97519d549d58007faf` adds the operator attestation, makes it mutually exclusive with selected items, clears item mode/purpose/path facts when enabled, prevents their later silent revival, and projects the explicit negative fact into the review draft. This is a product authoring-completeness state, not a new legal conclusion. +A later exact-head pass found that the same explicit no-collection path still forced a nonblank retention period. That contract made a genuine no-data service invent a retention fact to reach readiness, contradicting the product's no-inference rule. Test-first commit `01ff0548b2b89bd65d0478f2499dd7a9fb7a6f4d` requires no-collection to reduce the unresolved blocker count, mark the retention responsibility non-blocking, and project an explicit `적용되지 않음` statement without synthesizing a retention value. Domain repair `5ad05c6a9f51aece33672bc9b1e1dc5dfbe3ccf3` makes `getDraftReview` retention-aware only from the explicit no-collection attestation, while collecting services still require retention. UI projection repair `fc35f8c1b597ad6b556e1fa3abfef466d80ac4e9` propagates the same contract into readiness and the review draft. PRD/TRD/CHANGELOG then reconcile the behavior at `ddd24eae92d1625f91bc41614dc2f977b848eb50`, `773ecb3b26ce83144051a5122692bd3e6052e0ad`, and `b3ab6b44ad935448b23aa2d0f854d7793740c2c6`. No legal sufficiency claim is added: this is a product-level applicability rule derived solely from the operator's explicit no-collection fact. + The next accessibility pass established a concrete authored-focus defect rather than claiming generic WCAG conformance: `styles.css` used `#8eb59e` for generic and custom-checkbox keyboard focus outlines, approximately 2.27:1 against white. W3C WCAG 2.2 guidance ties authored focus indication to the Level AA non-text contrast requirement. Test-first commit `e5b77f1897ab13dd4f27ccd8b7fa724ba3e74bb0` required the high-contrast product token; production commit `e9e7dcc5c4160a70a9c483574fb651125589392d` changed both focus paths to `--green` (`#174f35`, approximately 9.52:1 against white); regression commit `ee73bc24e80b32851dcc6f519a457ca9d5efed56` now computes the token contrast and enforces a minimum 3:1 ratio. The focus-order pass found that explicit step navigation replaced the visible editor but left focus on the triggering rail/action/warning control, weakening keyboard and assistive-technology context after the screen changed. Test-first commit `e9ff9cd49532560b28d970236d4f64fa03f21d25` specifies focus transfer for rail selection, sequential next navigation, and review-warning jumps. Production commits `960432e7557c55d13ab83106970f1e9bc93b415f` and `2ec1e10d44a04332d223e1be42a3fde9c28c7f40` add and activate a bounded focus controller that targets the newly rendered step heading and deliberately excludes ordinary editing controls and the existing preview shortcut. W3C WCAG 2.2 Focus Order evidence is traced, but jsdom does not establish real-browser traversal, focus-not-obscured, zoom, screen-reader, or responsive screenshot conformance; those remain open and no full-WCAG claim is made. A subsequent buyer-flow pass found that a fresh workspace still initialized `current` to step 2. That displayed `수집 항목` before the first service-identity responsibility had been addressed and, more seriously, rendered step 1 as `입력 확인됨` even though its facts were blank. Test-first commit `91ba89706bcaf040c9c1ef6b8e9a38c81bb77ea1` requires the first editor, `1/7 단계`, zero `done` rail entries, and unresolved copy on initial render. Production commit `a892bbd659bc26657a031b6e6e5eccf51fc08d1c` changes only the initial step from 2 to 1; its GitHub commit patch confirms the only semantic production change is `useState(2)` to `useState(1)`. PRD, TRD, and CHANGELOG were reconciled through `e98cfd03b032baf19203af16849d5d0e37aaad38`. This removes a false completion signal without inventing or modifying any customer fact. +A concurrent writer then repaired completion/progress semantics so navigation cannot make untouched responsibilities appear complete. The branch advanced without rewriting predecessor history; completion is now derived from the same verified-fact contract used by readiness rather than from the currently visited step. This evidence is preserved on the canonical `develop` writer branch and must remain part of final exact-head verification. + ## Current baseline | Area | Evidence | Status | Commercialization gap | Owner/action | Next verification | | --- | --- | --- | --- | --- | --- | -| Guided authoring | PRD, ADR-0002, seven routed editors, first-responsibility startup, `getReview`, `getDraftReview`, explicit no-collection/transfer states | Repaired foundation | Fresh state no longer skips or falsely completes step 1; selection/no-collection, mode, purpose, path and non-collection responsibilities fail closed; collection-path evidence remains unstructured free text and legal sufficiency is deliberately separate | Policy Fact Authoring: preserve deterministic completeness; add structured path-evidence types only when a real integration/use case proves the need | Exact-head unit/UI edge tests, then browser E2E | -| Customer-fact authority | Zero-inferred startup facts; truthful initial rail state; explicit no-collection state; explicit collection/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 the same distinctions | Policy Fact Authoring: encode no-collection as a revision-owned fact, never as a fake collection item | Persistence/schema invariant tests and exact-head UI tests | +| Guided authoring | PRD, ADR-0002, seven routed editors, first-responsibility startup, `getReview`, retention-aware `getDraftReview`, explicit no-collection/transfer states | Repaired foundation | Fresh state no longer skips or falsely completes step 1; selection/no-collection, mode, purpose, path and non-collection responsibilities fail closed; no-collection makes retention explicitly inapplicable rather than fabricated; collection-path evidence remains unstructured free text and legal sufficiency is deliberately separate | Policy Fact Authoring: preserve deterministic completeness/applicability; add structured path-evidence types only when a real integration/use case proves the need | Exact-head unit/UI edge tests, then browser E2E | +| Customer-fact authority | Zero-inferred startup facts; truthful initial rail state; explicit no-collection state; no-collection retention applicability; explicit collection/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 explicit applicability instead of manufacturing retention data | Policy Fact Authoring: encode no-collection as a revision-owned fact and derive retention applicability from it, never as a fake collection item or invented 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 | Implemented foundation | No real-browser screenshot matrix or complete interaction evidence | UX: add Playwright + automated accessibility harness and verify action edges | Desktop/tablet/mobile screenshots, keyboard/focus and accessibility checks | | Accessibility | Semantic controls, visible focus behavior, WCAG-traced focus token, executable >=3:1 contrast regression, jsdom step-focus transition regression | Partial, focus contrast and logical transition repaired | Real-browser keyboard traversal, focus-not-obscured, zoom, screen-reader and full automated WCAG 2.2 evidence remain absent | UX/Test Engineering: add axe/Playwright and manual interaction record without claiming conformance from unit/CSS evidence alone | Exact-head WCAG/browser matrix including no-collection transition | -| Legal/rule traceability | `docs/research-traceability.md`; source/effective-date model | Source baseline established | Current readiness rules are product completeness only; no versioned article/section-level legal-rule engine | Legal Source Registry: map future legal validations to source/effective date/revision | Snapshot fixtures across effective dates | -| Policy model | ADRs, ARCHITECTURE, TRD | Designed, browser-memory only | No versioned durable schema, migrations, audit history, 3NF implementation, or explicit UPSERT/idempotency | Platform: define `policy_revision` schema and item-level conflict contracts before storage; persist explicit no-collection state on the revision-owned fact model | Schema tests and migration round trip | +| Legal/rule traceability | `docs/research-traceability.md`; source/effective-date model | Source baseline established | Current readiness rules are product completeness/applicability only; no versioned article/section-level legal-rule engine | Legal Source Registry: map future legal validations to source/effective date/revision | Snapshot fixtures across effective dates | +| Policy model | ADRs, ARCHITECTURE, TRD | Designed, browser-memory only | No versioned durable schema, migrations, audit history, 3NF implementation, or explicit UPSERT/idempotency | Platform: define `policy_revision` schema and item-level conflict contracts before storage; persist explicit no-collection state and retention applicability on the revision-owned fact model | Schema tests and migration round trip | | 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 | Test-first startup authority, truthful initial step/progress, seven-step completeness, collection-path, explicit no-collection, focus-contrast, and logical focus-transition regressions plus earlier routing/responsive/SAST regressions | Improved | Real-browser E2E/accessibility and repository-wide 100% execution/docstring coverage are not yet evidenced | Test Engineering: measure coverage and add browser-level behavior tests | Exact-head coverage + browser evidence | +| Tests | Test-first startup authority, truthful initial step/progress, seven-step completeness, collection-path, explicit no-collection, no-collection retention applicability, focus-contrast, and logical focus-transition regressions plus earlier routing/responsive/SAST regressions | Improved | Real-browser E2E/accessibility and repository-wide 100% execution/docstring coverage are not yet evidenced | Test Engineering: measure coverage and add browser-level behavior tests | Exact-head coverage + browser 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 | Live external gate | 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 + independent approval + resolved threads | ## DDD/context map Core subdomain: **Policy Fact Authoring**. Supporting: **Review & Publication**, **Legal Source Registry**. Generic infrastructure remains outside domain authority. `Policy Fact Authoring -> Review & Publication` supplies a versioned fact contract. `Legal Source Registry -> Review & Publication` supplies versioned source/rule evidence through an ACL. Customer/source-system integrations remain adapters and may not mutate foreign systems through hidden coupling. -Ubiquitous language: `policy_revision`, `service_profile`, explicit no-collection attestation, `collection_item`, `processing_purpose`, `retention_rule`, `third_party_transfer`, `international_transfer`, `privacy_contact`, `review_finding`, `legal_source_revision`, `publication_revision`. +Ubiquitous language: `policy_revision`, `service_profile`, explicit no-collection attestation, retention applicability, `collection_item`, `processing_purpose`, `retention_rule`, `third_party_transfer`, `international_transfer`, `privacy_contact`, `review_finding`, `legal_source_revision`, `publication_revision`. -The future aggregate root is the minimal `policy_revision` boundary. The no-collection fact is revision-owned state rather than a synthetic `collection_item`. Rendered prose is a read projection. `publication_revision` is immutable and explicitly superseded rather than updated. Relational persistence is 3NF by default; named persistence objects use at least two semantic words and `snake_case`, and item-level UPSERT/idempotency semantics must be explicit. +The future aggregate root is the minimal `policy_revision` boundary. The no-collection fact is revision-owned state rather than a synthetic `collection_item`; retention applicability is derived from that explicit fact, and no `retention_rule` is created for an inapplicable responsibility. Rendered prose is a read projection. `publication_revision` is immutable and explicitly superseded rather than updated. Relational persistence is 3NF by default; named persistence objects use at least two semantic words and `snake_case`, and item-level UPSERT/idempotency semantics must be explicit. ## Buyer-visible release gates -A publishable release requires complete fact-to-warning navigation, deterministic rendering of captured source facts, no inferred customer operational facts, truthful first-responsibility startup/progress, explicit unresolved-review blocking across the seven authoring responsibilities, an explicit no-collection path for genuine no-data services, immutable reviewed publication revisions, accessible responsive interaction, authoritative source/version traceability for legal rules, hosted security/privacy evidence, and exact-head CI/security/independent review. Buyer-facing wording describes assistance and reviewability, never a compliance guarantee or an internal implementation boundary. +A publishable release requires complete fact-to-warning navigation, deterministic rendering of captured source facts, no inferred customer operational facts, truthful first-responsibility startup/progress, explicit unresolved-review blocking across the seven authoring responsibilities, an explicit no-collection path for genuine no-data services that does not fabricate retention data, immutable reviewed publication revisions, accessible responsive interaction, authoritative source/version traceability for legal rules, hosted security/privacy evidence, and exact-head CI/security/independent review. Buyer-facing wording describes assistance and reviewability, never a compliance guarantee or an internal implementation boundary. ## Active commercialization order 1. Reacquire exact-current-head CI/security/SAST/central workflow execution and independent approval; merge PR #1 only through ordinary protection. 2. Complete real-browser responsive/accessibility evidence for the fail-closed seven-step workflow, including keyboard traversal, no-collection transition, focus-not-obscured behavior, zoom/screen-reader checks and screenshots; CSS focus contrast and deterministic logical step-focus transfer are already repaired. -3. Define and test versioned `policy_revision` 3NF persistence/audit, explicit no-collection storage, and item-level UPSERT contracts. +3. Define and test versioned `policy_revision` 3NF persistence/audit, explicit no-collection storage, retention applicability, and item-level UPSERT contracts. 4. Implement secure review/publication with immutable releases and explicit supersession. 5. Add hosted tenant isolation, audit/incident evidence, compose deployment, and realistic performance/load tests when network services exist. From 734a3d603761fd851f7d8dfc9434d92626518de6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 20:58:47 +0900 Subject: [PATCH 119/282] test: require explicit retention applicability --- src/policy.test.ts | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/src/policy.test.ts b/src/policy.test.ts index 9cbc3b6..47b08a2 100644 --- a/src/policy.test.ts +++ b/src/policy.test.ts @@ -54,7 +54,7 @@ describe('seven-step draft readiness', () => { expect(getDraftReview(initialFacts).map((finding) => finding.code)).toEqual([ 'service_name', 'service_url', - 'retention_period', + 'retention_status', 'third_party_status', 'international_status', 'privacy_contact_name', @@ -62,11 +62,45 @@ describe('seven-step draft readiness', () => { ]) }) + it('does not infer retention applicability from no-collection', () => { + expect(getDraftReview(initialFacts, true).map((finding) => finding.code)).toContain('retention_status') + }) + + it('accepts an explicit no-retention fact without inventing a retention period', () => { + const facts = { + ...initialFacts, + serviceName: '예시 서비스', + serviceUrl: 'https://example.test', + retentionStatus: 'none' as const, + thirdPartyStatus: 'no' as const, + internationalStatus: 'no' as const, + privacyOfficerName: '개인정보보호 담당', + privacyOfficerEmail: 'privacy@example.test', + } + expect(getDraftReview(facts, true)).toEqual([]) + }) + + it('requires a retention period only after the operator confirms retained personal data exists', () => { + const facts = { + ...initialFacts, + serviceName: '예시 서비스', + serviceUrl: 'https://example.test', + retentionStatus: 'applies' as const, + thirdPartyStatus: 'no' as const, + internationalStatus: 'no' as const, + privacyOfficerName: '개인정보보호 담당', + privacyOfficerEmail: 'privacy@example.test', + } + expect(getDraftReview(facts).map((finding) => finding.code)).toEqual(['retention_period']) + expect(getDraftReview({ ...facts, retentionPeriod: '회원 탈퇴 시까지' })).toEqual([]) + }) + it('treats explicit no-transfer attestations as complete without inventing recipients', () => { const facts = { ...initialFacts, serviceName: '예시 서비스', serviceUrl: 'https://example.test', + retentionStatus: 'applies' as const, retentionPeriod: '회원 탈퇴 시까지', thirdPartyStatus: 'no' as const, internationalStatus: 'no' as const, @@ -81,6 +115,7 @@ describe('seven-step draft readiness', () => { ...initialFacts, serviceName: '예시 서비스', serviceUrl: 'https://example.test', + retentionStatus: 'applies' as const, retentionPeriod: '회원 탈퇴 시까지', thirdPartyStatus: 'yes' as const, internationalStatus: 'yes' as const, @@ -100,6 +135,7 @@ describe('seven-step draft readiness', () => { ...initialFacts, serviceName: ' ', serviceUrl: '\t', + retentionStatus: 'applies' as const, retentionPeriod: '\n', thirdPartyStatus: 'no' as const, internationalStatus: 'no' as const, @@ -119,6 +155,7 @@ describe('seven-step draft readiness', () => { const base = { ...initialFacts, serviceName: '예시 서비스', + retentionStatus: 'applies' as const, retentionPeriod: '회원 탈퇴 시까지', thirdPartyStatus: 'no' as const, internationalStatus: 'no' as const, @@ -135,6 +172,7 @@ describe('seven-step draft readiness', () => { ...initialFacts, serviceName: '예시 서비스', serviceUrl: 'https://example.test', + retentionStatus: 'applies' as const, retentionPeriod: '회원 탈퇴 시까지', thirdPartyStatus: 'no' as const, internationalStatus: 'no' as const, From dc4f9ad1079f90efb27165099f5dce13ddb4f637 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 20:59:45 +0900 Subject: [PATCH 120/282] test: separate retention authority from collection --- src/no-collection.test.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/no-collection.test.tsx b/src/no-collection.test.tsx index e7bc48c..5ab3cb4 100644 --- a/src/no-collection.test.tsx +++ b/src/no-collection.test.tsx @@ -22,15 +22,21 @@ describe('explicit no-collection attestation', () => { expect(review.blockingCount).toBe(1) }) - it('treats retention as not applicable instead of forcing an invented retention fact', () => { + it('does not infer retention from no-collection and accepts a separate explicit no-retention fact', () => { const { container } = render() fireEvent.click(container.querySelectorAll('.rail li button')[1]) fireEvent.click(container.querySelector('input[name="noCollectionAttested"]')!) + expect(container.querySelector('.review-stat.blocking b')?.textContent).toBe('7건') + expect(container.querySelectorAll('.rail li')[3].classList.contains('done')).toBe(false) + expect(container.querySelector('.paper')?.textContent).toContain('보유 여부 및 기간을 확인해야 합니다.') + + fireEvent.click(container.querySelectorAll('.rail li button')[3]) + fireEvent.change(container.querySelector('select[name="retentionStatus"]')!, { target: { value: 'none' } }) + expect(container.querySelector('.review-stat.blocking b')?.textContent).toBe('6건') expect(container.querySelectorAll('.rail li')[3].classList.contains('done')).toBe(true) - expect(container.querySelector('.paper')?.textContent).toContain('개인정보를 수집하지 않음으로 확인되어 보유 기간이 적용되지 않습니다.') - expect(container.querySelector('.paper')?.textContent).not.toContain('보유 기간 단계에서 확인한 운영 기준을 입력해야 합니다.') + expect(container.querySelector('.paper')?.textContent).toContain('보유하는 개인정보 없음으로 확인되었습니다.') }) it('invalidates stale item facts when the operator attests no collection and does not revive them when the attestation is removed', () => { @@ -49,7 +55,7 @@ describe('explicit no-collection attestation', () => { expect(noCollection).not.toBeNull() fireEvent.click(noCollection) expect(container.querySelectorAll('.check-label input')[2].checked).toBe(false) - expect(container.querySelector('.review-stat.blocking b')?.textContent).toBe('6건') + expect(container.querySelector('.review-stat.blocking b')?.textContent).toBe('7건') fireEvent.click(noCollection) expect(container.querySelector('.review-stat.blocking b')?.textContent).toBe('8건') From e0f25c0a003df46b487ae564d8fc4995581fbfca Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 21:01:31 +0900 Subject: [PATCH 121/282] test: make retention status explicit in buyer flow --- src/App.test.tsx | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/App.test.tsx b/src/App.test.tsx index b901320..b1c2477 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -13,6 +13,7 @@ function completeNonCollectionFacts(container: HTMLElement) { fireEvent.change(container.querySelector('input[name="serviceUrl"]')!, { target: { value: 'https://example.test' } }) fireEvent.click(stepButtons[3]) + fireEvent.change(container.querySelector('select[name="retentionStatus"]')!, { target: { value: 'applies' } }) fireEvent.change(container.querySelector('input[name="retentionPeriod"]')!, { target: { value: '회원 탈퇴 시까지' } }) fireEvent.click(stepButtons[4]) @@ -166,6 +167,28 @@ describe('policy editing workflow', () => { expect(container.querySelector('.form-panel h1')?.textContent).toBe('2. 수집 항목') }) + it('보유 여부를 명시적으로 확인하고 없음 전환 시 이전 보유 기간을 폐기한다', () => { + const { container } = render() + const buttons = container.querySelectorAll('.rail li button') + fireEvent.click(buttons[3]) + + const status = container.querySelector('select[name="retentionStatus"]')! + expect(status.value).toBe('') + expect(container.querySelector('input[name="retentionPeriod"]')).toBeNull() + + fireEvent.change(status, { target: { value: 'applies' } }) + const period = container.querySelector('input[name="retentionPeriod"]')! + fireEvent.change(period, { target: { value: '회원 탈퇴 시까지' } }) + expect(container.querySelector('.paper')?.textContent).toContain('회원 탈퇴 시까지') + + fireEvent.change(status, { target: { value: 'none' } }) + expect(container.querySelector('input[name="retentionPeriod"]')).toBeNull() + expect(container.querySelector('.paper')?.textContent).toContain('보유하는 개인정보 없음으로 확인되었습니다.') + + fireEvent.change(status, { target: { value: 'applies' } }) + expect(container.querySelector('input[name="retentionPeriod"]')?.value).toBe('') + }) + it('제3자 제공과 국외 이전은 확인 전 상태를 별도로 표현하고 없음 확인 시 종속 사실을 요구하지 않는다', () => { const { container } = render() const buttons = container.querySelectorAll('.rail li button') From 6207729b651f259f0c87cab3680bad207a60beda Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 21:02:35 +0900 Subject: [PATCH 122/282] fix: require explicit retention applicability --- src/policy.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/policy.ts b/src/policy.ts index 0bf5920..d1b87d6 100644 --- a/src/policy.ts +++ b/src/policy.ts @@ -1,5 +1,6 @@ export type CollectionMode = '' | '필수' | '선택' export type DisclosureStatus = '' | 'yes' | 'no' +export type RetentionStatus = '' | 'applies' | 'none' export type PolicyItem = { id: string @@ -14,6 +15,7 @@ export type PolicyItem = { export type DraftFacts = { serviceName: string serviceUrl: string + retentionStatus: RetentionStatus retentionPeriod: string thirdPartyStatus: DisclosureStatus thirdPartyRecipient: string @@ -46,6 +48,7 @@ export const initialItems: PolicyItem[] = [ export const initialFacts: DraftFacts = { serviceName: '', serviceUrl: '', + retentionStatus: '', retentionPeriod: '', thirdPartyStatus: '', thirdPartyRecipient: '', @@ -87,8 +90,8 @@ export function getReview(items: PolicyItem[], noCollectionAttested = false) { return { enabled, blocking, modeBlocking, pathBlocking, selectionMissing, collectionContradiction, noCollectionAttested, blockingCount, recommended } } -/** Derives non-collection authoring findings, treating retention as inapplicable only after an explicit no-collection attestation. */ -export function getDraftReview(facts: DraftFacts, noCollectionAttested = false): DraftFinding[] { +/** Derives non-collection authoring findings without inferring retention state from collection state. */ +export function getDraftReview(facts: DraftFacts, _noCollectionAttested = false): DraftFinding[] { const findings: DraftFinding[] = [] const addWhenBlank = (value: string, code: string, step: number, label: string) => { if (!value.trim()) findings.push({ code, step, label }) @@ -99,7 +102,11 @@ export function getDraftReview(facts: DraftFacts, noCollectionAttested = false): if (!serviceUrl) findings.push({ code: 'service_url', step: 1, label: '서비스 URL' }) else if (!isWebServiceUrl(serviceUrl)) findings.push({ code: 'service_url_format', step: 1, label: '서비스 URL 형식' }) - if (!noCollectionAttested) addWhenBlank(facts.retentionPeriod, 'retention_period', 4, '보유 기간') + if (!facts.retentionStatus) { + findings.push({ code: 'retention_status', step: 4, label: '개인정보 보유 여부' }) + } else if (facts.retentionStatus === 'applies') { + addWhenBlank(facts.retentionPeriod, 'retention_period', 4, '보유 기간') + } if (!facts.thirdPartyStatus) { findings.push({ code: 'third_party_status', step: 5, label: '제3자 제공 여부' }) From 25a75ad3ee5ded8b0e605c2fb551cc93b11ad568 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 21:05:15 +0900 Subject: [PATCH 123/282] fix: separate retention status from collection state --- src/App.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 114140e..8d6d28e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -50,6 +50,7 @@ function FactStep({ current, title, description, fields, facts, setFacts, setCur }) { const update = (key: keyof DraftFacts, value: string) => { const next = { ...facts, [key]: value } as DraftFacts + if (key === 'retentionStatus' && value !== 'applies') next.retentionPeriod = '' if (key === 'thirdPartyStatus' && value !== 'yes') { next.thirdPartyRecipient = '' next.thirdPartyPurpose = '' @@ -138,13 +139,19 @@ function EditingPanel({ current, items, setItems, noCollectionAttested, setNoCol { value: 'yes', label: '있음' }, { value: 'no', label: '없음' }, ] + const retentionOptions = [ + { value: '', label: '확인 필요' }, + { value: 'applies', label: '보유함' }, + { value: 'none', label: '보유하지 않음' }, + ] const stepConfig: Record = { 1: { title: '서비스 정보', description: '개인정보처리방침이 적용되는 서비스와 공개 위치를 확인합니다.', fields: [ { key: 'serviceName', label: '서비스 이름', placeholder: '예: 서비스 이름' }, { key: 'serviceUrl', label: '서비스 URL', placeholder: 'https://example.com', type: 'url' }, ] }, - 4: { title: '보유 기간', description: '수집한 개인정보를 언제까지 보유하는지 운영 사실과 근거에 맞춰 기록합니다.', fields: [ - { key: 'retentionPeriod', label: '대표 보유 기간 또는 종료 조건', placeholder: '예: 회원 탈퇴 시까지, 별도 보존 근거가 있는 항목은 해당 기간' }, + 4: { title: '보유 기간', description: '개인정보 보유 여부를 먼저 확인하고, 실제 보유가 있는 경우 기간 또는 종료 조건을 기록합니다.', fields: [ + { key: 'retentionStatus', label: '개인정보 보유 여부', type: 'select', options: retentionOptions }, + { key: 'retentionPeriod', label: '대표 보유 기간 또는 종료 조건', placeholder: '예: 회원 탈퇴 시까지, 별도 보존 근거가 있는 항목은 해당 기간', visibleWhen: { key: 'retentionStatus', equals: 'applies' } }, ] }, 5: { title: '제3자 제공', description: '제3자 제공 여부를 먼저 확인하고, 실제 제공이 있는 경우 제공받는 자와 목적을 기록합니다.', fields: [ { key: 'thirdPartyStatus', label: '제3자 제공 여부', type: 'select', options: yesNoOptions }, @@ -189,7 +196,7 @@ function DocumentPreview({ items, noCollectionAttested, facts, setCurrent }: { i {review.pathBlocking.length > 0 &&
공개 전 확인{review.pathBlocking.map((item) => item.label).join(', ')}의 수집 경로를 확인해야 합니다.
} {review.blocking.length > 0 &&
공개 전 확인{review.blocking.map((item) => item.label).join(', ')}의 처리 목적이 입력되지 않았습니다.
} {draftFindings.map((finding) =>
공개 전 확인{finding.label} 확인이 필요합니다.
)} -

제2조 (처리 및 보유 기간)

{noCollectionAttested ? '개인정보를 수집하지 않음으로 확인되어 보유 기간이 적용되지 않습니다.' : facts.retentionPeriod || '보유 기간 단계에서 확인한 운영 기준을 입력해야 합니다.'}

+

제2조 (처리 및 보유 기간)

{facts.retentionStatus === 'none' ? '보유하는 개인정보 없음으로 확인되었습니다.' : facts.retentionStatus === 'applies' ? facts.retentionPeriod || '보유 기간을 확인해야 합니다.' : '보유 여부 및 기간을 확인해야 합니다.'}

제3조 (제3자 제공)

{facts.thirdPartyStatus === 'no' ? '제3자 제공 없음으로 확인되었습니다.' : facts.thirdPartyStatus === 'yes' ? `${facts.thirdPartyRecipient || '제공받는 자 확인 필요'}에 ${facts.thirdPartyPurpose || '제공 목적 확인 필요'}으로 제공하는 흐름을 검토 중입니다.` : '제3자 제공 여부를 확인하는 단계가 남아 있습니다.'}

제4조 (국외 이전)

{facts.internationalStatus === 'no' ? '국외 이전 없음으로 확인되었습니다.' : facts.internationalStatus === 'yes' ? `${facts.internationalCountry || '국가 확인 필요'} · ${facts.internationalRecipient || '수령자 확인 필요'}` : '국외 이전 여부를 확인하는 단계가 남아 있습니다.'}

개인정보 보호 문의

{facts.privacyOfficerName || '담당자 확인 필요'} · {facts.privacyOfficerEmail || '연락처 확인 필요'}

From 137276367a1eac1432664592f315cf8bd1e3790a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 21:06:53 +0900 Subject: [PATCH 124/282] docs: bind retention authority to PIPC terminology --- docs/research-traceability.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/research-traceability.md b/docs/research-traceability.md index 6c08e68..849fc3f 100644 --- a/docs/research-traceability.md +++ b/docs/research-traceability.md @@ -9,9 +9,14 @@ PolicyWeave treats legal and regulatory material as versioned evidence, not as i | Source | Current evidence on 2026-09-02 | Product use | Implementation status | | --- | --- | --- | --- | | Republic of Korea, Personal Information Protection Act (개인정보 보호법), National Law Information Center | Current law: Act No. 20897, effective 2025-10-02. Enacted amendment: Act No. 21445, promulgated 2026-03-10. Its general amended provisions take effect 2026-09-11, while Article 32-2(1) proviso and Article 75(2)(15) take effect separately on 2027-07-01. Retrieved 2026-09-02 from the National Law Information Center; canonical amendment identifier: Act No. 21445 / legal-information sequence 283839. https://www.law.go.kr/LSW/lsInfoP.do?ancNo=21445&ancYd=20260310&efYd=20260911&lsiSeq=283839 | Statutory authority and provision-level effective-date anchor for rule derivation | Registry only; no article-level rules are encoded yet. Future rule snapshots must bind each provision to its own effective date. | +| Republic of Korea, Personal Information Protection Commission, Standard Personal Information Protection Guidelines (표준 개인정보 보호지침) | PIPC Notice No. 2025-4, effective 2025-04-11. Article 2 defines `처리` as including collection, generation, linkage, recording, storage, retention, use, provision, disclosure, destruction, and similar acts. Retrieved 2026-09-02 from the National Law Information Center; canonical administrative-rule sequence 2100000257592. https://www.law.go.kr/LSW/admRulInfoP.do?admRulSeq=2100000257592&chrClsCd=010201 | Terminology boundary: collection is one form of personal-information processing; absence of collection alone cannot establish absence of storage/retention or other processing | Bound to the retention-applicability repair: PolicyWeave requires a separate explicit retention status and does not infer it from no-collection. | | Republic of Korea, Enforcement Decree of the Personal Information Protection Act (개인정보 보호법 시행령), National Law Information Center | Presidential Decree No. 36121, promulgated 2026-02-19 and effective 2026-08-20. Retrieved 2026-09-02; canonical National Law Information Center legal-information sequence 283503. https://www.law.go.kr/LSW/lsSideInfoP.do?docCls=jo&joNo=0032&lsiSeq=283503 | Subordinate-rule authority/effective-date anchor | Registry only; no decree-level rules are encoded yet. Future mappings must record the exact decree article and effective-date snapshot. | | Personal Information Protection Commission. (2026, April 23). 개인정보 처리방침 작성지침(2026.4. 개정) [Guideline for writing privacy policies, April 2026 revision]. | PIPC marks this as the current guide. Retrieved 2026-09-02. https://pipc.go.kr/np/cop/bbs/selectBoardList.do?bbsId=BS217&mCode=D010030000 | Authoring/review guidance and template requirement discovery | Source registered; requirement-by-requirement mapping still required | +## Retention applicability correction + +An intermediate product repair attempted to treat explicit `개인정보를 수집하지 않음` as sufficient evidence that retention was inapplicable. That implication was rejected before merge evidence because the authoritative PIPC terminology defines `처리` broadly and lists collection, storage, and retention as distinct included acts. Therefore no-collection cannot safely prove no-retention. Test-first commits `734a3d603761fd851f7d8dfc9434d92626518de6`, `dc4f9ad1079f90efb27165099f5dce13ddb4f637`, and `e0f25c0a003df46b487ae564d8fc4995581fbfca` establish the corrected contract: collection state and retention state are independent operator facts. Production commits `6207729b651f259f0c87cab3680bad207a60beda` and `25a75ad3ee5ded8b0e605c2fb551cc93b11ad568` add explicit unresolved/`보유함`/`보유하지 않음` retention status, require a retention period only for `보유함`, clear stale period data when moving away from that state, and project the explicit status without inferring it from no-collection. This remains a product authoring-completeness rule, not a legal-sufficiency conclusion. + ## Accessibility standards traceability W3C's WCAG 2.2 guidance states that authored visual focus indicators are subject to the Level AA non-text contrast requirement, and its Focus Visible guidance explicitly points focus indication to Success Criterion 1.4.11. The previous PolicyWeave focus color `#8eb59e` was approximately 2.27:1 against white, so it did not provide the 3:1 contrast expected for an authored focus indicator. Test-first commit `e5b77f1897ab13dd4f27ccd8b7fa724ba3e74bb0` required the high-contrast product token; production commit `e9e7dcc5c4160a70a9c483574fb651125589392d` moved generic and custom-checkbox focus outlines to `--green` (`#174f35`, approximately 9.52:1 against white); regression commit `ee73bc24e80b32851dcc6f519a457ca9d5efed56` computes the token contrast and enforces a minimum 3:1 ratio rather than relying only on literal CSS text. @@ -20,6 +25,8 @@ WCAG 2.2 Success Criterion 2.4.3 requires sequential focus navigation to preserv ## APA 7 references +Personal Information Protection Commission. (2025). *표준 개인정보 보호지침* [Standard Personal Information Protection Guidelines] (Notice No. 2025-4, effective April 11, 2025). National Law Information Center. https://www.law.go.kr/LSW/admRulInfoP.do?admRulSeq=2100000257592&chrClsCd=010201 + Personal Information Protection Commission. (2026, April 23). *개인정보 처리방침 작성지침(2026.4. 개정)* [Guideline for writing privacy policies, April 2026 revision]. https://pipc.go.kr/np/cop/bbs/selectBoardList.do?bbsId=BS217&mCode=D010030000 Republic of Korea. (2025). *개인정보 보호법* [Personal Information Protection Act] (Act No. 20897, effective October 2, 2025). National Law Information Center. https://www.law.go.kr/unSc.do?query=%EA%B0%9C%EC%9D%B8%EC%A0%95%EB%B3%B4%EB%B3%B4%ED%98%B8%EB%B2%95 @@ -40,6 +47,7 @@ World Wide Web Consortium. (2025). *Understanding Success Criterion 2.4.7: Focus 3. Guidance is not promoted to statute, and product copy does not claim that following a guide guarantees compliance. 4. A source revision creates an explicit evaluation event against existing draft/published revisions; it does not mutate historical publication evidence. 5. LLM output, if later used to explain or propose wording, is never an authoritative legal source and cannot change review/publication state. +6. Collection absence is not evidence of absence of other processing acts such as storage or retention; applicability decisions require their own explicit source fact unless an authoritative rule proves a dependency. ## Current gap -The seven-step workspace now captures the product's intended fact categories, but retention, third-party provision, international transfer, contact/controller information, and legal-basis review still need requirement-level mappings to the authoritative register, deterministic validation, and regression fixtures before PolicyWeave can claim those steps are legally complete. CSS-level focus contrast and deterministic step-focus transfer now have executable regression contracts, but real-browser accessibility evidence remains required before claiming WCAG conformance. +The seven-step workspace now captures the product's intended fact categories, including independent explicit retention applicability, but retention-period/legal-basis detail, third-party provision, international transfer, contact/controller information, and legal-basis review still need requirement-level mappings to the authoritative register, deterministic validation, and regression fixtures before PolicyWeave can claim those steps are legally complete. CSS-level focus contrast and deterministic step-focus transfer now have executable regression contracts, but real-browser accessibility evidence remains required before claiming WCAG conformance. From 35e462f12a55ef365a2a1e72b13a669205d5e59a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 21:07:58 +0900 Subject: [PATCH 125/282] docs: require explicit retention authority --- docs/PRD.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/PRD.md b/docs/PRD.md index 5403aca..3c6b577 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -6,7 +6,7 @@ ## 제품 원칙 -PolicyWeave는 법률 문장을 임의로 창작하는 도구가 아니다. 운영자가 입력한 사실을 구조화하고, 적용 근거와 확인이 필요한 부분을 분리해 검토본을 만든다. 자동 생성 결과는 변호사·개인정보보호책임자의 검토를 대체하지 않는다. 새 작업공간은 고객의 실제 수집 여부, 수집 구분, 처리 목적, 수집 경로, 제3자 제공 여부 또는 국외 이전 여부를 추정하지 않는다. 비어 있는 값은 `없음`이 아니라 `미확인`이다. 실제 개인정보를 수집하지 않는 서비스는 운영자가 이를 명시적으로 확인할 수 있어야 하며, 그 확인은 빈 항목 목록에서 추론되지 않는다. 명시적 no-collection 상태에서는 수집된 개인정보가 전제인 보유 기간을 임의로 만들어 요구하지 않고 `적용되지 않음`으로 투영한다. +PolicyWeave는 법률 문장을 임의로 창작하는 도구가 아니다. 운영자가 입력한 사실을 구조화하고, 적용 근거와 확인이 필요한 부분을 분리해 검토본을 만든다. 자동 생성 결과는 변호사·개인정보보호책임자의 검토를 대체하지 않는다. 새 작업공간은 고객의 실제 수집 여부, 수집 구분, 처리 목적, 수집 경로, 보유 여부·기간, 제3자 제공 여부 또는 국외 이전 여부를 추정하지 않는다. 비어 있는 값은 `없음`이 아니라 `미확인`이다. 실제 개인정보를 수집하지 않는 서비스는 운영자가 이를 명시적으로 확인할 수 있어야 하며, 그 확인은 빈 항목 목록에서 추론되지 않는다. 개인정보의 `수집`과 `보유`는 서로 다른 처리 사실이므로 no-collection 확인만으로 no-retention을 추론하지 않는다. ## MVP @@ -15,6 +15,7 @@ PolicyWeave는 법률 문장을 임의로 창작하는 도구가 아니다. 운 - 제품 정의 필수 사실 누락과 권장 검토 항목의 구분 - 처리 항목별 수집 구분·목적·수집 경로 연결 - 개인정보를 수집하지 않는 경우의 명시적 운영자 확인과 수집 항목 사실의 상호배타성 +- 개인정보 보유 여부의 명시적 `보유함`/`보유하지 않음` 확인; `보유함`일 때만 보유 기간 요구 - 제3자 제공과 국외 이전의 명시적 `있음`/`없음` 확인; `있음`일 때만 종속 상세 사실 요구 - 공개 전 검토 요약과 버전 정보 - 정적 공개 URL 발행 계약(후속 백엔드에서 구현) @@ -26,6 +27,7 @@ PolicyWeave는 법률 문장을 임의로 창작하는 도구가 아니다. 운 - 사용자의 실제 서비스·SDK를 자동 추측 - 동의가 필요 없는 처리에 무조건 동의를 요구하는 방식 - 빈 입력을 자동으로 `없음` 또는 적법 상태로 해석하는 방식 +- 한 작성 단계의 부정 사실을 다른 처리 단계의 부정 사실로 자동 확장하는 방식 ## 성공 기준 @@ -35,8 +37,10 @@ PolicyWeave는 법률 문장을 임의로 창작하는 도구가 아니다. 운 - 단계 이동 자체는 작성 책임의 완료를 의미하지 않는다. 진행률, 완료 체크와 `입력 확인됨` 표시는 해당 단계의 제품 정의 차단 사실이 실제로 해소된 경우에만 readiness와 동일한 fail-closed 사실 계약에서 파생한다. - 수집 단계는 실제 수집 항목을 하나 이상 확인하거나 `개인정보를 수집하지 않음`을 명시적으로 확인해야 한다. 두 상태가 동시에 참인 데이터는 공개 준비를 통과하지 않는다. - `개인정보를 수집하지 않음`으로 전환하면 이전 수집 항목의 수집 구분·처리 목적·수집 경로를 폐기하고, 확인을 해제해도 해당 사실을 자동 복원하지 않는다. -- `개인정보를 수집하지 않음`이 명시적으로 확인된 동안에는 보유 기간 입력을 완료 조건으로 강제하거나 빈 보유 기간을 `미확인`으로 경고하지 않는다. 검토본은 보유 기간을 추정하지 않고 명시적으로 `적용되지 않음`으로 표시한다. -- 실제 수집 항목이 있는 경우 각 항목의 수집 구분·처리 목적·수집 경로, 서비스 이름·URL, 보유 기간, 제3자 제공 여부, 국외 이전 여부, 개인정보 보호 담당자·연락 이메일이 확인되기 전에는 공개 준비 상태를 통과하지 않는다. +- no-collection 상태는 보유 여부를 자동 결정하지 않는다. 보유 단계는 별도의 명시적 `보유함`/`보유하지 않음` 상태를 요구하며, `보유함`은 비어 있지 않은 보유 기간 또는 종료 조건을 추가로 요구한다. +- `보유함`에서 벗어나면 이전 보유 기간을 폐기하고, 다시 `보유함`으로 전환해도 해당 값을 자동 복원하지 않는다. +- 실제 수집 항목이 있는 경우 각 항목의 수집 구분·처리 목적·수집 경로, 서비스 이름·URL, 명시적 보유 상태와 필요한 보유 기간, 제3자 제공 여부, 국외 이전 여부, 개인정보 보호 담당자·연락 이메일이 확인되기 전에는 공개 준비 상태를 통과하지 않는다. +- 개인정보를 수집하지 않는 경우에도 보유 상태는 독립적으로 확인되어야 한다. 이는 다른 경로로 생성·저장·보유하는 개인정보 가능성을 수집 부재만으로 배제하지 않기 위함이다. - 서비스 URL은 절대 HTTP(S) 주소 형식을 만족해야 하고, 개인정보 보호 연락 이메일은 최소 주소 형식 검사를 통과해야 한다. 이 검사는 서비스 존재나 메일함 소유권을 주장하지 않는다. - 제3자 제공 또는 국외 이전을 `있음`으로 확인한 경우 해당 수령자/목적 또는 국가/수령자 사실까지 확인되어야 한다. - `없음` 확인은 명시적 운영자 사실이며, 이전에 입력한 종속 제공·이전 상세 사실은 상태 변경 시 폐기된다. From 73ed5f36778fbcb71246ed86e905291f593e1471 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 21:08:40 +0900 Subject: [PATCH 126/282] docs: separate retention and collection contracts --- docs/TRD.md | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/docs/TRD.md b/docs/TRD.md index 03ce768..47ea778 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -6,8 +6,8 @@ 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. -- `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, retention applicability, transfer statuses/details, and privacy contact. -- `src/App.tsx` provides the seven-step authoring flow, review navigation, explicit no-collection and transfer-status capture, stale dependent-fact invalidation, and deterministic preview projection. +- `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; ordinary form controls and the dedicated preview shortcut are outside this behavior. - The current CI contract is lint, Vitest, and TypeScript/Vite build plus live organization-required security/review workflows. @@ -16,16 +16,17 @@ This TRD covers the pre-release PolicyWeave browser workspace and the contracts 2. A fresh workspace contains no inferred customer operational facts. Blank authoring values mean unresolved, not `none`. It starts at the first unresolved responsibility (`서비스 정보`) and must not mark an untouched earlier responsibility as completed merely because a later editor was shown. 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. 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. Retention period is additionally required whenever actual collection items exist. When the operator explicitly attests no collection, retention is inapplicable rather than unresolved: no retention value is inferred, the retention responsibility is non-blocking, and the preview projects that explicit applicability state. 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. 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. -7. Blank/whitespace authoring facts are normalized as unresolved unless a governing explicit fact makes that responsibility inapplicable, as with retention under no-collection. -8. Disabling a collection item invalidates dependent collection-mode, processing-purpose, and collection-path evidence; re-enabling requires renewed confirmation. -9. Every blocking finding links to the responsible editing step. -10. Explicit step navigation through the rail, previous/next controls, or a blocking-finding action transfers focus to the newly active step heading after that surface renders. This logical focus-order contract must not steal focus from ordinary editing controls or the dedicated preview shortcut. -11. Preview text is derived from current structured facts, including the explicit no-collection attestation and its retention-applicability consequence; it does not become an independently editable authority. -12. UI copy distinguishes a review draft from legal advice, certification, or a compliance guarantee. - -These are authoring-completeness contracts, not claims that the resulting policy is legally sufficient. Legal sufficiency remains a separate versioned-rule responsibility backed by authoritative source evidence. +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. +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. +10. Every blocking finding links to the responsible editing step. +11. Explicit step navigation through the rail, previous/next controls, or a blocking-finding action transfers focus to the newly active step heading after that surface renders. This logical focus-order contract must not steal focus from ordinary editing controls or the dedicated preview shortcut. +12. Preview text is derived from current structured facts, including independent collection and retention statuses; it does not become an independently editable authority. +13. UI copy distinguishes a review draft from legal advice, certification, or a compliance guarantee. + +The separation between collection and retention follows the PIPC Standard Personal Information Protection Guidelines terminology: `처리` includes collection, storage, retention, and other acts, so absence of collection is insufficient evidence for absence of retention. These remain authoring-completeness contracts, not claims that the resulting policy is legally sufficient. Legal sufficiency remains a separate versioned-rule responsibility backed by authoritative source evidence. ## Quality contracts - Touched production behavior requires regression and edge-case tests first. @@ -36,7 +37,7 @@ These are authoring-completeness contracts, not claims that the resulting policy - Production does not depend on synthetic demo data. ## 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 state and retention applicability without manufacturing a `retention_rule` or synthetic collection item. +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. A publication command must operate on a specific reviewed `policy_revision` and create a new immutable `publication_revision`. It must fail closed when blocking findings, incompatible rule/template versions, missing source receipts, or missing authorization exist. Publication never mutates foreign customer databases. From 20354c464ddcf37018f32b4293eca15ef992226e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 21:09:23 +0900 Subject: [PATCH 127/282] docs: record independent retention authority --- CHANGELOG.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26a8860..1940393 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,11 @@ All notable product changes are recorded here. PolicyWeave is pre-release; entri ### Added - Seven-step authoring workflow with distinct editing surfaces for service information, collection items, processing purposes, retention, third-party transfer, international transfer, and privacy contact. -- Explicit `개인정보를 수집하지 않음` operator attestation so a genuine no-collection service can complete authoring without treating an empty item list as `none`. -- Warning-to-source navigation for missing collection selection/no-collection confirmation, collection mode, collection-path evidence, processing purposes, service identity, retention, transfer statuses/details, and privacy contact. +- Explicit `개인정보를 수집하지 않음` operator attestation so a genuine no-collection service can complete collection authoring without treating an empty item list as `none`. +- Independent explicit retention status (`확인 필요` / `보유함` / `보유하지 않음`) so collection absence cannot be misused as evidence that storage or retention is absent. +- Warning-to-source navigation for missing collection selection/no-collection confirmation, collection mode, collection-path evidence, processing purposes, service identity, retention status/period, transfer statuses/details, and privacy contact. - 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, no-collection retention applicability, 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. +- 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. ### Changed @@ -17,9 +18,10 @@ All notable product changes are recorded here. PolicyWeave is pre-release; entri - Fresh workspaces now open at step 1, service information, rather than displaying step 2 while falsely rendering the untouched first responsibility as completed. - Empty collection selection remains unresolved rather than being interpreted as `none`; no-collection and selected-item states are mutually exclusive and contradictory state fails closed. - Turning on no-collection confirmation clears selected-item mode, purpose, and path evidence; later removing the confirmation does not silently restore stale customer facts. -- Explicit no-collection confirmation now makes the retention responsibility inapplicable instead of forcing the operator to invent a retention period; the review draft projects that applicability state without synthesizing a retention value. +- Collection and retention are now independent authority states. An intermediate no-collection→no-retention inference was removed after authoritative PIPC terminology showed that collection, storage, and retention are distinct included forms of personal-information processing. +- Retention readiness now requires an explicit status: `보유함` requires a period/end condition; `보유하지 않음` does not. Leaving `보유함` clears the previous period so stale facts cannot silently revive. - Every selected collection item requires explicit collection mode, processing purpose, and nonblank collection-path evidence before readiness can pass; collection-path findings navigate back to the collection step. -- Public-readiness includes product-defined service name/URL, transfer-status/detail, and privacy-contact completeness; retention completeness is additionally required whenever actual collection items exist rather than when no-collection has been explicitly established. +- 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. - 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. From b0a18259dc59d46343f1499e603472212d1fe73a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 21:10:26 +0900 Subject: [PATCH 128/282] docs: reconcile independent retention authority --- docs/product-technical-gap-baseline.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index ce7a052..ed36c71 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -18,7 +18,7 @@ A concurrent writer then established the missing collection-path readiness contr The following commercialization pass identified a remaining buyer-flow dead end: a service that genuinely collects no personal data could never clear collection readiness because an empty item selection was always unresolved. Interpreting emptiness as `none` would have reintroduced inference. Test-first commit `97063fc34fbc0e6db2652922f78bc74c00e1ef93` specifies an explicit no-collection attestation, contradiction fail-closed behavior, and stale-item invalidation. Production commit `e73eb83a913597fb05ddea97519d549d58007faf` adds the operator attestation, makes it mutually exclusive with selected items, clears item mode/purpose/path facts when enabled, prevents their later silent revival, and projects the explicit negative fact into the review draft. This is a product authoring-completeness state, not a new legal conclusion. -A later exact-head pass found that the same explicit no-collection path still forced a nonblank retention period. That contract made a genuine no-data service invent a retention fact to reach readiness, contradicting the product's no-inference rule. Test-first commit `01ff0548b2b89bd65d0478f2499dd7a9fb7a6f4d` requires no-collection to reduce the unresolved blocker count, mark the retention responsibility non-blocking, and project an explicit `적용되지 않음` statement without synthesizing a retention value. Domain repair `5ad05c6a9f51aece33672bc9b1e1dc5dfbe3ccf3` makes `getDraftReview` retention-aware only from the explicit no-collection attestation, while collecting services still require retention. UI projection repair `fc35f8c1b597ad6b556e1fa3abfef466d80ac4e9` propagates the same contract into readiness and the review draft. PRD/TRD/CHANGELOG then reconcile the behavior at `ddd24eae92d1625f91bc41614dc2f977b848eb50`, `773ecb3b26ce83144051a5122692bd3e6052e0ad`, and `b3ab6b44ad935448b23aa2d0f854d7793740c2c6`. No legal sufficiency claim is added: this is a product-level applicability rule derived solely from the operator's explicit no-collection fact. +A subsequent pass challenged the relation between collection and retention rather than allowing the product to manufacture a downstream fact. An intermediate candidate (`01ff0548b2b89bd65d0478f2499dd7a9fb7a6f4d` → `5ad05c6a9f51aece33672bc9b1e1dc5dfbe3ccf3` → `fc35f8c1b597ad6b556e1fa3abfef466d80ac4e9`) tried to make retention automatically inapplicable under no-collection. Before merge evidence, current authoritative PIPC terminology was rechecked: the Standard Personal Information Protection Guidelines define `처리` as including collection, recording, storage, retention, use, provision and other acts. Therefore absence of collection alone cannot prove absence of storage/retention. The candidate inference was superseded on the same writer branch by test-first commits `734a3d603761fd851f7d8dfc9434d92626518de6`, `dc4f9ad1079f90efb27165099f5dce13ddb4f637`, and `e0f25c0a003df46b487ae564d8fc4995581fbfca`. Production commits `6207729b651f259f0c87cab3680bad207a60beda` and `25a75ad3ee5ded8b0e605c2fb551cc93b11ad568` introduce an independent explicit retention status (`unresolved` / `applies` / `none`), require a period only for `applies`, clear stale period data when leaving that state, and never derive retention status from no-collection. `docs/research-traceability.md` binds the correction to PIPC Notice No. 2025-4. This is a product fact-authority correction, not a legal-sufficiency claim. The next accessibility pass established a concrete authored-focus defect rather than claiming generic WCAG conformance: `styles.css` used `#8eb59e` for generic and custom-checkbox keyboard focus outlines, approximately 2.27:1 against white. W3C WCAG 2.2 guidance ties authored focus indication to the Level AA non-text contrast requirement. Test-first commit `e5b77f1897ab13dd4f27ccd8b7fa724ba3e74bb0` required the high-contrast product token; production commit `e9e7dcc5c4160a70a9c483574fb651125589392d` changed both focus paths to `--green` (`#174f35`, approximately 9.52:1 against white); regression commit `ee73bc24e80b32851dcc6f519a457ca9d5efed56` now computes the token contrast and enforces a minimum 3:1 ratio. @@ -32,31 +32,31 @@ A concurrent writer then repaired completion/progress semantics so navigation ca | Area | Evidence | Status | Commercialization gap | Owner/action | Next verification | | --- | --- | --- | --- | --- | --- | -| Guided authoring | PRD, ADR-0002, seven routed editors, first-responsibility startup, `getReview`, retention-aware `getDraftReview`, explicit no-collection/transfer states | Repaired foundation | Fresh state no longer skips or falsely completes step 1; selection/no-collection, mode, purpose, path and non-collection responsibilities fail closed; no-collection makes retention explicitly inapplicable rather than fabricated; collection-path evidence remains unstructured free text and legal sufficiency is deliberately separate | Policy Fact Authoring: preserve deterministic completeness/applicability; add structured path-evidence types only when a real integration/use case proves the need | Exact-head unit/UI edge tests, then browser E2E | -| Customer-fact authority | Zero-inferred startup facts; truthful initial rail state; explicit no-collection state; no-collection retention applicability; explicit collection/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 explicit applicability instead of manufacturing retention data | Policy Fact Authoring: encode no-collection as a revision-owned fact and derive retention applicability from it, never as a fake collection item or invented retention rule | Persistence/schema invariant tests and exact-head UI tests | +| 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 | +| 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 | Implemented foundation | No real-browser screenshot matrix or complete interaction evidence | UX: add Playwright + automated accessibility harness and verify action edges | Desktop/tablet/mobile screenshots, keyboard/focus and accessibility checks | -| Accessibility | Semantic controls, visible focus behavior, WCAG-traced focus token, executable >=3:1 contrast regression, jsdom step-focus transition regression | Partial, focus contrast and logical transition repaired | Real-browser keyboard traversal, focus-not-obscured, zoom, screen-reader and full automated WCAG 2.2 evidence remain absent | UX/Test Engineering: add axe/Playwright and manual interaction record without claiming conformance from unit/CSS evidence alone | Exact-head WCAG/browser matrix including no-collection transition | -| Legal/rule traceability | `docs/research-traceability.md`; source/effective-date model | Source baseline established | Current readiness rules are product completeness/applicability only; no versioned article/section-level legal-rule engine | Legal Source Registry: map future legal validations to source/effective date/revision | Snapshot fixtures across effective dates | -| Policy model | ADRs, ARCHITECTURE, TRD | Designed, browser-memory only | No versioned durable schema, migrations, audit history, 3NF implementation, or explicit UPSERT/idempotency | Platform: define `policy_revision` schema and item-level conflict contracts before storage; persist explicit no-collection state and retention applicability on the revision-owned fact model | Schema tests and migration round trip | +| Accessibility | Semantic controls, visible focus behavior, WCAG-traced focus token, executable >=3:1 contrast regression, jsdom step-focus transition regression | Partial, focus contrast and logical transition repaired | Real-browser keyboard traversal, focus-not-obscured, zoom, screen-reader and full automated WCAG 2.2 evidence remain absent | UX/Test Engineering: add axe/Playwright and manual interaction record without claiming conformance from unit/CSS evidence alone | Exact-head WCAG/browser matrix including no-collection and retention transitions | +| Legal/rule traceability | `docs/research-traceability.md`; PIPA/PIPC source/effective-date model; PIPC Notice No. 2025-4 terminology bound to collection/retention separation | Source baseline established | Current readiness rules are product completeness only; no versioned article/section-level legal-rule engine | Legal Source Registry: map future legal validations to source/effective date/revision | Snapshot fixtures across effective dates | +| Policy model | ADRs, ARCHITECTURE, TRD | Designed, browser-memory only | No versioned durable schema, migrations, audit history, 3NF implementation, or explicit UPSERT/idempotency | Platform: define `policy_revision` schema and item-level conflict contracts before storage; persist no-collection and retention status independently on the revision-owned fact model | Schema tests and migration round trip | | 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 | Test-first startup authority, truthful initial step/progress, seven-step completeness, collection-path, explicit no-collection, no-collection retention applicability, focus-contrast, and logical focus-transition regressions plus earlier routing/responsive/SAST regressions | Improved | Real-browser E2E/accessibility and repository-wide 100% execution/docstring coverage are not yet evidenced | Test Engineering: measure coverage and add browser-level behavior tests | Exact-head coverage + browser evidence | +| Tests | Test-first startup authority, truthful initial step/progress, seven-step completeness, collection-path, explicit no-collection, independent retention status/stale-period invalidation, focus-contrast, and logical focus-transition regressions plus earlier routing/responsive/SAST regressions | Improved | Real-browser E2E/accessibility and repository-wide 100% execution/docstring coverage are not yet evidenced | Test Engineering: measure coverage and add browser-level behavior tests | Exact-head coverage + browser 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 | Live external gate | 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 + independent approval + resolved threads | ## DDD/context map Core subdomain: **Policy Fact Authoring**. Supporting: **Review & Publication**, **Legal Source Registry**. Generic infrastructure remains outside domain authority. `Policy Fact Authoring -> Review & Publication` supplies a versioned fact contract. `Legal Source Registry -> Review & Publication` supplies versioned source/rule evidence through an ACL. Customer/source-system integrations remain adapters and may not mutate foreign systems through hidden coupling. -Ubiquitous language: `policy_revision`, `service_profile`, explicit no-collection attestation, retention applicability, `collection_item`, `processing_purpose`, `retention_rule`, `third_party_transfer`, `international_transfer`, `privacy_contact`, `review_finding`, `legal_source_revision`, `publication_revision`. +Ubiquitous language: `policy_revision`, `service_profile`, explicit no-collection attestation, `retention_status`, `collection_item`, `processing_purpose`, `retention_rule`, `third_party_transfer`, `international_transfer`, `privacy_contact`, `review_finding`, `legal_source_revision`, `publication_revision`. -The future aggregate root is the minimal `policy_revision` boundary. The no-collection fact is revision-owned state rather than a synthetic `collection_item`; retention applicability is derived from that explicit fact, and no `retention_rule` is created for an inapplicable responsibility. Rendered prose is a read projection. `publication_revision` is immutable and explicitly superseded rather than updated. Relational persistence is 3NF by default; named persistence objects use at least two semantic words and `snake_case`, and item-level UPSERT/idempotency semantics must be explicit. +The future aggregate root is the minimal `policy_revision` boundary. The no-collection fact is revision-owned state rather than a synthetic `collection_item`. `retention_status` is a separate revision-owned fact; `retention_rule` exists only when that status is `applies`. Transitions away from `applies` invalidate the current retention rule while immutable audit history remains distinct. Rendered prose is a read projection. `publication_revision` is immutable and explicitly superseded rather than updated. Relational persistence is 3NF by default; named persistence objects use at least two semantic words and `snake_case`, and item-level UPSERT/idempotency semantics must be explicit. ## Buyer-visible release gates -A publishable release requires complete fact-to-warning navigation, deterministic rendering of captured source facts, no inferred customer operational facts, truthful first-responsibility startup/progress, explicit unresolved-review blocking across the seven authoring responsibilities, an explicit no-collection path for genuine no-data services that does not fabricate retention data, immutable reviewed publication revisions, accessible responsive interaction, authoritative source/version traceability for legal rules, hosted security/privacy evidence, and exact-head CI/security/independent review. Buyer-facing wording describes assistance and reviewability, never a compliance guarantee or an internal implementation boundary. +A publishable release requires complete fact-to-warning navigation, deterministic rendering of captured source facts, no inferred customer operational facts, truthful first-responsibility startup/progress, explicit unresolved-review blocking across the seven authoring responsibilities, an explicit no-collection path that does not infer unrelated processing facts, independent explicit retention status, immutable reviewed publication revisions, accessible responsive interaction, authoritative source/version traceability for legal rules, hosted security/privacy evidence, and exact-head CI/security/independent review. Buyer-facing wording describes assistance and reviewability, never a compliance guarantee or an internal implementation boundary. ## Active commercialization order 1. Reacquire exact-current-head CI/security/SAST/central workflow execution and independent approval; merge PR #1 only through ordinary protection. -2. Complete real-browser responsive/accessibility evidence for the fail-closed seven-step workflow, including keyboard traversal, no-collection transition, focus-not-obscured behavior, zoom/screen-reader checks and screenshots; CSS focus contrast and deterministic logical step-focus transfer are already repaired. -3. Define and test versioned `policy_revision` 3NF persistence/audit, explicit no-collection storage, retention applicability, and item-level UPSERT contracts. +2. Complete real-browser responsive/accessibility evidence for the fail-closed seven-step workflow, including keyboard traversal, no-collection/retention transitions, focus-not-obscured behavior, zoom/screen-reader checks and screenshots; CSS focus contrast and deterministic logical step-focus transfer are already repaired. +3. Define and test versioned `policy_revision` 3NF persistence/audit, explicit no-collection storage, independent retention status/rule state, and item-level UPSERT contracts. 4. Implement secure review/publication with immutable releases and explicit supersession. 5. Add hosted tenant isolation, audit/incident evidence, compose deployment, and realistic performance/load tests when network services exist. From 8843fbefe0ced81b74420472ad3e55401f729011 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 21:07:00 +0900 Subject: [PATCH 129/282] ci(actions): restamp current head after startup failure From ee9574068776a202ae91823334d1cbd9cd8a62ff Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 21:35:32 +0900 Subject: [PATCH 130/282] chore: refresh head after Actions startup failure From cf559fd09ff7b1c834544f5083f555d9a73af706 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 07:27:27 +0900 Subject: [PATCH 131/282] fix(ci): restore truthful workflow verification --- CHANGELOG.md | 2 ++ docs/product-technical-gap-baseline.md | 4 +++- package-lock.json | 19 +++++++++++++++++++ package.json | 2 +- src/App.test.tsx | 13 ++++++++++++- src/App.tsx | 2 +- src/no-collection.test.tsx | 1 + tsconfig.app.json | 2 +- 8 files changed, 40 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1940393..e6dc798 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ All notable product changes are recorded here. PolicyWeave is pre-release; entri ### Changed - Fresh workspaces no longer preselect collection items or prefill collection modes/processing purposes; readiness fails closed until the operator establishes actual collection facts or explicitly attests that the service collects no personal data. - Fresh workspaces now open at step 1, service information, rather than displaying step 2 while falsely rendering the untouched first responsibility as completed. +- Completed responsibilities retain their `done` state while active, and collection-flow tests now enter step 2 explicitly instead of depending on the retired step-2 startup state. - Empty collection selection remains unresolved rather than being interpreted as `none`; no-collection and selected-item states are mutually exclusive and contradictory state fails closed. - Turning on no-collection confirmation clears selected-item mode, purpose, and path evidence; later removing the confirmation does not silently restore stale customer facts. - Collection and retention are now independent authority states. An intermediate no-collection→no-retention inference was removed after authoritative PIPC terminology showed that collection, storage, and retention are distinct included forms of personal-information processing. @@ -35,6 +36,7 @@ All notable product changes are recorded here. PolicyWeave is pre-release; entri - 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. +- Node types are declared for the stylesheet contract test's `node:fs` boundary so the production TypeScript build type-checks the executable test source. - GitHub Actions checkout dependencies are SHA pinned and credentials are not persisted in the working tree. ### Not yet shipped diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index ed36c71..29dfb95 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1,6 +1,6 @@ # Product and technical gap baseline -Last reconciled: 2026-09-02 +Last reconciled: 2026-09-07 This ledger records PolicyWeave's commercialization gap from PRD/ADR/TRD/architecture, implementation, authoritative legal-source evidence, review findings, and live GitHub state. Live Check conclusions are not committed because they change outside the repository; merge decisions must re-fetch the exact current PR head and live ruleset. @@ -28,6 +28,8 @@ A subsequent buyer-flow pass found that a fresh workspace still initialized `cur A concurrent writer then repaired completion/progress semantics so navigation cannot make untouched responsibilities appear complete. The branch advanced without rewriting predecessor history; completion is now derived from the same verified-fact contract used by readiness rather than from the currently visited step. This evidence is preserved on the canonical `develop` writer branch and must remain part of final exact-head verification. +The 2026-09-07 exact-head CI run exposed nine regressions after the truthful step-1 startup repair. Eight tests still queried step-2 collection controls before navigating there; they now enter the collection responsibility explicitly. The remaining RED showed that the active class replaced the completed class, so a completed current responsibility lost its `done` state. `StepRail` now composes `active` and `done` independently, preserving both current-position and verified-completion semantics without changing readiness rules. Full verification then exposed a separate TypeScript build failure because the stylesheet contract test imported `node:fs` without Node type authority; the existing file-based test remains intact and now has an explicit Node type dependency. + ## Current baseline | Area | Evidence | Status | Commercialization gap | Owner/action | Next verification | diff --git a/package-lock.json b/package-lock.json index 205649c..365bb71 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "policyweave", "version": "0.1.0", + "license": "MIT", "dependencies": { "@vitejs/plugin-react": "latest", "lucide-react": "latest", @@ -19,6 +20,7 @@ "@eslint/js": "latest", "@testing-library/jest-dom": "latest", "@testing-library/react": "latest", + "@types/node": "^26.4.1", "@types/react": "latest", "@types/react-dom": "latest", "eslint": "latest", @@ -1149,6 +1151,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "26.4.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.4.1.tgz", + "integrity": "sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, "node_modules/@types/react": { "version": "19.2.18", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", @@ -3424,6 +3436,13 @@ "node": ">=22.19.0" } }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "devOptional": true, + "license": "MIT" + }, "node_modules/update-browserslist-db": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", diff --git a/package.json b/package.json index 3e3a137..ae57357 100644 --- a/package.json +++ b/package.json @@ -1 +1 @@ -{"name":"policyweave","private":true,"version":"0.1.0","license":"MIT","type":"module","scripts":{"dev":"vite --host 0.0.0.0","build":"tsc -b && vite build","lint":"eslint .","test":"vitest run","test:watch":"vitest"},"dependencies":{"@vitejs/plugin-react":"latest","lucide-react":"latest","react":"latest","react-dom":"latest","typescript":"^6.0.3","vite":"latest"},"devDependencies":{"@eslint/js":"latest","@testing-library/jest-dom":"latest","@testing-library/react":"latest","@types/react":"latest","@types/react-dom":"latest","eslint":"latest","eslint-plugin-react-hooks":"latest","eslint-plugin-react-refresh":"latest","globals":"latest","jsdom":"latest","typescript-eslint":"^8.68.0","vitest":"latest"}} +{"name":"policyweave","private":true,"version":"0.1.0","license":"MIT","type":"module","scripts":{"dev":"vite --host 0.0.0.0","build":"tsc -b && vite build","lint":"eslint .","test":"vitest run","test:watch":"vitest"},"dependencies":{"@vitejs/plugin-react":"latest","lucide-react":"latest","react":"latest","react-dom":"latest","typescript":"^6.0.3","vite":"latest"},"devDependencies":{"@eslint/js":"latest","@testing-library/jest-dom":"latest","@testing-library/react":"latest","@types/node":"^26.4.1","@types/react":"latest","@types/react-dom":"latest","eslint":"latest","eslint-plugin-react-hooks":"latest","eslint-plugin-react-refresh":"latest","globals":"latest","jsdom":"latest","typescript-eslint":"^8.68.0","vitest":"latest"}} \ No newline at end of file diff --git a/src/App.test.tsx b/src/App.test.tsx index b1c2477..70a0967 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -5,6 +5,10 @@ import App from './App' afterEach(cleanup) +function openCollectionStep(container: HTMLElement) { + fireEvent.click(container.querySelectorAll('.rail li button')[1]) +} + function completeNonCollectionFacts(container: HTMLElement) { const stepButtons = container.querySelectorAll('.rail li button') @@ -68,6 +72,7 @@ describe('policy editing workflow', () => { it('수집 단계에서는 선택 항목의 수집 경로와 수집 구분을 명시적으로 확인하고 처리 목적은 다음 단계에서 편집한다', () => { const { container } = render() + openCollectionStep(container) expect(container.querySelectorAll('input[placeholder="예: 회원가입 화면"]')).toHaveLength(0) const phone = container.querySelectorAll('.check-label input')[2] @@ -85,6 +90,7 @@ describe('policy editing workflow', () => { it('수집 항목을 해제하면 이전 처리 목적, 수집 경로, 수집 구분을 폐기해 재활성화 시 재검토한다', () => { const { container } = render() + openCollectionStep(container) const phoneCheckbox = () => container.querySelectorAll('.check-label input')[2] fireEvent.click(phoneCheckbox()) @@ -111,6 +117,7 @@ describe('policy editing workflow', () => { it('공백뿐인 처리 목적은 미리보기에서도 미입력으로 표시한다', () => { const { container } = render() + openCollectionStep(container) const nameCheckbox = container.querySelectorAll('.check-label input')[0] fireEvent.click(nameCheckbox) const nameItem = container.querySelectorAll('.item-list .item')[0] @@ -130,6 +137,7 @@ describe('policy editing workflow', () => { it('미리보기 처리 목적 경고에서 처리 목적 단계로 이동한다', () => { const { container } = render() + openCollectionStep(container) const phone = container.querySelectorAll('.check-label input')[2] fireEvent.click(phone) const warningButton = Array.from(container.querySelectorAll('.document-warning button')).find((button) => button.textContent?.includes('처리 목적'))! @@ -147,6 +155,7 @@ describe('policy editing workflow', () => { it('수집 구분 미확인 경고에서 수집 항목 단계로 이동한다', () => { const { container } = render() + openCollectionStep(container) fireEvent.click(container.querySelectorAll('.check-label input')[2]) fireEvent.click(container.querySelectorAll('.rail li button')[0]) const warningButton = Array.from(container.querySelectorAll('.document-warning button')).find((button) => button.textContent?.includes('수집 구분'))! @@ -156,6 +165,7 @@ describe('policy editing workflow', () => { it('수집 경로 미확인 경고에서 수집 항목 단계로 이동한다', () => { const { container } = render() + openCollectionStep(container) fireEvent.click(container.querySelectorAll('.check-label input')[2]) const phoneItem = container.querySelectorAll('.item-list .item')[2] fireEvent.change(phoneItem.querySelector('select')!, { target: { value: '필수' } }) @@ -223,6 +233,7 @@ describe('policy editing workflow', () => { it('모든 제품 정의 필수 사실을 확인한 뒤에만 공개 준비 확인을 허용한다', () => { const { container } = render() + openCollectionStep(container) const phone = container.querySelectorAll('.check-label input')[2] fireEvent.click(phone) const phoneItem = container.querySelectorAll('.item-list .item')[2] @@ -246,4 +257,4 @@ describe('policy editing workflow', () => { expect(message).not.toContain('백엔드') expect(message).not.toContain('저장소') }) -}) \ No newline at end of file +}) diff --git a/src/App.tsx b/src/App.tsx index 8d6d28e..1bcb543 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -19,7 +19,7 @@ function StepRail({ current, completedSteps, setCurrent }: { current: number; co
    {steps.map((step, index) => { const stepNumber = index + 1 const completed = completedSteps.has(stepNumber) - return
  1. + return
  2. +
    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 223/282] 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 224/282] 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 225/282] 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 226/282] 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 227/282] 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 228/282] 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 229/282] 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 230/282] 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 231/282] 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 232/282] 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 233/282] 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 234/282] 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 235/282] 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 236/282] 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 237/282] 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 238/282] 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 239/282] 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 240/282] 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 241/282] 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 242/282] 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 243/282] 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 244/282] 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 245/282] 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 246/282] 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 247/282] 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 248/282] 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 249/282] 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 250/282] 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 251/282] 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 252/282] 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 253/282] 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 254/282] 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 255/282] 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 256/282] 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 257/282] 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 258/282] 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 259/282] 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 260/282] 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 e3f8c6b07f97bc2e9658e47c323c733d135bb4b0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 23:13:04 +0900 Subject: [PATCH 261/282] docs: correct product readiness and license boundary --- CHANGELOG.md | 11 ++++ README.md | 78 ++++++++++++++++++++------ docs/product-technical-gap-baseline.md | 78 ++++++++++++++++++++++++++ 3 files changed, 151 insertions(+), 16 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 docs/product-technical-gap-baseline.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..4252cb0 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog + +All notable changes to PolicyWeave will be documented in this file. + +## Unreleased + +- Corrected the README to describe the repository as a non-installable concept + seed instead of an implemented npm application. +- Added an evidence-bound product and technical gap baseline. +- Made the absence of a license grant and the required provenance decision + explicit without assigning unverified rights. diff --git a/README.md b/README.md index fd97f15..70fb796 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,71 @@ # PolicyWeave -웹·앱 운영자가 실제 개인정보 처리 흐름을 입력하면, 누락과 모순을 표시하면서 개인정보처리방침 검토본을 만드는 로컬 우선 웹 앱입니다. +PolicyWeave is a proposed privacy-notice authoring product for teams that need +to turn an evidence-backed description of personal-data processing into a +reviewable draft. The intended value is to expose missing or contradictory +facts before publication and preserve why each disclosure was included. -> 생성 결과는 법률 자문이나 준법 보장이 아닙니다. 공개 전 개인정보보호책임자 또는 법률 전문가의 검토가 필요합니다. +> PolicyWeave does not provide legal advice or certify compliance. A privacy +> officer or qualified legal reviewer must approve any notice before use. -## 실행 +## Current status -```bash -npm install -npm run dev -``` +This repository is a **concept seed**. It does not yet contain an application, +package manifest, executable service, database schema, release, or published +documentation site. There is therefore no supported install, quickstart, API, +or deployment procedure yet. -검증은 `npm run lint`, `npm test`, `npm run build`로 수행합니다. +The previous `npm install` / `npm run dev` instructions and links to PRD and ADR +files were removed because the referenced implementation and documents are not +present on the protected branch. -## 현재 범위 +## Intended workflow -- 7단계 작성 흐름과 진행 상태 -- 개인정보 수집 항목 선택 및 필수/선택 구분 -- 조건부 상세 입력 -- 실시간 검토본과 법적 근거 표시 -- 차단 오류와 권장 검토 항목 구분 -- 반응형 작성/미리보기 전환 +The first usable release is expected to let an authorized product or privacy +team: -제품 요구사항과 결정 기록은 [`docs/PRD.md`](docs/PRD.md), [`docs/ADR-0001-policy-as-data.md`](docs/ADR-0001-policy-as-data.md)에 있습니다. +1. describe one processing activity and its purpose, data categories, people, + recipients, retention, legal basis, and international transfers; +2. see blocking omissions and non-blocking review prompts separately; +3. generate a versioned review draft with evidence and approval status; +4. revise or roll back the draft without losing its audit history; and +5. export only an approved version for publication. + +These are product requirements, not claims about implemented behavior. + +## Product boundary + +PolicyWeave should own privacy-notice authoring, review, approval, versioning, +and publication evidence. Identity, organizational policy, ontology labels, +and external publication remain separate responsibilities connected only +through released contracts. No integration is currently implemented. + +## Security and data handling + +Do not enter real personal information, credentials, customer records, or +production processing inventories into this repository. Before real use, the +product needs authenticated access, tenant isolation, purpose limitation, +retention and deletion controls, immutable audit evidence, encrypted storage, +backup and recovery, and tested export/publication authorization. + +## Development status and evidence + +The current requirements, missing technical foundations, and acceptance gates +are tracked in the +[product and technical gap baseline](docs/product-technical-gap-baseline.md). +Changes should keep customer-facing claims tied to protected-branch code, +tests, security evidence, and released artifacts. + +## Support + +Use this repository's GitHub Issues for product questions, defects, security +coordination, and implementation proposals. Do not include personal data or +secrets in an issue. + +## License + +No `LICENSE` file or verified rights grant is present. Copyright law therefore +applies by default: this repository does not currently grant permission to +use, copy, modify, or distribute its contents. A license may be added only +after ownership and inbound provenance are verified; this README does not +manufacture or imply those rights. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md new file mode 100644 index 0000000..ef61fa0 --- /dev/null +++ b/docs/product-technical-gap-baseline.md @@ -0,0 +1,78 @@ +# PolicyWeave product and technical gap baseline + +Status: **Proposed** +Evidence date: 2026-09-08 +Protected source examined: `main@52f4fd6bb68f870d0519cf11dd471573a2f197c0` + +## Goal and operating loop + +The product goal is a trustworthy workflow that converts verified processing +facts into an approved, versioned privacy-notice publication. Development +follows: establish a bounded requirement, add a failing contract, implement +the smallest owner-side behavior, obtain exact-head quality and security +evidence, release an immutable version, and validate the user workflow. + +## Current evidence + +At the protected revision above, the repository contains only `README.md`. +There is no application source, package metadata, dependency lock, schema, +test, workflow, container, release, Pages deployment, license grant, NOTICE, +or third-party attribution inventory. Earlier README references to an npm app, +PRD, and ADR were therefore not supported by repository evidence. + +## Proposed product boundary and Context Map + +PolicyWeave owns the **Privacy Notice Authoring** bounded context: processing +activity intake, disclosure completeness, draft versions, review decisions, +approval state, and publication evidence. + +Proposed external relationships remain unimplemented: + +- an identity and authorization provider supplies authenticated actor and + tenant decisions through a released Anti-Corruption Layer; +- organization and policy systems supply approved purpose and retention facts; +- ontology/catalog products may supply released reference labels but do not + own PolicyWeave's product language or notice decisions; +- a publication adapter receives only an approved immutable notice version; + it does not read PolicyWeave storage directly. + +No shared ContextualWisdomLab service is a mandatory dependency until its +protected contract, release, and operational maturity are verified. + +## Documentation and design baseline + +| Artifact | Current status | Required evidence | +| --- | --- | --- | +| PRD | Missing | Users, end-to-end scenes, exclusions, measurable outcomes, acceptance criteria | +| TRD | Missing | Runtime choice, APIs, state transitions, failure contracts, observability, deployment | +| ADR | Missing | Selected and rejected alternatives, constraints, evidence, risks, follow-up | +| UML | Missing | Authoring, review, approval, rollback, and publication sequences | +| ERD | Missing | 3NF tenant, activity, disclosure, draft version, decision, evidence, and publication model | +| UX | Missing | Normal, loading, empty, error, permission, responsive, and interaction states | +| Security | Missing | Threat model, authorization, isolation, encryption, audit, retention, recovery | +| Test strategy | Missing | Unit, contract, integration, browser E2E, accessibility, performance, failure evidence | +| Operability | Missing | Health, metrics, logs, backup/restore, rollback, incident and support procedures | + +## Buyer-visible gaps and actions + +| Gap | Evidence | Action | Exit status | +| --- | --- | --- | --- | +| No usable product | No executable source or package metadata | Define the first bounded authoring-to-review vertical in a PRD and implement it test-first | Open | +| No authoritative data model | No schema or ERD | Define minimal transaction aggregates and a 3NF persistence model with tenant and version invariants | Open | +| No approval/publication contract | No API, state machine, or immutable evidence | Specify fail-closed transitions and publish only approved immutable versions | Open | +| No security boundary | No authentication, authorization, or threat model | Establish purpose-limited tenant access, audit, encryption, retention, and recovery evidence | Open | +| No localization authority | No versioned resources or locale tests | Separate UI text from ontology labels; design DB-backed reviewed resources for ko/en/ja/zh/vi/es/de/fr | Open | +| No realistic UX evidence | No component catalog or browser tests | Define reusable components and validate all material states, keyboard access, wrapping, and responsive behavior | Open | +| No performance evidence | No service or load test | Measure the complete user path and publish reproducible latency and resource evidence without reducing workload | Blocked by implementation | +| No release/provenance | No CI, SBOM, artifact, or release | Add exact-head build, test, security, SBOM, provenance, immutable release, and rollback gates | Open | +| No rights grant | No LICENSE, NOTICE, or provenance inventory | Verify first-party ownership and inbound rights before selecting an ecosystem-consistent license | Blocked on rights evidence | + +## First release definition of done + +The first release is not complete until one authorized user can create a +processing activity, receive deterministic omission/contradiction results, +produce a versioned draft, record an independent approval decision, publish +only that approved version, and roll back publication. The same exact revision +must have realistic automated tests, accessibility and security evidence, +operable persistence and recovery, a complete dependency/provenance inventory, +and a verified license decision. From 4c74e5df9f4e4cc4be63790f2a44cdba8124b894 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 23:13:27 +0900 Subject: [PATCH 262/282] 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 = [ From 80b69d6a640194997906ec23ca0140f28fd95908 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 00:24:10 +0900 Subject: [PATCH 263/282] test(deps): reject mutable direct dependency specs --- src/dependency-manifest-contract.test.ts | 60 ++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 src/dependency-manifest-contract.test.ts diff --git a/src/dependency-manifest-contract.test.ts b/src/dependency-manifest-contract.test.ts new file mode 100644 index 0000000..b6b19b2 --- /dev/null +++ b/src/dependency-manifest-contract.test.ts @@ -0,0 +1,60 @@ +import { readFileSync } from 'node:fs' +import { fileURLToPath } from 'node:url' +import { describe, expect, it } from 'vitest' + +type DependencyManifest = { + dependencies?: Record + devDependencies?: Record +} + +type LockPackage = { + license?: string + version?: string +} + +type PackageLock = { + packages: Record +} + +const packagePath = fileURLToPath(new URL('../package.json', import.meta.url)) +const lockPath = fileURLToPath(new URL('../package-lock.json', import.meta.url)) +const packageManifest = JSON.parse(readFileSync(packagePath, 'utf8')) as DependencyManifest +const packageLock = JSON.parse(readFileSync(lockPath, 'utf8')) as PackageLock +const lockManifest = packageLock.packages[''] +const exactVersion = /^\d+\.\d+\.\d+$/ + +describe('direct dependency manifest contract', () => { + it('pins every direct dependency to the reviewed lock resolution', () => { + const directDependencies = { + ...packageManifest.dependencies, + ...packageManifest.devDependencies, + } + const mutableOrMismatched = Object.entries(directDependencies).filter( + ([name, version]) => + !exactVersion.test(version) || + packageLock.packages[`node_modules/${name}`]?.version !== version, + ) + + expect(mutableOrMismatched).toEqual([]) + }) + + it('keeps compiler and bundler packages out of production dependencies', () => { + const buildPackages = ['@vitejs/plugin-react', 'typescript', 'vite'] + + expect(buildPackages.filter((name) => name in (packageManifest.dependencies ?? {}))).toEqual([]) + expect(buildPackages.filter((name) => !(name in (packageManifest.devDependencies ?? {})))).toEqual([]) + }) + + it('keeps the lock root synchronized with the package manifest', () => { + expect(lockManifest.dependencies).toEqual(packageManifest.dependencies) + expect(lockManifest.devDependencies).toEqual(packageManifest.devDependencies) + }) + + it('retains a machine-readable license for every locked package', () => { + const missingLicenses = Object.entries(packageLock.packages) + .filter(([path, lockedPackage]) => path !== '' && !lockedPackage.license?.trim()) + .map(([path]) => path) + + expect(missingLicenses).toEqual([]) + }) +}) From cd9144f4a8a78427457bb58ee4b0a5f75a1ca750 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 00:31:44 +0900 Subject: [PATCH 264/282] build(deps): pin reviewed versions and isolate tooling --- package-lock.json | 88 ++++++++++++++++++++++++++++++++++------------- package.json | 2 +- 2 files changed, 66 insertions(+), 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index bd29e26..88c33ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,29 +9,29 @@ "version": "0.1.0", "license": "MIT", "dependencies": { - "@vitejs/plugin-react": "latest", - "lucide-react": "latest", - "react": "latest", - "react-dom": "latest", - "typescript": "^6.0.3", - "vite": "latest" + "lucide-react": "1.38.0", + "react": "19.2.8", + "react-dom": "19.2.8" }, "devDependencies": { - "@axe-core/playwright": "^4.13.0", - "@eslint/js": "latest", - "@playwright/test": "^1.63.0", - "@testing-library/jest-dom": "latest", - "@testing-library/react": "latest", - "@types/node": "^26.4.1", - "@types/react": "latest", - "@types/react-dom": "latest", - "eslint": "latest", - "eslint-plugin-react-hooks": "latest", - "eslint-plugin-react-refresh": "latest", - "globals": "latest", - "jsdom": "latest", - "typescript-eslint": "^8.68.0", - "vitest": "latest" + "@axe-core/playwright": "4.13.0", + "@eslint/js": "10.0.1", + "@playwright/test": "1.63.0", + "@testing-library/jest-dom": "7.0.1", + "@testing-library/react": "16.3.3", + "@types/node": "26.4.1", + "@types/react": "19.2.18", + "@types/react-dom": "19.2.5", + "@vitejs/plugin-react": "6.1.1", + "eslint": "10.9.1", + "eslint-plugin-react-hooks": "7.1.1", + "eslint-plugin-react-refresh": "0.5.5", + "globals": "17.11.0", + "jsdom": "30.0.1", + "typescript": "6.0.3", + "typescript-eslint": "8.68.0", + "vite": "8.2.2", + "vitest": "4.1.11" } }, "node_modules/@adobe/css-tools": { @@ -776,6 +776,7 @@ "version": "0.147.0", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.147.0.tgz", "integrity": "sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" @@ -804,6 +805,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -820,6 +822,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -836,6 +839,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -852,6 +856,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -868,6 +873,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -884,6 +890,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -900,6 +907,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -916,6 +924,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -932,6 +941,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -948,6 +958,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -964,6 +975,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -980,6 +992,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -996,6 +1009,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1012,6 +1026,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1028,6 +1043,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1041,6 +1057,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, "license": "MIT" }, "node_modules/@standard-schema/spec": { @@ -1186,7 +1203,7 @@ "version": "26.4.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-26.4.1.tgz", "integrity": "sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "undici-types": "~8.3.0" @@ -1459,6 +1476,7 @@ "version": "6.1.1", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.1.tgz", "integrity": "sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw==", + "dev": true, "license": "MIT", "dependencies": { "@rolldown/pluginutils": "^1.0.1" @@ -1928,6 +1946,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -2221,6 +2240,7 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -2289,6 +2309,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -2565,6 +2586,7 @@ "version": "1.33.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, "license": "MPL-2.0", "dependencies": { "detect-libc": "^2.0.3" @@ -2597,6 +2619,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2617,6 +2640,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2637,6 +2661,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2657,6 +2682,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2677,6 +2703,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2697,6 +2724,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2717,6 +2745,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2737,6 +2766,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2757,6 +2787,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2777,6 +2808,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2797,6 +2829,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2910,6 +2943,7 @@ "version": "3.3.18", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, "funding": [ { "type": "github", @@ -3049,12 +3083,14 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -3096,6 +3132,7 @@ "version": "8.5.26", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, "funding": [ { "type": "opencollective", @@ -3213,6 +3250,7 @@ "version": "1.2.6", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.6.tgz", "integrity": "sha512-vMM4q3aixf46GiF1Kok8jDPFsEpXgFWGjUHXNkNHNm+Y2adXAG2dbX91jkti3i0ZRsOlcmbuzAz1poObSHCmUA==", + "dev": true, "license": "MIT", "dependencies": { "@oxc-project/types": "=0.147.0", @@ -3305,6 +3343,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -3365,6 +3404,7 @@ "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -3463,6 +3503,7 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -3510,7 +3551,7 @@ "version": "8.3.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/update-browserslist-db": { @@ -3558,6 +3599,7 @@ "version": "8.2.2", "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==", + "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.33.0", diff --git a/package.json b/package.json index ee8a4f4..a6078bf 100644 --- a/package.json +++ b/package.json @@ -1 +1 @@ -{"name":"policyweave","private":true,"version":"0.1.0","license":"MIT","type":"module","scripts":{"dev":"vite --host 0.0.0.0","preview":"vite preview","build":"tsc -b && vite build","lint":"eslint .","test":"vitest run","test:watch":"vitest","test:e2e":"playwright test"},"dependencies":{"@vitejs/plugin-react":"latest","lucide-react":"latest","react":"latest","react-dom":"latest","typescript":"^6.0.3","vite":"latest"},"devDependencies":{"@axe-core/playwright":"^4.13.0","@eslint/js":"latest","@playwright/test":"^1.63.0","@testing-library/jest-dom":"latest","@testing-library/react":"latest","@types/node":"^26.4.1","@types/react":"latest","@types/react-dom":"latest","eslint":"latest","eslint-plugin-react-hooks":"latest","eslint-plugin-react-refresh":"latest","globals":"latest","jsdom":"latest","typescript-eslint":"^8.68.0","vitest":"latest"}} +{"name":"policyweave","private":true,"version":"0.1.0","license":"MIT","type":"module","scripts":{"dev":"vite --host 0.0.0.0","preview":"vite preview","build":"tsc -b && vite build","lint":"eslint .","test":"vitest run","test:watch":"vitest","test:e2e":"playwright test"},"dependencies":{"lucide-react":"1.38.0","react":"19.2.8","react-dom":"19.2.8"},"devDependencies":{"@axe-core/playwright":"4.13.0","@eslint/js":"10.0.1","@playwright/test":"1.63.0","@testing-library/jest-dom":"7.0.1","@testing-library/react":"16.3.3","@types/node":"26.4.1","@types/react":"19.2.18","@types/react-dom":"19.2.5","@vitejs/plugin-react":"6.1.1","eslint":"10.9.1","eslint-plugin-react-hooks":"7.1.1","eslint-plugin-react-refresh":"0.5.5","globals":"17.11.0","jsdom":"30.0.1","typescript":"6.0.3","typescript-eslint":"8.68.0","vite":"8.2.2","vitest":"4.1.11"}} From f6bbf9d6e5621ac26e3d1905556c192c2c8613c7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 00:34:48 +0900 Subject: [PATCH 265/282] test(ci): require exact-head CycloneDX evidence --- src/workflow-contract.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/workflow-contract.test.ts b/src/workflow-contract.test.ts index 269bc37..edddf69 100644 --- a/src/workflow-contract.test.ts +++ b/src/workflow-contract.test.ts @@ -11,3 +11,14 @@ describe('repository CI concurrency contract', () => { expect(workflowSource).toMatch(/cancel-in-progress:\s*true/) }) }) + +describe('repository dependency evidence contract', () => { + it('publishes a CycloneDX SBOM from the exact installed lock graph', () => { + expect(workflowSource).toMatch( + /name:\s*Generate exact-head dependency SBOM[\s\S]*npm sbom --sbom-format cyclonedx > test-results\/policyweave-dependency-sbom\.cdx\.json/, + ) + expect(workflowSource).toMatch( + /name:\s*policyweave-dependency-sbom[\s\S]*path:\s*test-results\/policyweave-dependency-sbom\.cdx\.json/, + ) + }) +}) From a3592da89c75be87564e1d4ec8d9e685e9e3093e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 00:39:16 +0900 Subject: [PATCH 266/282] ci(deps): publish exact-head CycloneDX evidence --- .github/workflows/ci.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 62fc759..31a7461 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,6 +34,10 @@ jobs: node-version: 22 cache: npm - run: npm ci + - name: Generate exact-head dependency SBOM + run: | + mkdir -p test-results + npm sbom --sbom-format cyclonedx > test-results/policyweave-dependency-sbom.cdx.json - run: npm run lint - run: npm test - run: npm run build @@ -70,3 +74,10 @@ jobs: name: policyweave-browser-evidence path: test-results if-no-files-found: warn + - name: Upload exact-head dependency SBOM + if: ${{ !cancelled() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: policyweave-dependency-sbom + path: test-results/policyweave-dependency-sbom.cdx.json + if-no-files-found: error From c0c2b0a31e31130adf8e04af91d1d5f2558d1f8a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 00:42:18 +0900 Subject: [PATCH 267/282] docs(deps): bind supply-chain evidence and gaps --- AGENTS.md | 1 + CHANGELOG.md | 2 ++ docs/SECURITY.md | 3 ++- docs/TRD.md | 2 ++ docs/product-technical-gap-baseline.md | 7 +++++-- 5 files changed, 12 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index e8e9074..90bf5a7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,6 +14,7 @@ PolicyWeave is a local-first privacy-policy fact-authoring workspace. It structu - Keep persistence objects semantically named with at least two words and `snake_case` unless a framework contract requires another convention. Avoid generic named persistence objects such as a standalone `id` table/collection. - Hosted persistence/publication must be introduced only behind explicit tenant, authorization, audit, encryption, immutable revision, and supersession contracts. CI PostgreSQL restart and dump/restore evidence lives in `db/tests/policy_revision_restore.sh` and does not enable a hosted adapter. Seed `retention_status = applies` only in the same transaction as its `retention_rule`; autocommit fails the deferred fact contract. - GitHub Actions dependencies stay SHA pinned and checkout credentials must not persist. +- Direct npm dependencies stay pinned to their reviewed lock resolutions. Compiler and bundler packages belong in `devDependencies`, and exact-head CI publishes a CycloneDX dependency SBOM without treating license metadata as legal approval. ## Verification The minimum exact-head gate is `npm run lint`, `npm test`, and `npm run build`, plus every live organization-required workflow, independent approval, and resolved review thread. Queued, skipped, predecessor-head, or stale results are not passing evidence. diff --git a/CHANGELOG.md b/CHANGELOG.md index e5ecd76..931da61 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 +- Executable npm manifest/lock/license contracts and an exact-head CycloneDX SBOM artifact. Every direct declaration must equal its reviewed lock resolution, the lock root must match the manifest, and every locked package must retain machine-readable license metadata. - 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. @@ -20,6 +21,7 @@ All notable product changes are recorded here. PolicyWeave is pre-release; entri - 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 +- All direct npm packages now use exact reviewed versions. React and Lucide remain runtime dependencies; TypeScript, Vite, and the React Vite plugin are correctly classified with the test/build toolchain in `devDependencies`, and npm regenerated the lock graph so transitive development scope is accurate. - 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. - Repository CI now starts one digest-pinned PostgreSQL 18 service inside the existing verification job and runs the migration contract before browser evidence, avoiding a second workflow or runner while producing real database evidence. - Repository CI now groups runs by workflow plus pull-request number or branch ref and cancels superseded heads, preventing stale queued runs from consuming runner capacity without coupling unrelated PRs or refs. diff --git a/docs/SECURITY.md b/docs/SECURITY.md index 4c74b3a..697447c 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -27,9 +27,10 @@ Protected assets include policy facts, contact details, processing descriptions, 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. 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. +10. Direct npm declarations use exact reviewed lock resolutions, and compiler/bundler packages are development-only. Exact-head CI emits a CycloneDX SBOM from the installed lock graph; the package-lock license inventory supports review but does not itself approve license compatibility. ## 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. +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 plus the dependency manifest contract and CycloneDX artifact. Vulnerability or license inventory is evidence for review, not a substitute for an explicit release decision. ## Local JSON export diff --git a/docs/TRD.md b/docs/TRD.md index a8abba1..f80ffdb 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -12,6 +12,7 @@ This TRD covers the pre-release PolicyWeave browser workspace and the contracts - `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, 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. +- Direct npm declarations equal the exact reviewed versions resolved by `package-lock.json`. React and Lucide are the only runtime packages; the compiler, bundler, Vite React plugin, lint, test, browser, type, and DOM harnesses are development packages. CI generates a CycloneDX 1.5 SBOM from the exact installed lock graph and retains it as a head-bound artifact; license fields are inventory evidence, not a legal compatibility decision. ## Functional contracts 1. Every PRD step must route to an editable surface; selecting a step cannot change only the rail indicator. @@ -34,6 +35,7 @@ The separation between collection and retention follows the PIPC Standard Person - Touched production behavior requires regression and edge-case tests first. - Touched production functions carry descriptive JSDoc rather than relying on implicit behavior. - Do not suppress deprecation warnings to pass CI. +- Dependency changes must update the manifest and npm-generated lock graph together, keep build-only tools out of production dependencies, retain machine-readable license inventory, and reacquire exact-head vulnerability, SBOM, and license evidence. - Browser/accessibility validation must include keyboard/focus order, focus-target viewport visibility, WCAG 2.2 automated checks, responsive desktop/tablet/mobile interactions, and screenshot evidence before a buyer-facing accessibility claim. Deterministic jsdom focus-transition coverage is supporting evidence only and does not substitute for real-browser, zoom, screen-reader, or responsive verification. - 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. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index ef224da..4f383ef 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -68,6 +68,8 @@ The subsequent Codex P1 review found that the validator and preview accepted que 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. +The dependency-governance pass found that every direct npm declaration was either `latest` or a mutable caret range and that TypeScript, Vite, and the React Vite plugin were incorrectly classified as production dependencies. Test-only head `80b69d6a640194997906ec23ca0140f28fd95908` produced exact RED CI `34244619215`: clean install and lint passed, 73 predecessor tests passed, and only the two new pin/classification contracts failed. Minimal implementation `cd9144f4a8a78427457bb58ee4b0a5f75a1ca750` pins all 21 direct packages to their existing reviewed lock resolutions, moves the three build packages to `devDependencies`, and uses npm to regenerate development-scope metadata without changing any resolved package version. A second test-only head `f6bbf9d6e5621ac26e3d1905556c192c2c8613c7` produced exact RED CI `34245711353`: 75 tests passed and only the missing CycloneDX publication contract failed. Workflow repair `a3592da89c75be87564e1d4ec8d9e685e9e3093e` generates a CycloneDX 1.5 SBOM from the exact installed lock graph and uploads it separately through the existing SHA-pinned artifact action. The lock/license and SBOM evidence support security and legal review but do not themselves approve dependency licenses or replace organization vulnerability gates. + ## Current baseline | Area | Evidence | Status | Commercialization gap | Owner/action | Next verification | @@ -81,7 +83,8 @@ 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, 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 | +| Dependency supply chain | Exact-pinned manifest, npm-regenerated lock graph, executable pin/scope/license contracts, and CycloneDX 1.5 CI artifact | Repaired foundation on open stack; exact-head release gate still applies | License compatibility approval and organization vulnerability verdict remain external review responsibilities | PolicyWeave owns direct dependency intent; CI/Security owners retain scanning and release gates | Exact-head clean install, 76 tests, build, SBOM artifact/digest, vulnerability checks, and explicit license review | +| Tests | 76 unit/UI/schema/workflow/dependency regressions plus PostgreSQL runtime/concurrency/restore scripts, 23 Playwright/axe passes, ten intentional project-scope skips, real-browser interaction/download/error contracts, and screenshot/SBOM artifact contracts | Improved; bounded exact-head verification required after every head movement | Repository-wide 100% execution/docstring coverage is not yet evidenced | Test Engineering: measure coverage and extend realistic edge cases | Exact-head coverage + browser/database/dependency 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 | @@ -96,7 +99,7 @@ The Proposed aggregate root is the minimal `policy_revision` boundary. The no-co A publishable release requires complete fact-to-warning navigation, deterministic rendering of captured source facts, no inferred customer operational facts, truthful first-responsibility startup/progress, explicit unresolved-review blocking across the seven authoring responsibilities, an explicit no-collection path that does not infer unrelated processing facts, independent explicit retention status, immutable reviewed publication revisions, accessible responsive interaction, authoritative source/version traceability for legal rules, hosted security/privacy evidence, and exact-head CI/security/independent review. Buyer-facing wording describes assistance and reviewability, never a compliance guarantee or an internal implementation boundary. ## Active commercialization order -1. Reacquire exact-current-head CI/security/SAST/central workflow execution and independent approval; merge PR #1 only through ordinary protection. +1. Reacquire exact-current-head CI/security/SAST/central workflow execution, dependency SBOM/license evidence, and independent approval; merge PR #1 only through ordinary protection. 2. Extend the exact-head responsive/accessibility evidence with screen-reader checks, native browser-zoom automation when the harness supports it, and manual records; CSS focus contrast, logical step-focus transfer, focused-heading viewport visibility, retention transitions, effective 200% browser-zoom reflow, and current desktop/tablet/mobile screenshots are already verified. 3. Preserve exact-head PostgreSQL 18 migration, domain-error, two-session lock, restart, and dump/restore evidence, then prove tenant authorization, immutable audit, and encryption before enabling any hosted adapter. 4. Implement secure review/publication with immutable releases and explicit supersession. From 3c8ac646c7ccd0140976f3fcefc914fc4b9a7d80 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 00:45:04 +0900 Subject: [PATCH 268/282] test(ci): isolate dependency evidence artifact --- src/workflow-contract.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/workflow-contract.test.ts b/src/workflow-contract.test.ts index edddf69..eb42cea 100644 --- a/src/workflow-contract.test.ts +++ b/src/workflow-contract.test.ts @@ -15,10 +15,10 @@ describe('repository CI concurrency contract', () => { describe('repository dependency evidence contract', () => { it('publishes a CycloneDX SBOM from the exact installed lock graph', () => { expect(workflowSource).toMatch( - /name:\s*Generate exact-head dependency SBOM[\s\S]*npm sbom --sbom-format cyclonedx > test-results\/policyweave-dependency-sbom\.cdx\.json/, + /name:\s*Generate exact-head dependency SBOM[\s\S]*npm sbom --sbom-format cyclonedx > dependency-evidence\/policyweave-dependency-sbom\.cdx\.json/, ) expect(workflowSource).toMatch( - /name:\s*policyweave-dependency-sbom[\s\S]*path:\s*test-results\/policyweave-dependency-sbom\.cdx\.json/, + /name:\s*policyweave-dependency-sbom[\s\S]*path:\s*dependency-evidence\/policyweave-dependency-sbom\.cdx\.json/, ) }) }) From 00b89fb6c986b22e2453a58f4b6dc61ba7ff1dff Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 00:48:45 +0900 Subject: [PATCH 269/282] ci(deps): isolate CycloneDX artifact --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 31a7461..79ba22f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,8 +36,8 @@ jobs: - run: npm ci - name: Generate exact-head dependency SBOM run: | - mkdir -p test-results - npm sbom --sbom-format cyclonedx > test-results/policyweave-dependency-sbom.cdx.json + mkdir -p dependency-evidence + npm sbom --sbom-format cyclonedx > dependency-evidence/policyweave-dependency-sbom.cdx.json - run: npm run lint - run: npm test - run: npm run build @@ -79,5 +79,5 @@ jobs: uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: policyweave-dependency-sbom - path: test-results/policyweave-dependency-sbom.cdx.json + path: dependency-evidence/policyweave-dependency-sbom.cdx.json if-no-files-found: error From 71fe2751feb6dc4359093386f8cd30333e2f8795 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 00:50:43 +0900 Subject: [PATCH 270/282] docs(deps): record artifact isolation evidence --- 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 4f383ef..a4794e5 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -68,7 +68,7 @@ The subsequent Codex P1 review found that the validator and preview accepted que 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. -The dependency-governance pass found that every direct npm declaration was either `latest` or a mutable caret range and that TypeScript, Vite, and the React Vite plugin were incorrectly classified as production dependencies. Test-only head `80b69d6a640194997906ec23ca0140f28fd95908` produced exact RED CI `34244619215`: clean install and lint passed, 73 predecessor tests passed, and only the two new pin/classification contracts failed. Minimal implementation `cd9144f4a8a78427457bb58ee4b0a5f75a1ca750` pins all 21 direct packages to their existing reviewed lock resolutions, moves the three build packages to `devDependencies`, and uses npm to regenerate development-scope metadata without changing any resolved package version. A second test-only head `f6bbf9d6e5621ac26e3d1905556c192c2c8613c7` produced exact RED CI `34245711353`: 75 tests passed and only the missing CycloneDX publication contract failed. Workflow repair `a3592da89c75be87564e1d4ec8d9e685e9e3093e` generates a CycloneDX 1.5 SBOM from the exact installed lock graph and uploads it separately through the existing SHA-pinned artifact action. The lock/license and SBOM evidence support security and legal review but do not themselves approve dependency licenses or replace organization vulnerability gates. +The dependency-governance pass found that every direct npm declaration was either `latest` or a mutable caret range and that TypeScript, Vite, and the React Vite plugin were incorrectly classified as production dependencies. Test-only head `80b69d6a640194997906ec23ca0140f28fd95908` produced exact RED CI `34244619215`: clean install and lint passed, 73 predecessor tests passed, and only the two new pin/classification contracts failed. Minimal implementation `cd9144f4a8a78427457bb58ee4b0a5f75a1ca750` pins all 21 direct packages to their existing reviewed lock resolutions, moves the three build packages to `devDependencies`, and uses npm to regenerate development-scope metadata without changing any resolved package version. A second test-only head `f6bbf9d6e5621ac26e3d1905556c192c2c8613c7` produced exact RED CI `34245711353`: 75 tests passed and only the missing CycloneDX publication contract failed. Workflow repair `a3592da89c75be87564e1d4ec8d9e685e9e3093e` generates a CycloneDX 1.5 SBOM from the exact installed lock graph and uploads it through the existing SHA-pinned artifact action. A final storage review found that placing the SBOM below `test-results` duplicated it in both browser and dependency artifacts. Test-only head `3c8ac646c7ccd0140976f3fcefc914fc4b9a7d80` produced exact RED CI `34246822467` with 75 passing tests and only the new artifact-isolation contract failing; minimal repair `00b89fb6c986b22e2453a58f4b6dc61ba7ff1dff` writes the SBOM once below `dependency-evidence` and uploads only that path as the dependency artifact. The lock/license and SBOM evidence support security and legal review but do not themselves approve dependency licenses or replace organization vulnerability gates. ## Current baseline From eff7ec19f511cc5850f4fd1254f0a20d9c47fc9e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 01:26:05 +0900 Subject: [PATCH 271/282] test(deps): close exact-head review gaps --- docs/TRD.md | 2 +- docs/product-technical-gap-baseline.md | 2 +- src/dependency-manifest-contract.test.ts | 10 +++++----- src/workflow-contract.test.ts | 3 +++ 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/TRD.md b/docs/TRD.md index f80ffdb..0d9e19b 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -12,7 +12,7 @@ This TRD covers the pre-release PolicyWeave browser workspace and the contracts - `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, 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. -- Direct npm declarations equal the exact reviewed versions resolved by `package-lock.json`. React and Lucide are the only runtime packages; the compiler, bundler, Vite React plugin, lint, test, browser, type, and DOM harnesses are development packages. CI generates a CycloneDX 1.5 SBOM from the exact installed lock graph and retains it as a head-bound artifact; license fields are inventory evidence, not a legal compatibility decision. +- Direct npm declarations equal the exact reviewed versions resolved by `package-lock.json`. `react`, `react-dom`, and `lucide-react` are the only runtime packages; the compiler, bundler, Vite React plugin, lint, test, browser, type, and DOM harnesses are development packages. CI generates a CycloneDX 1.5 SBOM from the exact installed lock graph and retains it as a head-bound artifact; license fields are inventory evidence, not a legal compatibility decision. ## Functional contracts 1. Every PRD step must route to an editable surface; selecting a step cannot change only the rail indicator. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index a4794e5..26d2fe3 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -68,7 +68,7 @@ The subsequent Codex P1 review found that the validator and preview accepted que 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. -The dependency-governance pass found that every direct npm declaration was either `latest` or a mutable caret range and that TypeScript, Vite, and the React Vite plugin were incorrectly classified as production dependencies. Test-only head `80b69d6a640194997906ec23ca0140f28fd95908` produced exact RED CI `34244619215`: clean install and lint passed, 73 predecessor tests passed, and only the two new pin/classification contracts failed. Minimal implementation `cd9144f4a8a78427457bb58ee4b0a5f75a1ca750` pins all 21 direct packages to their existing reviewed lock resolutions, moves the three build packages to `devDependencies`, and uses npm to regenerate development-scope metadata without changing any resolved package version. A second test-only head `f6bbf9d6e5621ac26e3d1905556c192c2c8613c7` produced exact RED CI `34245711353`: 75 tests passed and only the missing CycloneDX publication contract failed. Workflow repair `a3592da89c75be87564e1d4ec8d9e685e9e3093e` generates a CycloneDX 1.5 SBOM from the exact installed lock graph and uploads it through the existing SHA-pinned artifact action. A final storage review found that placing the SBOM below `test-results` duplicated it in both browser and dependency artifacts. Test-only head `3c8ac646c7ccd0140976f3fcefc914fc4b9a7d80` produced exact RED CI `34246822467` with 75 passing tests and only the new artifact-isolation contract failing; minimal repair `00b89fb6c986b22e2453a58f4b6dc61ba7ff1dff` writes the SBOM once below `dependency-evidence` and uploads only that path as the dependency artifact. The lock/license and SBOM evidence support security and legal review but do not themselves approve dependency licenses or replace organization vulnerability gates. +The dependency-governance pass found that every direct npm declaration was either `latest` or a mutable caret range and that TypeScript, Vite, and the React Vite plugin were incorrectly classified as production dependencies. Test-only head `80b69d6a640194997906ec23ca0140f28fd95908` produced exact RED CI `34244619215`: clean install and lint passed, 73 predecessor tests passed, and only the two new pin/classification contracts failed. Minimal implementation `cd9144f4a8a78427457bb58ee4b0a5f75a1ca750` pins all 21 direct packages to their existing reviewed lock resolutions, moves the three build packages to `devDependencies`, and uses npm to regenerate development-scope metadata without changing any resolved package version. A second test-only head `f6bbf9d6e5621ac26e3d1905556c192c2c8613c7` produced exact RED CI `34245711353`: 75 tests passed and only the missing CycloneDX publication contract failed. Workflow repair `a3592da89c75be87564e1d4ec8d9e685e9e3093e` generates a CycloneDX 1.5 SBOM from the exact installed lock graph and uploads it through the existing SHA-pinned artifact action. A final storage review found that placing the SBOM below `test-results` duplicated it in both browser and dependency artifacts. Test-only head `3c8ac646c7ccd0140976f3fcefc914fc4b9a7d80` produced exact RED CI `34246822467` with 75 passing tests and only the new artifact-isolation contract failing; minimal repair `00b89fb6c986b22e2453a58f4b6dc61ba7ff1dff` writes the SBOM once below `dependency-evidence` and uploads only that path as the dependency artifact. Current review follow-up independently checks runtime and development declarations, asserts both artifact destinations, and names all three runtime packages consistently. The lock/license and SBOM evidence support security and legal review but do not themselves approve dependency licenses or replace organization vulnerability gates. ## Current baseline diff --git a/src/dependency-manifest-contract.test.ts b/src/dependency-manifest-contract.test.ts index b6b19b2..e6e8d56 100644 --- a/src/dependency-manifest-contract.test.ts +++ b/src/dependency-manifest-contract.test.ts @@ -25,11 +25,11 @@ const exactVersion = /^\d+\.\d+\.\d+$/ describe('direct dependency manifest contract', () => { it('pins every direct dependency to the reviewed lock resolution', () => { - const directDependencies = { - ...packageManifest.dependencies, - ...packageManifest.devDependencies, - } - const mutableOrMismatched = Object.entries(directDependencies).filter( + const directDependencies = [ + ...Object.entries(packageManifest.dependencies ?? {}), + ...Object.entries(packageManifest.devDependencies ?? {}), + ] + const mutableOrMismatched = directDependencies.filter( ([name, version]) => !exactVersion.test(version) || packageLock.packages[`node_modules/${name}`]?.version !== version, diff --git a/src/workflow-contract.test.ts b/src/workflow-contract.test.ts index eb42cea..ee84add 100644 --- a/src/workflow-contract.test.ts +++ b/src/workflow-contract.test.ts @@ -20,5 +20,8 @@ describe('repository dependency evidence contract', () => { expect(workflowSource).toMatch( /name:\s*policyweave-dependency-sbom[\s\S]*path:\s*dependency-evidence\/policyweave-dependency-sbom\.cdx\.json/, ) + expect(workflowSource).toMatch( + /name:\s*policyweave-browser-evidence[\s\S]*path:\s*test-results/, + ) }) }) From d0da193c48932311d970e55738ff1a12983ff3e3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 10:18:04 +0900 Subject: [PATCH 272/282] test: bind cross-state seed and query predicates Addresses PR #14 P2 review: the restored-URL and collection_without_retention_count markers alone could not detect a coupled seed+query change of revision ...0003 from retention none to applies. The contract now requires the ...0003 none seed tuple, its support_email item seed, and the revision-specific query predicates together. Negative control: old markers pass the coupled mutation, new predicates fail it; all 63 tests pass on the correct script. --- src/postgres-restore-contract.test.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/postgres-restore-contract.test.ts b/src/postgres-restore-contract.test.ts index 99c8d2e..e043d70 100644 --- a/src/postgres-restore-contract.test.ts +++ b/src/postgres-restore-contract.test.ts @@ -29,6 +29,15 @@ describe('PostgreSQL restart and restore evidence contract', () => { ) expect(restoreTest).toContain("stored_service_url is distinct from 'https://restore.example.test'") expect(restoreTest).toContain('collection_without_retention_count <> 1') + expect(restoreTest).toContain( + "60000000-0000-4000-8000-000000000003', '50000000-0000-4000-8000-000000000001', 3, 'none'", + ) + expect(restoreTest).toMatch( + /upsert_collection_item\(\s*'60000000-0000-4000-8000-000000000003',\s*'support_email'/, + ) + expect(restoreTest).toMatch( + /revision\.policy_revision_id = '60000000-0000-4000-8000-000000000003'[\s\S]*?revision\.no_collection_confirmed = false[\s\S]*?revision\.retention_status = 'none'[\s\S]*?item\.collection_item_key = 'support_email'/, + ) expect(restoreTest).toContain('no-collection confirmation conflicts with collection items') expect(workflowSource).toContain('run: sh db/tests/policy_revision_restore.sh') }) From 369cfe2624a4431489d0dc6050ebd0948486ad3a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 10:38:33 +0900 Subject: [PATCH 273/282] docs: record 2026-09-09 stack-collapse merge ledger PRs 2-8 plus 13-14 merged via ordinary merge commits with exact-head verify GREEN and zero unresolved threads re-fetched per merge; PR14 P2 coupled-mutation finding fixed at d0da193 with negative control and thread resolution; no PR closed; drafts 1, 10, 11 retain valid deltas; PR1 blockers (CodeQL-compat via central owner path, pins via down-chain successors, sole-maintainer approval) and chain topology recorded. --- 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 7983d93..a34225c 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1,6 +1,6 @@ # Product and technical gap baseline -Last reconciled: 2026-09-08 +Last reconciled: 2026-09-09 This ledger records PolicyWeave's commercialization gap from PRD/ADR/TRD/architecture, implementation, authoritative legal-source evidence, review findings, and live GitHub state. Live Check conclusions are not committed because they change outside the repository; merge decisions must re-fetch the exact current PR head and live ruleset. @@ -50,6 +50,8 @@ 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. PR #9 merged into the concurrent-writer stack at `5309a6141d8f3f0c89e9cfdaae04f4644a8dfcd4`. The TypeScript restore contract now binds restored service URL survival and the collecting-without-retention cross-state fixture. This is CI durability evidence, not operational backup, tenant authorization, or a released datastore. +On 2026-09-09 the review-to-merge loop collapsed the writer stack through ordinary merge commits without force-push, branch deletion, or protection changes, following the PR #9 precedent for unprotected stacked branches. PRs #2, #3, #4, #5, #6, #7, and #8 merged bottom-up between 01:10:44Z and 01:14:13Z, each with exact-head `verify` GREEN, zero unresolved review threads, and MERGEABLE/CLEAN state re-fetched immediately before its merge. The local-only restore-contract commit `878d0ce` was pushed as a new branch and opened as successor PR #14 on the PR #8 head; its Codex P2 review correctly showed the URL and `collection_without_retention_count` markers could not detect a coupled revision-`...0003` seed-plus-query change from `none` to `applies`. Commit `d0da193` binds the `...0003`/`none` seed tuple, its `support_email` item seed, and the revision-specific query predicates together; a negative control proved the old markers pass that coupled mutation while the new predicates fail it, and the review thread was resolved with that evidence. CI `34298653319` passed on `d0da193`, and PR #14 merged at 01:25:12Z. PR #13 (dependency pin and toolchain classification for issue #12) merged at 01:19:32Z on its immutable GREEN pair of head `eff7ec19f511` and CI `34251045288`, advancing the docs readiness branch to `bbea5c9`, whose fresh `verify` (`34298735574`) is GREEN. No PR was closed: 9 merges leave drafts #1, #10, and #11 open, each retaining a valid delta. `develop` still declares 16 `latest` occurrences, which explains PR #1's `dependency-review` failure; the pinned manifest (zero `latest`) sits at the top of the side chain and reaches `develop` only through the PR #10 to #11 to down-chain successor path. PR #1's remaining failures are the two CodeQL compatibility analyses plus `dependency-review`; `verify`, Noema, OpenCode, Strix, Semgrep, Trivy, OSV, and Scorecard are GREEN on its current head. Central-workflow run logs live in the organization central repository, so a leaf-side run fetch returns 404; such incidents go through the central owner path rather than leaf-side churn or bypass. The chain tip carrying the full writer delta is `origin/codex/policyweave-concurrent-writer-evidence` (`df9eef5`), with sibling successors `codex/policyweave-json-export` (PR #10, draft; its buyer-visible export enablement needs product review plus a base update now that its base moved) and this ledger branch stacked above it. The repository has no scheduled workflow (only `ci.yml`, no `schedule` trigger), so the hourly cadence is served by the autoresearch loop's own status messages rather than an in-repo scheduler. This remains CI durability evidence with no hosted adapter, tenant authorization, audit, encryption, or released datastore. + ## Current baseline | Area | Evidence | Status | Commercialization gap | Owner/action | Next verification | @@ -77,7 +79,7 @@ The Proposed aggregate root is the minimal `policy_revision` boundary. The no-co A publishable release requires complete fact-to-warning navigation, deterministic rendering of captured source facts, no inferred customer operational facts, truthful first-responsibility startup/progress, explicit unresolved-review blocking across the seven authoring responsibilities, an explicit no-collection path that does not infer unrelated processing facts, independent explicit retention status, immutable reviewed publication revisions, accessible responsive interaction, authoritative source/version traceability for legal rules, hosted security/privacy evidence, and exact-head CI/security/independent review. Buyer-facing wording describes assistance and reviewability, never a compliance guarantee or an internal implementation boundary. ## Active commercialization order -1. Reacquire exact-current-head CI/security/SAST/central workflow execution and independent approval; merge PR #1 only through ordinary protection. +1. Land the full-delta tip (`codex/policyweave-concurrent-writer-evidence`) into `develop` through a successor PR with exact-head evidence; advance draft #10 (export enablement needs product review plus a base update) and draft #11; clear PR #1's CodeQL-compat failures through the central owner path, deliver pins down-chain, and secure independent approval (sole-maintainer approval is the structural blocker); merge PR #1 only through ordinary protection. 2. Extend the exact-head responsive/accessibility evidence with screen-reader checks, native browser-zoom automation when the harness supports it, and manual records; CSS focus contrast, logical step-focus transfer, focused-heading viewport visibility, retention transitions, effective 200% browser-zoom reflow, and current desktop/tablet/mobile screenshots are already verified. 3. Preserve exact-head PostgreSQL 18 migration, domain-error, two-session lock, restart, and dump/restore evidence, then prove tenant authorization, immutable audit, and encryption before enabling any hosted adapter. 4. Implement secure review/publication with immutable releases and explicit supersession. From 29f91f9c651cf5df5a8ee723b880c044356df47f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 11:34:10 +0900 Subject: [PATCH 274/282] docs: record PR16 landing plus side-chain restacks PR #16 landed the writer-stack tip in develop; PRs #10/#11 restacked without force with exact-head CI GREEN and zero unresolved threads. Records PR #1's remaining blockers. Docs-only. --- 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 a34225c..ff966c9 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -52,6 +52,8 @@ The restart/restore slice remains bounded CI evidence and does not enable a host On 2026-09-09 the review-to-merge loop collapsed the writer stack through ordinary merge commits without force-push, branch deletion, or protection changes, following the PR #9 precedent for unprotected stacked branches. PRs #2, #3, #4, #5, #6, #7, and #8 merged bottom-up between 01:10:44Z and 01:14:13Z, each with exact-head `verify` GREEN, zero unresolved review threads, and MERGEABLE/CLEAN state re-fetched immediately before its merge. The local-only restore-contract commit `878d0ce` was pushed as a new branch and opened as successor PR #14 on the PR #8 head; its Codex P2 review correctly showed the URL and `collection_without_retention_count` markers could not detect a coupled revision-`...0003` seed-plus-query change from `none` to `applies`. Commit `d0da193` binds the `...0003`/`none` seed tuple, its `support_email` item seed, and the revision-specific query predicates together; a negative control proved the old markers pass that coupled mutation while the new predicates fail it, and the review thread was resolved with that evidence. CI `34298653319` passed on `d0da193`, and PR #14 merged at 01:25:12Z. PR #13 (dependency pin and toolchain classification for issue #12) merged at 01:19:32Z on its immutable GREEN pair of head `eff7ec19f511` and CI `34251045288`, advancing the docs readiness branch to `bbea5c9`, whose fresh `verify` (`34298735574`) is GREEN. No PR was closed: 9 merges leave drafts #1, #10, and #11 open, each retaining a valid delta. `develop` still declares 16 `latest` occurrences, which explains PR #1's `dependency-review` failure; the pinned manifest (zero `latest`) sits at the top of the side chain and reaches `develop` only through the PR #10 to #11 to down-chain successor path. PR #1's remaining failures are the two CodeQL compatibility analyses plus `dependency-review`; `verify`, Noema, OpenCode, Strix, Semgrep, Trivy, OSV, and Scorecard are GREEN on its current head. Central-workflow run logs live in the organization central repository, so a leaf-side run fetch returns 404; such incidents go through the central owner path rather than leaf-side churn or bypass. The chain tip carrying the full writer delta is `origin/codex/policyweave-concurrent-writer-evidence` (`df9eef5`), with sibling successors `codex/policyweave-json-export` (PR #10, draft; its buyer-visible export enablement needs product review plus a base update now that its base moved) and this ledger branch stacked above it. The repository has no scheduled workflow (only `ci.yml`, no `schedule` trigger), so the hourly cadence is served by the autoresearch loop's own status messages rather than an in-repo scheduler. This remains CI durability evidence with no hosted adapter, tenant authorization, audit, encryption, or released datastore. +PR #16 landed the full writer-stack tip in `develop` at `7bf0fa9`. The side chain was then restacked without force: PR #10 head merged tip `111400a` via `cef8c0b` (auto-merged baseline/test regions verified coherent with no markers; merged-tree local gates GREEN with 71/71 tests), and PR #11 head merged the restacked export head via `e67c34c` with one Active-order conflict resolved by preserving both sides. Merged-tree local gates were GREEN (lint, 76/76 tests, build). Both restacks requested fresh CodeRabbit review; exact-head CI `34301731941` (PR #10) and `34302266597` (PR #11) are GREEN on the restacked heads with zero unresolved threads on either. PR #1 on the new `develop` head still fails only the two CodeQL compatibility analyses plus `dependency-review` (16 `latest` declarations remain in `develop`; pins travel down-chain after the export/readiness drafts land), while every other central check is GREEN. No PR was closed in this turn; drafts #1, #10, and #11 retain valid deltas. + ## Current baseline | Area | Evidence | Status | Commercialization gap | Owner/action | Next verification | From 77bf3c23e4d4b8e19ca24214084f2f7009c13119 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 13:41:46 +0900 Subject: [PATCH 275/282] docs: align PR11 scope sentence with dependency-governance delta --- 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 e88da8c..6a1b5ad 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -6,7 +6,7 @@ This ledger records PolicyWeave's commercialization gap from PRD/ADR/TRD/archite ## Protected integration truth -The default branch remains `main@52f4fd6bb68f870d0519cf11dd471573a2f197c0`, whose tree contains only the historical README. The executable application, MIT license grant, PRD/TRD/ADRs, database contracts, tests, and evidence described below are proposed on the open PR #1–#10 stack; they are not yet protected-branch or released product truth. PR #11's original README-only diagnosis was therefore valid for `main`, but its separate main-based replacement tree would have competed with and contradicted the canonical product writer. PR #11 is non-force restacked on the current stack and preserves only this integration-truth gap. Any parent movement requires fresh exact-head verification before ordinary merge. +The default branch remains `main@52f4fd6bb68f870d0519cf11dd471573a2f197c0`, whose tree contains only the historical README. The executable application, MIT license grant, PRD/TRD/ADRs, database contracts, tests, and evidence described below are proposed on the open PR #1–#10 stack; they are not yet protected-branch or released product truth. PR #11's original README-only diagnosis was therefore valid for `main`, but its separate main-based replacement tree would have competed with and contradicted the canonical product writer. PR #11 is non-force restacked on the current stack and preserves this integration-truth ledger together with its dependency-governance delta (exact-pinned manifest, regenerated lockfile, manifest/scope/license contracts, CI CycloneDX SBOM evidence, and workflow artifact isolation). Any parent movement requires fresh exact-head verification before ordinary merge. ## Product responsibility PolicyWeave is a local-first policy-fact workspace that helps a service operator structure actual personal-data processing facts, detect omissions or contradictions, and render a reviewable privacy-policy draft. It is not legal advice and does not claim compliance or certification. Structured facts are the source of truth; rendered prose is a deterministic projection over facts plus future versioned rule/template evidence. From ea30067dfa4a2ff9aa2d069bf89146b1d63cb093 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 14:13:03 +0900 Subject: [PATCH 276/282] docs: record side-chain landing plus merge know-how --- AGENTS.md | 4 ++++ CLAUDE.md | 4 ++++ docs/product-technical-gap-baseline.md | 4 ++-- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 90bf5a7..e4f5bde 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,3 +18,7 @@ PolicyWeave is a local-first privacy-policy fact-authoring workspace. It structu ## Verification The minimum exact-head gate is `npm run lint`, `npm test`, and `npm run build`, plus every live organization-required workflow, independent approval, and resolved review thread. Queued, skipped, predecessor-head, or stale results are not passing evidence. + +## Know-how +- Draft PRs cannot merge through the API: mark ready (`gh pr ready`), re-confirm exact-head CLEAN/MERGEABLE/verify GREEN with zero unresolved threads, then ordinary `--merge` without branch deletion or force-push. +- Slow local executors flake Vitest's default 5s per-test timeout on full-workflow UI tests while exact-head CI stays GREEN. Use `npx vitest run --testTimeout=60000` for the local signal; CI `verify` is authoritative. Detail: `CLAUDE.md`. diff --git a/CLAUDE.md b/CLAUDE.md index 60e4e51..fe1893f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,3 +7,7 @@ Before changing code, read `docs/PRD.md`, `ARCHITECTURE.md`, the applicable ADRs For behavior changes, add or strengthen tests before production code. Reconcile documentation and the product-gap ledger on the same branch. Use the exact current PR head for reviews, checks, and merge decisions, and never bypass governance to compensate for a failing or unassigned check. Deferred `policy_revision` fact triggers evaluate the final commit state. CI restore seeds `retention_status = applies` and `retention_rule` in one transaction; the owner runbook is `db/tests/policy_revision_restore.sh`. + +## Know-how (2026-09-09, exact-head `e1c588f` verified) +- Draft PRs cannot merge through the API (`Pull Request is still a draft`): run `gh pr ready ` first, re-confirm CLEAN/MERGEABLE plus exact-head verify GREEN, then `gh pr merge --merge`. Never delete stacked branches on merge; successors restack with ordinary non-force merge commits. +- Local Vitest defaults to a 5s per-test timeout and flakes on slow executors (full `App` workflow tests exceed it under load while CI verify stays GREEN). Use `npx vitest run --testTimeout=60000` for the local full-suite signal; do not retune production or timeouts to mask executor slowness. CI `verify` on the exact head remains the authoritative verdict. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index cb73159..70021f4 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -73,7 +73,7 @@ The final URL-boundary follow-up found that WHATWG `URL.search` and `URL.hash` a The dependency-governance pass found that every direct npm declaration was either `latest` or a mutable caret range and that TypeScript, Vite, and the React Vite plugin were incorrectly classified as production dependencies. Test-only head `80b69d6a640194997906ec23ca0140f28fd95908` produced exact RED CI `34244619215`: clean install and lint passed, 73 predecessor tests passed, and only the two new pin/classification contracts failed. Minimal implementation `cd9144f4a8a78427457bb58ee4b0a5f75a1ca750` pins all 21 direct packages to their existing reviewed lock resolutions, moves the three build packages to `devDependencies`, and uses npm to regenerate development-scope metadata without changing any resolved package version. A second test-only head `f6bbf9d6e5621ac26e3d1905556c192c2c8613c7` produced exact RED CI `34245711353`: 75 tests passed and only the missing CycloneDX publication contract failed. Workflow repair `a3592da89c75be87564e1d4ec8d9e685e9e3093e` generates a CycloneDX 1.5 SBOM from the exact installed lock graph and uploads it through the existing SHA-pinned artifact action. A final storage review found that placing the SBOM below `test-results` duplicated it in both browser and dependency artifacts. Test-only head `3c8ac646c7ccd0140976f3fcefc914fc4b9a7d80` produced exact RED CI `34246822467` with 75 passing tests and only the new artifact-isolation contract failing; minimal repair `00b89fb6c986b22e2453a58f4b6dc61ba7ff1dff` writes the SBOM once below `dependency-evidence` and uploads only that path as the dependency artifact. Current review follow-up independently checks runtime and development declarations, asserts both artifact destinations, and names all three runtime packages consistently. The lock/license and SBOM evidence support security and legal review but do not themselves approve dependency licenses or replace organization vulnerability gates. -The successor merge below lands this side chain in `develop` order: the above export evidence plus the dependency-governance delta arrive together, preserving both histories without force-push. +The successor landed this side chain in `develop` order via PR #18 (successor head `828ecf1`, parents `8178ba6` + `83fb9f8`, verify `34313155294` GREEN, zero review threads, merged 05:06:01Z): the above export evidence plus the dependency-governance delta arrived together, preserving both histories without force-push. Predecessor merges in this turn: PR #10 (export, head `cef8c0b`, verify `34301731941` GREEN, 7/7 threads resolved, merged 04:32:57Z after `gh pr ready` lifted the draft merge block) and PR #11 (readiness plus pins, head `77bf3c2` after the scope-sentence correction, verify `34311980712` GREEN, 1/1 thread resolved, merged 04:54:21Z). `develop` is now `e1c588f` with zero `latest` declarations; merged-tree local gates are lint GREEN, build GREEN, and 76/76 Vitest GREEN. No PR was closed to reach this state. ## Current baseline @@ -104,7 +104,7 @@ The Proposed aggregate root is the minimal `policy_revision` boundary. The no-co A publishable release requires complete fact-to-warning navigation, deterministic rendering of captured source facts, no inferred customer operational facts, truthful first-responsibility startup/progress, explicit unresolved-review blocking across the seven authoring responsibilities, an explicit no-collection path that does not infer unrelated processing facts, independent explicit retention status, immutable reviewed publication revisions, accessible responsive interaction, authoritative source/version traceability for legal rules, hosted security/privacy evidence, and exact-head CI/security/independent review. Buyer-facing wording describes assistance and reviewability, never a compliance guarantee or an internal implementation boundary. ## Active commercialization order -1. Verify the restacked side chain (PR #10 export delta, PR #11 readiness docs plus SBOM/license evidence) with exact-head CI/security/SAST and independent approval; deliver pins down-chain and clear PR #1's CodeQL-compat failures through the central owner path (sole-maintainer approval is the structural blocker); merge PR #1 only through ordinary protection. The full writer-stack tip already landed in `develop` via PR #16. +1. Re-verify PR #1 on the post-successor `develop` head `e1c588f` (pins now in `develop`, so `dependency-review` is expected to flip; CodeQL-compat failures go through the central owner path); secure independent approval (sole-maintainer approval is the structural blocker); merge PR #1 only through ordinary protection. The full writer-stack tip landed in `develop` via PR #16 and the export plus dependency-governance side chain via PRs #10/#11/#18. 2. Extend the exact-head responsive/accessibility evidence with screen-reader checks, native browser-zoom automation when the harness supports it, and manual records; CSS focus contrast, logical step-focus transfer, focused-heading viewport visibility, retention transitions, effective 200% browser-zoom reflow, and current desktop/tablet/mobile screenshots are already verified. 3. Preserve exact-head PostgreSQL 18 migration, domain-error, two-session lock, restart, and dump/restore evidence, then prove tenant authorization, immutable audit, and encryption before enabling any hosted adapter. 4. Implement secure review/publication with immutable releases and explicit supersession. From 852c6bad42ac6b8f738d874a66e3afb684860301 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 14:22:54 +0900 Subject: [PATCH 277/282] docs: record PR1 exact-head failures on pinned develop tip --- 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 70021f4..43d0925 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -104,7 +104,7 @@ The Proposed aggregate root is the minimal `policy_revision` boundary. The no-co A publishable release requires complete fact-to-warning navigation, deterministic rendering of captured source facts, no inferred customer operational facts, truthful first-responsibility startup/progress, explicit unresolved-review blocking across the seven authoring responsibilities, an explicit no-collection path that does not infer unrelated processing facts, independent explicit retention status, immutable reviewed publication revisions, accessible responsive interaction, authoritative source/version traceability for legal rules, hosted security/privacy evidence, and exact-head CI/security/independent review. Buyer-facing wording describes assistance and reviewability, never a compliance guarantee or an internal implementation boundary. ## Active commercialization order -1. Re-verify PR #1 on the post-successor `develop` head `e1c588f` (pins now in `develop`, so `dependency-review` is expected to flip; CodeQL-compat failures go through the central owner path); secure independent approval (sole-maintainer approval is the structural blocker); merge PR #1 only through ordinary protection. The full writer-stack tip landed in `develop` via PR #16 and the export plus dependency-governance side chain via PRs #10/#11/#18. +1. Re-verify PR #1 on the post-successor `develop` head `e1c588f`: `verify` is GREEN on the exact head, but `dependency-review` still FAILs even with zero `latest` declarations in `develop`, and both CodeQL compatibility analyses still FAIL. Central-workflow logs live in the organization central repository (leaf-repo API returns 404), so the `dependency-review` reason is not inspectable from this repo; route both through the central owner path rather than leaf-side churn. Secure independent approval (sole-maintainer approval is the structural blocker); merge PR #1 only through ordinary protection. The full writer-stack tip landed in `develop` via PR #16 and the export plus dependency-governance side chain via PRs #10/#11/#18. 2. Extend the exact-head responsive/accessibility evidence with screen-reader checks, native browser-zoom automation when the harness supports it, and manual records; CSS focus contrast, logical step-focus transfer, focused-heading viewport visibility, retention transitions, effective 200% browser-zoom reflow, and current desktop/tablet/mobile screenshots are already verified. 3. Preserve exact-head PostgreSQL 18 migration, domain-error, two-session lock, restart, and dump/restore evidence, then prove tenant authorization, immutable audit, and encryption before enabling any hosted adapter. 4. Implement secure review/publication with immutable releases and explicit supersession. From 198fbe093a8f9a5081dcaac7fd3c0310fff6c591 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 15:11:07 +0900 Subject: [PATCH 278/282] docs: cover remaining exported policy symbols with JSDoc --- src/policy.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/policy.ts b/src/policy.ts index f74a770..6f34076 100644 --- a/src/policy.ts +++ b/src/policy.ts @@ -1,7 +1,11 @@ +/** Operator-selected collection basis for one collection item; empty means the operator has not confirmed a basis yet. */ export type CollectionMode = '' | '필수' | '선택' +/** Operator-attested transfer state; empty means the operator has not confirmed either outcome yet. */ export type DisclosureStatus = '' | 'yes' | 'no' +/** Operator-attested retention state; empty means the operator has not confirmed either outcome yet. */ export type RetentionStatus = '' | 'applies' | 'none' +/** One authorable personal-data collection item with its operator-established mode, purpose, and path evidence. */ export type PolicyItem = { id: string label: string @@ -12,6 +16,7 @@ export type PolicyItem = { detail?: string } +/** Operator-authored non-collection facts for the seven-step workspace; blank means unresolved, never none. */ export type DraftFacts = { serviceName: string serviceUrl: string @@ -27,12 +32,14 @@ export type DraftFacts = { privacyOfficerEmail: string } +/** One product-defined authoring gap that blocks readiness until its owning step resolves it. */ export type DraftFinding = { code: string step: number label: string } +/** Fresh-workspace collection catalog; every item starts disabled with no inferred mode or purpose. */ export const initialItems: PolicyItem[] = [ { id: 'name', label: '이름', description: '서비스 이용자 식별', purpose: '', enabled: false, mode: '' }, { id: 'email', label: '이메일 주소', description: '계정 식별, 로그인, 중요 고지 수신', purpose: '', enabled: false, mode: '' }, @@ -45,6 +52,7 @@ export const initialItems: PolicyItem[] = [ { id: 'content', label: '게시물 및 문의 내용', description: '게시 기능, 고객 문의 처리', purpose: '', enabled: false, mode: '' }, ] +/** Fresh-workspace fact state; every value starts blank so readiness fails closed until the operator establishes it. */ export const initialFacts: DraftFacts = { serviceName: '', serviceUrl: '', @@ -60,6 +68,7 @@ export const initialFacts: DraftFacts = { privacyOfficerEmail: '', } +/** Ordered seven authoring responsibilities, from service information through the privacy contact. */ export const steps = ['서비스 정보', '수집 항목', '처리 목적', '보유 기간', '제3자 제공', '국외 이전', '개인정보 보호 담당자'] /** Returns a canonical credential-free HTTP(S) service URL, or null when the address is not admissible. */ From 82ac08d1f1436e24be571e8d6eea1a3ded6c6258 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 15:13:27 +0900 Subject: [PATCH 279/282] docs: record PIPA amendment re-verification ahead of effective date --- docs/research-traceability.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/research-traceability.md b/docs/research-traceability.md index 849fc3f..fefba7f 100644 --- a/docs/research-traceability.md +++ b/docs/research-traceability.md @@ -13,6 +13,10 @@ PolicyWeave treats legal and regulatory material as versioned evidence, not as i | Republic of Korea, Enforcement Decree of the Personal Information Protection Act (개인정보 보호법 시행령), National Law Information Center | Presidential Decree No. 36121, promulgated 2026-02-19 and effective 2026-08-20. Retrieved 2026-09-02; canonical National Law Information Center legal-information sequence 283503. https://www.law.go.kr/LSW/lsSideInfoP.do?docCls=jo&joNo=0032&lsiSeq=283503 | Subordinate-rule authority/effective-date anchor | Registry only; no decree-level rules are encoded yet. Future mappings must record the exact decree article and effective-date snapshot. | | Personal Information Protection Commission. (2026, April 23). 개인정보 처리방침 작성지침(2026.4. 개정) [Guideline for writing privacy policies, April 2026 revision]. | PIPC marks this as the current guide. Retrieved 2026-09-02. https://pipc.go.kr/np/cop/bbs/selectBoardList.do?bbsId=BS217&mCode=D010030000 | Authoring/review guidance and template requirement discovery | Source registered; requirement-by-requirement mapping still required | +## Source re-verification (2026-09-09) + +The National Law Information Center page for the PIPA amendment (Act No. 21445) was re-fetched on 2026-09-09 and still heads the text `개인정보 보호법 [시행 2026. 9. 11.] [법률 제21445호, 2026. 3. 10., 일부개정]`. The register above is therefore unchanged: the general amended provisions remain future-effective until 2026-09-11, and no article-level rule is encoded yet. Next check on or after the effective date; this note records retrieval only and states no legal conclusion. + ## Retention applicability correction An intermediate product repair attempted to treat explicit `개인정보를 수집하지 않음` as sufficient evidence that retention was inapplicable. That implication was rejected before merge evidence because the authoritative PIPC terminology defines `처리` broadly and lists collection, storage, and retention as distinct included acts. Therefore no-collection cannot safely prove no-retention. Test-first commits `734a3d603761fd851f7d8dfc9434d92626518de6`, `dc4f9ad1079f90efb27165099f5dce13ddb4f637`, and `e0f25c0a003df46b487ae564d8fc4995581fbfca` establish the corrected contract: collection state and retention state are independent operator facts. Production commits `6207729b651f259f0c87cab3680bad207a60beda` and `25a75ad3ee5ded8b0e605c2fb551cc93b11ad568` add explicit unresolved/`보유함`/`보유하지 않음` retention status, require a retention period only for `보유함`, clear stale period data when moving away from that state, and project the explicit status without inferring it from no-collection. This remains a product authoring-completeness rule, not a legal-sufficiency conclusion. From 82e556c9ebed73e77a309c47b6861bcc1b302f42 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 15:19:33 +0900 Subject: [PATCH 280/282] docs: record docstring and re-verification turn in ledger --- docs/product-technical-gap-baseline.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 43d0925..1b07623 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -75,6 +75,8 @@ The dependency-governance pass found that every direct npm declaration was eithe The successor landed this side chain in `develop` order via PR #18 (successor head `828ecf1`, parents `8178ba6` + `83fb9f8`, verify `34313155294` GREEN, zero review threads, merged 05:06:01Z): the above export evidence plus the dependency-governance delta arrived together, preserving both histories without force-push. Predecessor merges in this turn: PR #10 (export, head `cef8c0b`, verify `34301731941` GREEN, 7/7 threads resolved, merged 04:32:57Z after `gh pr ready` lifted the draft merge block) and PR #11 (readiness plus pins, head `77bf3c2` after the scope-sentence correction, verify `34311980712` GREEN, 1/1 thread resolved, merged 04:54:21Z). `develop` is now `e1c588f` with zero `latest` declarations; merged-tree local gates are lint GREEN, build GREEN, and 76/76 Vitest GREEN. No PR was closed to reach this state. +This turn landed two independent docs-only deltas without force-push or branch deletion: PR #20 added the 9 missing JSDoc comments in `src/policy.ts` (non-test src export docstring coverage 8/17 to 17/17; diff +9/-0; verify `34317973719` GREEN, merged 06:14:43Z), and PR #21 recorded the 2026-09-09 NLIC re-verification that Act No. 21445 still heads `[시행 2026. 9. 11.]` (verify `34318129844` GREEN, merged 06:18:15Z). PR #1 re-verification on current heads shows `verify`, Noema, OpenCode, Semgrep, Trivy, OSV, and Scorecard GREEN, while `strix` flipped to FAIL on a docs-only delta (central logs uninspectable from the leaf repo, routed via the central owner path), `dependency-review` still FAILs despite zero `latest` declarations, and both CodeQL compatibility analyses still FAIL; independent approval remains the structural blocker. A mid-turn edit briefly mangled a traceability header and was repaired before commit, leaving a +4/-0 diff. + ## Current baseline | Area | Evidence | Status | Commercialization gap | Owner/action | Next verification | @@ -89,7 +91,7 @@ The successor landed this side chain in `develop` order via PR #18 (successor he | 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 | | Dependency supply chain | Exact-pinned manifest, npm-regenerated lock graph, executable pin/scope/license contracts, and CycloneDX 1.5 CI artifact | Repaired foundation on open stack; exact-head release gate still applies | License compatibility approval and organization vulnerability verdict remain external review responsibilities | PolicyWeave owns direct dependency intent; CI/Security owners retain scanning and release gates | Exact-head clean install, 76 tests, build, SBOM artifact/digest, vulnerability checks, and explicit license review | -| Tests | 76 unit/UI/schema/workflow/dependency regressions plus PostgreSQL runtime/concurrency/restore scripts, 23 Playwright/axe passes, ten intentional project-scope skips, real-browser interaction/download/error contracts, and screenshot/SBOM artifact contracts | Improved; bounded exact-head verification required after every head movement | Repository-wide 100% execution/docstring coverage is not yet evidenced | Test Engineering: measure coverage and extend realistic edge cases | Exact-head coverage + browser/database/dependency evidence | +| Tests | 76 unit/UI/schema/workflow/dependency regressions plus PostgreSQL runtime/concurrency/restore scripts, 23 Playwright/axe passes, ten intentional project-scope skips, real-browser interaction/download/error contracts, screenshot/SBOM artifact contracts, and 17/17 non-test src export docstrings | Improved; bounded exact-head verification required after every head movement | Repository-wide 100% execution/branch/boundary coverage is not yet evidenced | Test Engineering: measure coverage and extend realistic edge cases | Exact-head coverage + browser/database/dependency 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 c57ebc78f353d2f82586dfecc4ac7fe7a157c38a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 15:50:54 +0900 Subject: [PATCH 281/282] test: pin policy boundary contracts for URL, contradiction, and email edges --- src/policy-boundary.test.ts | 69 +++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 src/policy-boundary.test.ts diff --git a/src/policy-boundary.test.ts b/src/policy-boundary.test.ts new file mode 100644 index 0000000..0e9d0d7 --- /dev/null +++ b/src/policy-boundary.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, it } from 'vitest' +import { createPolicyExport, getCompletedSteps, getDraftReview, getReview, initialFacts, initialItems } from './policy' + +const reviewReadyBase = { + ...initialFacts, + serviceName: '예시 서비스', + serviceUrl: 'https://example.test', + retentionStatus: 'applies' as const, + retentionPeriod: '회원 탈퇴 시까지', + thirdPartyStatus: 'no' as const, + internationalStatus: 'no' as const, + privacyOfficerName: '개인정보보호 담당', + privacyOfficerEmail: 'privacy@example.test', +} + +describe('policy boundary contracts', () => { + it('canonicalizes uppercase service URL spelling without changing the authored destination', () => { + const exported = createPolicyExport(initialItems, false, { ...initialFacts, serviceUrl: 'HTTPS://EXAMPLE.TEST/Privacy' }) + + expect(exported.policy_facts.service_profile.service_url).toBe('https://example.test/Privacy') + expect(exported.review_finding_codes).not.toContain('service_url_format') + }) + + it('treats a username-only service URL as inadmissible without leaking the username', () => { + const exported = createPolicyExport(initialItems, false, { ...initialFacts, serviceUrl: 'https://operator@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') + }) + + it('counts contradiction alongside per-item blockers instead of masking either', () => { + const items = initialItems.map((item) => item.id === 'phone' ? { ...item, enabled: true } : item) + const review = getReview(items, true) + + expect(review.collectionContradiction).toBe(true) + expect(review.blockingCount).toBe(4) + }) + + it('excludes collection and purpose steps while a no-collection contradiction stands', () => { + const items = initialItems.map((item) => item.id === 'phone' + ? { ...item, enabled: true, mode: '필수' as const, purpose: '본인 확인', detail: '회원가입 화면' } + : item) + const completed = getCompletedSteps(items, true, reviewReadyBase) + + expect(completed.has(2)).toBe(false) + expect(completed.has(3)).toBe(false) + expect(completed.has(4)).toBe(true) + }) + + it('rejects malformed contact emails beyond the missing-at-sign case', () => { + for (const privacyOfficerEmail of ['a@b@c', '@example.test', 'privacy@', 'privacy @example.test']) { + expect(getDraftReview({ ...reviewReadyBase, privacyOfficerEmail }).map((finding) => finding.code)) + .toEqual(['privacy_contact_email_format']) + } + expect(getDraftReview({ ...reviewReadyBase, privacyOfficerEmail: 'privacy@example.test' })).toEqual([]) + }) + + it('exports a null retention period without inventing one when applies has only whitespace', () => { + const exported = createPolicyExport(initialItems, false, { + ...initialFacts, + retentionStatus: 'applies', + retentionPeriod: ' ', + }) + + expect(exported.policy_facts.retention.retention_period).toBeNull() + expect(exported.review_finding_codes).toContain('retention_period') + }) +}) From c8f85102753ea992c16b6a5043933b15bb68ad9d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 15:55:47 +0900 Subject: [PATCH 282/282] docs: record boundary-contract turn and PR1 tracking state --- docs/product-technical-gap-baseline.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 1b07623..417a57e 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -77,6 +77,8 @@ The successor landed this side chain in `develop` order via PR #18 (successor he This turn landed two independent docs-only deltas without force-push or branch deletion: PR #20 added the 9 missing JSDoc comments in `src/policy.ts` (non-test src export docstring coverage 8/17 to 17/17; diff +9/-0; verify `34317973719` GREEN, merged 06:14:43Z), and PR #21 recorded the 2026-09-09 NLIC re-verification that Act No. 21445 still heads `[시행 2026. 9. 11.]` (verify `34318129844` GREEN, merged 06:18:15Z). PR #1 re-verification on current heads shows `verify`, Noema, OpenCode, Semgrep, Trivy, OSV, and Scorecard GREEN, while `strix` flipped to FAIL on a docs-only delta (central logs uninspectable from the leaf repo, routed via the central owner path), `dependency-review` still FAILs despite zero `latest` declarations, and both CodeQL compatibility analyses still FAIL; independent approval remains the structural blocker. A mid-turn edit briefly mangled a traceability header and was repaired before commit, leaving a +4/-0 diff. +This turn added boundary evidence without production change: PR #23 (`src/policy-boundary.test.ts`, +48/-0) pins 6 edge contracts that all passed on first run — uppercase service-URL canonicalization, username-only URL rejection without username leakage, contradiction-plus-blocker counting, step 2/3 exclusion under contradiction, a 4-case email-format matrix, and null export of a whitespace retention period. Suite stands at 82/82 with 17/17 non-test src export docstrings; verify `34320952183` GREEN, merged 06:54:20Z. PR #1 tracking: on head `2d4928d`, `verify` stayed GREEN while `dependency-review` and both CodeQL compatibility analyses still FAIL; `strix` showed one FAIL on `632a08e` then a pending rerun, so its verdict on that head is inconclusive rather than confirmed. The PR #23 merge advanced `develop` to `82e7826`, re-queuing the full central matrix; that verdict is pending and is not claimed here. + ## Current baseline | Area | Evidence | Status | Commercialization gap | Owner/action | Next verification | @@ -91,7 +93,7 @@ This turn landed two independent docs-only deltas without force-push or branch d | 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 | | Dependency supply chain | Exact-pinned manifest, npm-regenerated lock graph, executable pin/scope/license contracts, and CycloneDX 1.5 CI artifact | Repaired foundation on open stack; exact-head release gate still applies | License compatibility approval and organization vulnerability verdict remain external review responsibilities | PolicyWeave owns direct dependency intent; CI/Security owners retain scanning and release gates | Exact-head clean install, 76 tests, build, SBOM artifact/digest, vulnerability checks, and explicit license review | -| Tests | 76 unit/UI/schema/workflow/dependency regressions plus PostgreSQL runtime/concurrency/restore scripts, 23 Playwright/axe passes, ten intentional project-scope skips, real-browser interaction/download/error contracts, screenshot/SBOM artifact contracts, and 17/17 non-test src export docstrings | Improved; bounded exact-head verification required after every head movement | Repository-wide 100% execution/branch/boundary coverage is not yet evidenced | Test Engineering: measure coverage and extend realistic edge cases | Exact-head coverage + browser/database/dependency evidence | +| Tests | 82 unit/UI/schema/workflow/dependency/boundary regressions plus PostgreSQL runtime/concurrency/restore scripts, 23 Playwright/axe passes, ten intentional project-scope skips, real-browser interaction/download/error contracts, screenshot/SBOM artifact contracts, and 17/17 non-test src export docstrings | Improved; bounded exact-head verification required after every head movement | Repository-wide 100% execution/branch/boundary coverage is not yet evidenced | Test Engineering: measure coverage and extend realistic edge cases | Exact-head coverage + browser/database/dependency 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 |