Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 browser can download and restore the exact deterministic schema-v1 JSON draft containing normalized operator-authored facts and readiness finding codes. Restore treats the local file as untrusted input, admits only the closed schema and catalog, and recomputes derived readiness evidence before atomically replacing workspace state. This local portability boundary is not publication, persistence, backup, 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, non-collection authoring-completeness findings, and schema-v1 validation/reconstruction; `src/App.tsx` owns browser orchestration, bounded local file selection, 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 and restore the exact deterministic schema-v1 JSON draft containing normalized operator-authored facts and readiness finding codes. Restore treats the local file as untrusted input, requires strict non-replacing UTF-8 decoding, admits only the closed schema and catalog, and recomputes derived readiness evidence before atomically replacing workspace state. This local portability boundary is not publication, persistence, backup, 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, non-collection authoring-completeness findings, and schema-v1 validation/reconstruction; `src/App.tsx` owns browser orchestration, bounded local file selection, 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.

Expand Down
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ All notable product changes are recorded here. PolicyWeave is pre-release; entri
## Unreleased

### Added
- Fail-closed local schema-v1 draft restore with exact object-shape, canonical-string, collection-catalog, closed-status, contradiction, and derived-evidence validation. Files above 1 MiB are rejected before parsing, invalid files preserve the current workspace, and accepted facts are reconstructed without trusting file-supplied readiness or finding claims. The import and authoring controls are disabled only while a selected file is read and validated, then re-enabled on success or failure so accepted restore cannot overwrite concurrent edits; the visible import control exposes its disabled state and the existing live status region announces `JSON 초안 확인 중` during that interval. The native authoring `fieldset` remains a semantic grid item rather than using `display: contents`. Its local-file control retains a visible high-contrast keyboard focus indicator and 44 px target. The return path performs no network request and does not claim cross-version migration, persistence, backup, publication, or legal approval.
- Fail-closed local schema-v1 draft restore with exact object-shape, canonical-string, collection-catalog, closed-status, contradiction, and derived-evidence validation. Files above 1 MiB are rejected before parsing, invalid files preserve the current workspace, and accepted facts are reconstructed without trusting file-supplied readiness or finding claims. The import and authoring controls are disabled only while a selected file is read and validated; the visible import control exposes its disabled state and the existing live status region announces `JSON 초안 확인 중`. A keyboard-operable cancel action invokes the browser stream reader's `cancel()`, immediately restores editing, transfers focus to the active authoring control, releases the reader lock, and invalidates the attempt so a late success, error, or cleanup cannot replace current facts or feedback. Incremental strict UTF-8 decoding preserves valid characters split across stream chunks and rejects ill-formed byte sequences instead of inserting replacement characters into operator facts. Tablet-width pending controls wrap, and unbroken document names can shrink and wrap without widening the document. The native authoring `fieldset` remains a semantic grid item rather than using `display: contents`. Its local-file control retains a visible high-contrast keyboard focus indicator and 44 px target. The return path performs no network request and does not claim operating-system interruption beyond the browser API, cross-version migration, persistence, backup, publication, or legal approval.
- Runtime categorical-status admission regression matrix: 64 invalid-input cases, all 16 valid collection/retention/transfer combinations and disabled-item isolation. The matrix verifies stable owning findings, incomplete readiness, null export of unsupported statuses, source non-mutation and deterministic valid projections; ADR-0004 binds its hosted RED and bounded local verification without claiming a released external interoperability or cross-version migration contract.
- 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.
Expand Down
27 changes: 22 additions & 5 deletions docs/ADR-0005-local-draft-restore.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
- Status: Proposed
- Date: 2026-09-26
- Owner: Policy Fact Authoring
- Scope: `src/policy.ts`, `src/App.tsx`, schema-v1 local portability
- Evidence: RED commits `86d370b1`, `0088d686`, `8be9e8dc`, and `7bedeca1`; implementation commits `d5ab6bd2`, `768e1c85`, and `50afb82b`; browser contract commit `95b34a18`
- Scope: `src/policy.ts`, `src/local-draft-reader.ts`, `src/App.tsx`, schema-v1 local portability
- Evidence: RED commits `86d370b1`, `0088d686`, `8be9e8dc`, `7bedeca1`, and `80718969`; implementation commits `d5ab6bd2`, `768e1c85`, `50afb82b`, and `5e878b68`; browser contract commit `95b34a18`

## Problem

