diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 62fc759..79ba22f 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 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 @@ -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: dependency-evidence/policyweave-dependency-sbom.cdx.json + if-no-files-found: error 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..0d9e19b 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`, `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. @@ -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..26d2fe3 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 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 | 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. 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"}} diff --git a/src/dependency-manifest-contract.test.ts b/src/dependency-manifest-contract.test.ts new file mode 100644 index 0000000..e6e8d56 --- /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 = [ + ...Object.entries(packageManifest.dependencies ?? {}), + ...Object.entries(packageManifest.devDependencies ?? {}), + ] + const mutableOrMismatched = 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([]) + }) +}) diff --git a/src/workflow-contract.test.ts b/src/workflow-contract.test.ts index 269bc37..ee84add 100644 --- a/src/workflow-contract.test.ts +++ b/src/workflow-contract.test.ts @@ -11,3 +11,17 @@ 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 > dependency-evidence\/policyweave-dependency-sbom\.cdx\.json/, + ) + 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/, + ) + }) +})