Expand All @@ -17,7 +17,8 @@ This decision does not introduce hosted persistence, publication, legal approval
- Structured operator facts remain authoritative; exported readiness and finding fields are derived evidence.
- Unknown properties, missing properties, unsupported versions, wrong runtime types, non-canonical strings/URLs, duplicate/unknown collection keys, catalog-label mismatches, and contradictory facts fail closed.
- Collection, retention, and transfer statuses use exact existing vocabulary without case, whitespace, or type coercion.
- The browser must not replace current work until the complete file has passed validation, and authoring controls must remain locked while that validation is pending so accepted state cannot overwrite concurrent edits.
- File bytes must be well-formed UTF-8. Decoding must not replace malformed sequences with U+FFFD and then admit the altered value as an operator fact.
- The browser must not replace current work until the complete file has passed validation. Authoring controls remain locked while validation is pending, but the operator can cancel that attempt; a late success or failure from the invalidated attempt must not replace facts, overwrite the cancellation message, or relock/unlock a newer attempt.
- Import remains local and bounded to 1 MiB; it performs no network request and adds no dependency.
- Current Korean catalog labels are identity-checked for schema-v1. Future localized resource releases require a new reviewed compatibility decision rather than weakening this check.

Expand All @@ -33,7 +34,7 @@ This decision does not introduce hosted persistence, publication, legal approval

`restorePolicyExport(unknown)` validates the exact schema-v1 object graph and reconstructs browser workspace state only from admitted facts. The canonical built-in collection catalog supplies label and description authority; the file may select catalog keys and supply their operator-authored mode, path, and purpose, but may not define new items or rename existing ones.

After reconstruction, PolicyWeave runs `createPolicyExport` again. Imported `document_state` and ordered `review_finding_codes` must match the recomputed result. The UI reads at most one 1 MiB local file, disables the import input and authoring fieldset while reading and validating, applies all state setters only after validation succeeds, unlocks on success or failure, returns to the first authoring step, and reports success or a retry action through the existing live status output.
After reconstruction, PolicyWeave runs `createPolicyExport` again. Imported `document_state` and ordered `review_finding_codes` must match the recomputed result. The UI reads at most one 1 MiB local file through its browser `ReadableStream` and one incremental `TextDecoder('utf-8', { fatal: true })`, so valid multibyte characters may cross chunks while malformed byte sequences reject the attempt. It disables the import input and authoring fieldset while reading and validating, and exposes a keyboard-operable cancel action outside that fieldset. Each attempt owns both an `AbortController` and a monotonically invalidated token. Cancellation reaches the active stream reader through `cancel()`, while success, error, and cleanup effects run only for the current token. The two safeguards immediately unlock the unchanged workspace, move focus from the removed cancel button to the current step's first enabled authoring control, release the browser reader lock, and prevent a late result from applying. This contract proves browser stream cancellation; it does not claim operating-system interruption beyond the browser API.

## User, operations, and failure scenes

Expand All @@ -43,6 +44,8 @@ After reconstruction, PolicyWeave runs `createPolicyExport` again. Imported `doc
- A file with an unknown collection key, duplicate key, mismatched label, non-canonical string/URL, uppercase status, extra property, or contradictory no-collection state is rejected with bounded user guidance rather than partially applied.
- A file larger than 1 MiB is rejected before JSON parsing. The limit bounds local memory/parse work; it is not a general upload or denial-of-service guarantee.
- While a selected file is still being read, authoring inputs cannot accept changes that a later successful restore would overwrite. A failed read or validation unlocks the unchanged workspace for correction and retry.
- An operator cancels a stalled read, hears the cancellation through the existing live output, resumes editing from the current step's first enabled control instead of losing focus to the document, and is not overwritten when the old browser promise later resolves.
- A valid UTF-8 character split across stream chunks is reconstructed by one incremental decoder rather than corrupted at chunk boundaries. An ill-formed byte sequence fails the entire attempt instead of becoming replacement characters in customer facts. Cancellation invokes the underlying browser reader, releases its lock, and still rejects a non-conforming late chunk as an aborted attempt.

## Evidence

Expand All @@ -56,6 +59,20 @@ Pending-feedback test-only commit `3e2eba61e7e4f02c5ac8b3f9ee23895d515a37b3` the

Semantic-fieldset test-only commit `52d252a2c0c46b12c6cecdebd3dcc67940322bde` reproduced the remaining accessibility risk by failing while `.editing-lock` used `display: contents`. Implementation `9d540895569ab945a087bed99c7d4906b82ae532` keeps the native disabled/`aria-busy` fieldset as the middle grid item, resets only its user-agent box, and gives the contained editing panel the grid item's height so bounded scrolling remains available. Focused style/import validation passed 10/10 locally; hosted browser and assistive-technology evidence remain separate gates.

Cancellation test-only commit `88234a88a591ee2c6367dd6e7c198723423ec79d` reproduces the missing cancel action while a controlled `File.text()` promise remains pending and specifies that the current facts, live message, and unlocked controls survive a late valid result. It also adds one desktop Chromium keyboard/live-region contract. Minimal implementation `84aa45a1f629a04986f2bfe31f0557ed04968fd6` gives each attempt a ref-backed token, gates success/error/finally effects on that token, and clears the file input when cancellation invalidates it. The focused Vitest import matrix passed 5/5 locally. Chromium execution remains a hosted exact-head gate because the local Playwright browser binary is unavailable.

CodeRabbit exact-head review finding `4111539873` identified that removing the focused cancel button left keyboard focus on the document. Test-only commit `e75f8b463b9781bc2defe0ba7f98e46ff1fb7afd` adds jsdom and desktop Chromium assertions for returning focus to the active service-name input; the focused Vitest matrix failed 1/5 with `document.activeElement` equal to `body`. The minimal repair scopes a ref to the authoring fieldset and, after React unlocks it, focuses its first enabled input, select, textarea, or button. Local verification on the final tree passed documentation/configuration contracts 6/6, Vitest 176/176, ESLint, TypeScript/Vite build, and diff checking. Hosted exact-head browser evidence and independent approval remain separate gates.

Pending-state reflow test-only commit `7ea567bdcacb7ab64d24711afc1ef812afecf8aa` exposed that only the <=720 px layout wrapped the topbar even though cancellation adds another transient control. The CSS contract failed 1/7 because the 1300 px media block had no topbar wrap rule. Minimal repair `9623449f35719c0e959f4cdbd8a881fdcf4ad97c` reuses native flex wrapping at that existing breakpoint and extends the real-browser cancellation case to the tablet profile with a document-width overflow assertion. The CSS contract passes 7/7 and the production build succeeds locally. Playwright test discovery reached the tablet case, but local Chromium launch stopped before page execution because the browser binary is unavailable; hosted exact-head execution remains authoritative.

Long-name reflow test-only commit `66ae499b0105359c5bc0faecdd07209e1b2ad475` narrows the preceding claim: flex wrapping alone cannot shrink an automatic-minimum flex item when `service_name` is one long unspaced token. It adds an exact CSS contract and changes the desktop/tablet browser case to a 320-character unbroken name; the CSS contract failed 1/8 before production changed. Minimal repair `803cd609203f3e01d5f4be16a653ef3671143a9a` applies `min-width: 0` and `overflow-wrap: anywhere` only to the document-name item at the existing tablet breakpoint. The CSS contract passes 8/8 and the production build succeeds locally. Actual browser execution remains a hosted exact-head gate because the local Chromium binary is unavailable.

Stream-source RED `807189694322a7620e8c42aa0799e9cfc4957226` failed at module resolution because no abortable reader existed. Minimal implementation `5e878b68824dac8f3be8b56048050a37d66f4734` adds the dependency-free incremental reader, connects one `AbortController` to each UI attempt, and changes the browser contract to observe the underlying stream `cancel()` callback. Local verification passed preview contracts 6/6, Vitest 181/181 across 17 files, ESLint, the TypeScript/Vite production build, Playwright discovery of 39 cases, and diff checking. Local execution of the changed Playwright case stopped before page execution because the Chromium binary is absent; hosted exact-head browser and security results plus independent approval remain merge gates.

CodeRabbit review found that the UTF-8 fixture split only the trailing ASCII quote and brace, so it could not detect removal of incremental decoder state. Test repair `234d7e9aa8a49cc1c90e6510275b564a12b04b20` moves the boundary inside the final three-byte Korean character. A local mutation that removed `{ stream: true }` then failed 1/3 focused reader cases with `정책` decoded as `정��`; restoring the production decoder passed the focused 3/3 and the full 181/181 Vitest suite, preview contracts 6/6, ESLint, the TypeScript/Vite production build, and diff checking.

The current repair adds an exact raw-byte regression in which `0xC3 0x28` previously decoded to `U+FFFD` plus `(` and returned successfully. Strict fatal decoding makes the focused reader suite reject that sequence while retaining split-character reconstruction and cancellation behavior. Hosted exact-head verification and independent review remain required.

## Consequences and follow-up

PolicyWeave now owns a deterministic local export/restore round trip for schema-v1. This closes the missing current-version return path, not version migration. Any schema-v2 work must define explicit migration, loss reporting, compatibility fixtures, and rollback behavior. DB-backed versioned ko/en/ja/zh/vi/es/de/fr resources remain a separate owner contract; schema-v1 catalog-label identity must not be relaxed by embedding a full translation catalog in the browser.
PolicyWeave now owns a deterministic local export/restore round trip for schema-v1, including browser stream cancellation and token-invalidated stale result effects. This closes the missing current-version return path and the bounded underlying browser-reader cancellation gap, not version migration or operating-system-level interruption beyond the browser API. Any schema-v2 work must define explicit migration, loss reporting, compatibility fixtures, and rollback behavior. DB-backed versioned ko/en/ja/zh/vi/es/de/fr resources remain a separate owner contract; schema-v1 catalog-label identity must not be relaxed by embedding a full translation catalog in the browser.
Loading
Loading